Merge release-0.28.17 from 'tools/mic'
[platform/upstream/mic.git] / mic / imager / loop.py
1 #!/usr/bin/python -tt
2 #
3 # Copyright (c) 2011 Intel, Inc.
4 #
5 # This program is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by the Free
7 # Software Foundation; version 2 of the License
8 #
9 # This program is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 # for more details.
13 #
14 # You should have received a copy of the GNU General Public License along
15 # with this program; if not, write to the Free Software Foundation, Inc., 59
16 # Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17
18 import os
19 import sys
20 import glob
21 import shutil
22
23 from mic import kickstart, msger
24 from mic.utils.errors import CreatorError, MountError, VdfsError
25 from mic.utils import misc, runner, fs_related as fs
26 from mic.imager.baseimager import BaseImageCreator
27 from mic.archive import packing, compressing
28
29
30 # The maximum string length supported for LoopImageCreator.fslabel
31 FSLABEL_MAXLEN = 32
32 # Support for Read-only file system list
33 AFTER_MNT_FS = {"squashfs":".sqsh", "vdfs":".vdfs"}
34
35 def save_mountpoints(fpath, loops, arch = None):
36     """Save mount points mapping to file
37
38     :fpath, the xml file to store partition info
39     :loops, dict of partition info
40     :arch, image arch
41     """
42
43     if not fpath or not loops:
44         return
45
46     from xml.dom import minidom
47     doc = minidom.Document()
48     imgroot = doc.createElement("image")
49     doc.appendChild(imgroot)
50     if arch:
51         imgroot.setAttribute('arch', arch)
52     for loop in loops:
53         part = doc.createElement("partition")
54         imgroot.appendChild(part)
55         for (key, val) in loop.items():
56             if isinstance(val, fs.Mount):
57                 continue
58             part.setAttribute(key, str(val))
59
60     with open(fpath, 'w') as wf:
61         wf.write(doc.toprettyxml(indent='  '))
62
63     return
64
65 def load_mountpoints(fpath):
66     """Load mount points mapping from file
67
68     :fpath, file path to load
69     """
70
71     if not fpath:
72         return
73
74     from xml.dom import minidom
75     mount_maps = []
76     with open(fpath, 'r') as rf:
77         dom = minidom.parse(rf)
78     imgroot = dom.documentElement
79     for part in imgroot.getElementsByTagName("partition"):
80         p  = dict(part.attributes.items())
81
82         try:
83             mp = (p['mountpoint'], p['label'], p['name'],
84                   int(p['size']), p['fstype'])
85         except KeyError:
86             msger.warning("Wrong format line in file: %s" % fpath)
87         except ValueError:
88             msger.warning("Invalid size '%s' in file: %s" % (p['size'], fpath))
89         else:
90             mount_maps.append(mp)
91
92     return mount_maps
93
94 class LoopImageCreator(BaseImageCreator):
95     """Installs a system into a loopback-mountable filesystem image.
96
97     LoopImageCreator is a straightforward ImageCreator subclass; the system
98     is installed into an ext3 filesystem on a sparse file which can be
99     subsequently loopback-mounted.
100
101     When specifying multiple partitions in kickstart file, each partition
102     will be created as a separated loop image.
103     """
104     img_format = 'loop'
105
106     def __init__(self, creatoropts=None, pkgmgr=None,
107                  compress_image=None,
108                  shrink_image=False):
109         """Initialize a LoopImageCreator instance.
110
111         This method takes the same arguments as ImageCreator.__init__()
112         with the addition of:
113
114         fslabel -- A string used as a label for any filesystems created.
115         """
116
117         BaseImageCreator.__init__(self, creatoropts, pkgmgr)
118
119         self.compress_image = compress_image
120         self.shrink_image = shrink_image
121
122         self.__fslabel = None
123         self.fslabel = self.name
124
125         self.__blocksize = 4096
126         if self.ks:
127             self.__fstype = kickstart.get_image_fstype(self.ks,
128                                                        "ext3")
129             self.__fsopts = kickstart.get_image_fsopts(self.ks,
130                                                        "defaults,noatime")
131             if self.__fstype in AFTER_MNT_FS.keys():
132                 self.__fstype = "ext4"
133
134             allloops = []
135             for part in sorted(kickstart.get_partitions(self.ks),
136                                key=lambda p: p.mountpoint):
137                 aft_fstype = None
138                 if part.fstype == "swap":
139                     continue
140                 elif part.fstype in AFTER_MNT_FS.keys():
141                     aft_fstype = part.fstype
142                     part.fstype = "ext4"
143
144                 label = part.label
145                 fslabel = part.fslabel
146                 if fslabel == '':
147                     fslabel = label
148                 mp = part.mountpoint
149                 if mp == '/':
150                     # the base image
151                     if not label:
152                         label = self.name
153                 else:
154                     mp = mp.rstrip('/')
155                     if not label:
156                         msger.warning('no "label" specified for loop img at %s'
157                                       ', use the mountpoint as the name' % mp)
158                         label = mp.split('/')[-1]
159
160                 imgname = misc.strip_end(label, '.img') + '.img'
161                 allloops.append({
162                     'mountpoint': mp,
163                     'label': label,
164                     'fslabel':fslabel,
165                     'name': imgname,
166                     'size': part.size or 4096 * 1024 * 1024,
167                     'fstype': part.fstype or 'ext3',
168                     'fsopts': part.fsopts or None,
169                     'aft_fstype': aft_fstype or None,
170                     'extopts': part.extopts or None,
171                     'f2fsopts': part.f2fsopts or None,
172                     'vdfsopts': part.vdfsopts or None,
173                     'squashfsopts': part.squashfsopts or None,
174                     'squashfsoptions_maxsize': part.squashfsoptions_maxsize or None,
175                     'cpioopts': part.cpioopts or None,
176                     'loop': None,  # to be created in _mount_instroot
177                     'uuid': part.uuid or None,
178                     'kspart' : part,
179                     'exclude_image' : part.exclude_image or None,
180                     'no_shrink': part.no_shrink or False,
181                     'init_expand': part.init_expand or False,
182                     })
183             self._instloops = allloops
184
185         else:
186             self.__fstype = None
187             self.__fsopts = None
188             self._instloops = []
189
190         self._imgdir = None
191         self._umountdir = None
192
193         if self.ks:
194             self.__image_size = kickstart.get_image_size(self.ks,
195                                                          4096 * 1024 * 1024)
196         else:
197             self.__image_size = 0
198
199         self._img_name = self.name + ".img"
200
201     def get_image_names(self):
202         if not self._instloops:
203             return None
204
205         names = []
206         for lo in self._instloops :
207             names.append(lo['name'])
208             for ro in AFTER_MNT_FS.values():
209                 names.append(lo['name'].replace('.img',ro))
210         return list(set(names))
211
212     def _set_fstype(self, fstype):
213         self.__fstype = fstype
214
215     def _set_image_size(self, imgsize):
216         self.__image_size = imgsize
217
218
219     #
220     # Properties
221     #
222     def __get_fslabel(self):
223         if self.__fslabel is None:
224             return self.name
225         else:
226             return self.__fslabel
227     def __set_fslabel(self, val):
228         if val is None:
229             self.__fslabel = None
230         else:
231             self.__fslabel = val[:FSLABEL_MAXLEN]
232     #A string used to label any filesystems created.
233     #
234     #Some filesystems impose a constraint on the maximum allowed size of the
235     #filesystem label. In the case of ext3 it's 16 characters, but in the case
236     #of ISO9660 it's 32 characters.
237     #
238     #mke2fs silently truncates the label, but mkisofs aborts if the label is
239     #too long. So, for convenience sake, any string assigned to this attribute
240     #is silently truncated to FSLABEL_MAXLEN (32) characters.
241     fslabel = property(__get_fslabel, __set_fslabel)
242
243     def __get_image(self):
244         if self._imgdir is None:
245             raise CreatorError("_image is not valid before calling mount()")
246         return os.path.join(self._imgdir, self._img_name)
247     #The location of the image file.
248     #
249     #This is the path to the filesystem image. Subclasses may use this path
250     #in order to package the image in _stage_final_image().
251     #
252     #Note, this directory does not exist before ImageCreator.mount() is called.
253     #
254     #Note also, this is a read-only attribute.
255     _image = property(__get_image)
256
257     def __get_blocksize(self):
258         return self.__blocksize
259     def __set_blocksize(self, val):
260         if self._instloops:
261             raise CreatorError("_blocksize must be set before calling mount()")
262         try:
263             self.__blocksize = int(val)
264         except ValueError:
265             raise CreatorError("'%s' is not a valid integer value "
266                                "for _blocksize" % val)
267     #The block size used by the image's filesystem.
268     #
269     #This is the block size used when creating the filesystem image. Subclasses
270     #may change this if they wish to use something other than a 4k block size.
271     #
272     #Note, this attribute may only be set before calling mount().
273     _blocksize = property(__get_blocksize, __set_blocksize)
274
275     def __get_fstype(self):
276         return self.__fstype
277     def __set_fstype(self, val):
278         if val != "ext2" and val != "ext3":
279             raise CreatorError("Unknown _fstype '%s' supplied" % val)
280         self.__fstype = val
281     #The type of filesystem used for the image.
282     #
283     #This is the filesystem type used when creating the filesystem image.
284     #Subclasses may change this if they wish to use something other ext3.
285     #
286     #Note, only ext2 and ext3 are currently supported.
287     #
288     #Note also, this attribute may only be set before calling mount().
289     _fstype = property(__get_fstype, __set_fstype)
290
291     def __get_fsopts(self):
292         return self.__fsopts
293     def __set_fsopts(self, val):
294         self.__fsopts = val
295     #Mount options of filesystem used for the image.
296     #
297     #This can be specified by --fsoptions="xxx,yyy" in part command in
298     #kickstart file.
299     _fsopts = property(__get_fsopts, __set_fsopts)
300
301
302     #
303     # Helpers for subclasses
304     #
305     def _resparse(self, size=None):
306         """Rebuild the filesystem image to be as sparse as possible.
307
308         This method should be used by subclasses when staging the final image
309         in order to reduce the actual space taken up by the sparse image file
310         to be as little as possible.
311
312         This is done by resizing the filesystem to the minimal size (thereby
313         eliminating any space taken up by deleted files) and then resizing it
314         back to the supplied size.
315
316         size -- the size in, in bytes, which the filesystem image should be
317                 resized to after it has been minimized; this defaults to None,
318                 causing the original size specified by the kickstart file to
319                 be used (or 4GiB if not specified in the kickstart).
320         """
321         minsize = 0
322         for item in self._instloops:
323             if not item['cpioopts']:
324                 if item['no_shrink']:
325                     item['loop'].resparse()
326                     continue
327                 if item['name'] == self._img_name:
328                     minsize = item['loop'].resparse(size)
329                 else:
330                     item['loop'].resparse(size)
331
332         return minsize
333
334     def _base_on(self, base_on=None):
335         if base_on and self._image != base_on:
336             shutil.copyfile(base_on, self._image)
337
338     def _check_imgdir(self):
339         if self._imgdir is None:
340             self._imgdir = self._mkdtemp()
341
342
343     #
344     # Actual implementation
345     #
346     def _mount_instroot(self, base_on=None):
347         if base_on and os.path.isfile(base_on):
348             self._imgdir = os.path.dirname(base_on)
349             imgname = os.path.basename(base_on)
350             self._base_on(base_on)
351             self._set_image_size(misc.get_file_size(self._image))
352
353             # here, self._instloops must be []
354             self._instloops.append({
355                  "mountpoint": "/",
356                  "label": self.name,
357                  "name": imgname,
358                  "size": self.__image_size or 4096,
359                  "fstype": self.__fstype or "ext3",
360                  "f2fsopts": None,
361                  "extopts": None,
362                  "loop": None,
363                  "uuid": None,
364                  "kspart": None,
365                  "exclude_image" : None
366                  })
367
368         self._check_imgdir()
369
370         for loop in self._instloops:
371             fstype = loop['fstype']
372             fsopt = loop['fsopts']
373             mp = os.path.join(self._instroot, loop['mountpoint'].lstrip('/'))
374             size = loop['size'] * 1024 * 1024
375             imgname = loop['name']
376
377             if fstype in ("ext2", "ext3", "ext4"):
378                 MyDiskMount = fs.ExtDiskMount
379             elif fstype == "btrfs":
380                 MyDiskMount = fs.BtrfsDiskMount
381             elif fstype in ("vfat", "msdos"):
382                 MyDiskMount = fs.VfatDiskMount
383             elif fstype == "f2fs":
384                 MyDiskMount = fs.F2fsDiskMount
385             else:
386                 raise MountError('Cannot support fstype: %s' % fstype)
387
388             loop['loop'] = MyDiskMount(fs.SparseLoopbackDisk(
389                                            os.path.join(self._imgdir, imgname),
390                                            size),
391                                        mp,
392                                        fstype,
393                                        self._blocksize,
394                                        loop['fslabel'],
395                                        fsopt,
396                                        fsuuid = loop['uuid'])
397
398             if fstype in ("ext2", "ext3", "ext4"):
399                 loop['loop'].extopts = loop['extopts']
400             elif fstype == "f2fs":
401                 loop['loop'].f2fsopts = loop['f2fsopts']
402
403             try:
404                 msger.verbose('Mounting image "%s" on "%s"' % (imgname, mp))
405                 fs.makedirs(mp)
406                 loop['loop'].mount(fsopt, init_expand=loop['init_expand'])
407                 # Make an autogenerated uuid avaialble in _get_post_scripts_env()
408                 if loop['kspart'] and loop['kspart'].uuid is None and \
409                    loop['loop'].uuid:
410                     loop['kspart'].uuid = loop['loop'].uuid
411
412             except MountError as e:
413                 raise
414
415     def _unmount_instroot(self):
416         for item in reversed(self._instloops):
417             try:
418                 item['loop'].cleanup()
419             except:
420                 pass
421
422     def _get_sign_scripts_env(self):
423         env = BaseImageCreator._get_sign_scripts_env(self)
424
425         # Directory path of %post-umounts scripts
426         if self._umountdir:
427             env['UMOUNT_SCRIPTS_PATH'] = str(self._umountdir)
428
429         return env
430
431     def _stage_final_image(self):
432
433         if self.pack_to or self.shrink_image:
434             self._resparse(0)
435         else:
436             self._resparse()
437
438         for item in self._instloops:
439             imgfile = os.path.join(self._imgdir, item['name'])
440
441             if item['aft_fstype'] in AFTER_MNT_FS.keys():
442                 mountpoint = misc.mkdtemp()
443                 ext4img = os.path.join(self._imgdir, item['name'])
444                 runner.show('mount -t ext4 %s %s' % (ext4img, mountpoint))
445                 runner.show('ls -al %s' % (mountpoint))
446 #                item['loop'].mount(None, 'not_create')
447 #                point_mnt = os.path.join(self._instroot, item['mountpoint'].lstrip('/'))
448
449                 fs_suffix = AFTER_MNT_FS[item['aft_fstype']]
450                 if item['aft_fstype'] == "squashfs":
451 #                    fs.mksquashfs(mountpoint, self._outdir+"/"+item['label']+fs_suffix)
452                     args = "mksquashfs " + mountpoint + " " + self._imgdir+"/"+item['label']+fs_suffix
453                     if item['squashfsopts']:
454                         squashfsopts=item['squashfsopts'].replace(',', ' ')
455                         runner.show("mksquashfs --help")
456                         runner.show("%s %s" % (args, squashfsopts))
457                     else:
458                         runner.show("%s " % args)
459
460                     if item['squashfsoptions_maxsize']:
461                         squashfsoptions_maxsize=int(item['squashfsoptions_maxsize']) * 1024 * 1024
462                         imgsize = os.stat(self._imgdir+"/"+item['label']+fs_suffix).st_size
463                         if imgsize > squashfsoptions_maxsize:
464                             msger.error("squashfs img size is too large (%d > %d)" % (imgsize, squashfsoptions_maxsize))
465                             sys.exit()
466
467                 if item['aft_fstype'] == "vdfs":
468                     ##FIXME temporary code - replace this with fs.mkvdfs()
469                     if item['vdfsopts']:
470                         vdfsopts=item['vdfsopts'].replace(',', ' ')
471                     else:
472                         vdfsopts="-i -z 1024M"
473
474                     fullpathmkvdfs = "mkfs.vdfs" #find_binary_path("mkfs.vdfs")
475                     runner.show("%s --help" % fullpathmkvdfs)
476 #                    fs.mkvdfs(mountpoint, self._outdir+"/"+item['label']+fs_suffix, vdfsopts)
477                     ret = runner.show('%s %s -r %s %s' % (fullpathmkvdfs, vdfsopts, mountpoint, self._imgdir+"/"+item['label']+fs_suffix))
478                     if ret != 0:
479                         runner.show("mkfs.vdfs return error")
480                         raise VdfsError("' %s' exited with error (%d)" % (fullpathmkvdfs, ret))
481
482                 runner.show('umount %s' % mountpoint)
483 #               os.unlink(mountpoint)
484                 runner.show('mv %s %s' % (self._imgdir+"/"+item['label']+fs_suffix, self._imgdir+"/"+item['label']+".img") )
485                 runner.show('ls -al %s' % self._imgdir)
486
487             if item['fstype'] == "ext4":
488                 if not item['cpioopts']:
489                     runner.show('/sbin/tune2fs -O ^huge_file,extents,uninit_bg %s '
490                             % imgfile)
491                     runner.quiet(["/sbin/e2fsck", "-f", "-y", imgfile])
492             self.image_files.setdefault('partitions', {}).update(
493                     {item['mountpoint']: item['label']})
494             if self.compress_image:
495                 compressing(imgfile, self.compress_image)
496                 self.image_files.setdefault('image_files', []).append(
497                                 '.'.join([item['name'], self.compress_image]))
498             else:
499                 self.image_files.setdefault('image_files', []).append(item['name'])
500
501         for item in os.listdir(self._imgdir):
502             imgfile = os.path.join(self._imgdir, item)
503             imgsize = os.path.getsize(imgfile)
504             msger.info("filesystem size of %s : %s bytes" % (item, imgsize))
505
506         self.run_sign_scripts()
507         if not self.pack_to:
508             for item in os.listdir(self._imgdir):
509                 shutil.move(os.path.join(self._imgdir, item),
510                             os.path.join(self._outdir, item))
511         else:
512             msger.info("Pack all loop images together to %s" % self.pack_to)
513             dstfile = os.path.join(self._outdir, self.pack_to)
514             packing(dstfile, self._imgdir)
515             self.image_files['image_files'] = [self.pack_to]
516
517
518         if self.pack_to:
519             mountfp_xml = os.path.splitext(self.pack_to)[0]
520             mountfp_xml = misc.strip_end(mountfp_xml, '.tar') + ".xml"
521         else:
522             mountfp_xml = self.name + ".xml"
523         # save mount points mapping file to xml
524         save_mountpoints(os.path.join(self._outdir, mountfp_xml),
525                          self._instloops,
526                          self.target_arch)
527
528     def copy_attachment(self):
529         if not hasattr(self, '_attachment') or not self._attachment:
530             return
531
532         self._check_imgdir()
533
534         msger.info("Copying attachment files...")
535         for item in self._attachment:
536             if not os.path.exists(item):
537                 continue
538             dpath = os.path.join(self._imgdir, os.path.basename(item))
539             msger.verbose("Copy attachment %s to %s" % (item, dpath))
540             shutil.copy(item, dpath)
541
542     def move_post_umount_scripts(self):
543         scripts_dir = self._instroot + "/var/tmp/post_umount_scripts"
544         if not os.path.exists(scripts_dir):
545             return
546         self._umountdir = self._mkdtemp("umount")
547         msger.info("Moving post umount scripts...")
548         for item in os.listdir(scripts_dir):
549             spath = os.path.join(scripts_dir, item)
550             dpath = os.path.join(self._umountdir, item)
551             msger.verbose("Move post umount scripts %s to %s" % (spath, dpath))
552             shutil.move(spath, dpath)
553         shutil.rmtree(scripts_dir)
554
555     def postinstall(self):
556         BaseImageCreator.postinstall(self)
557         self.move_post_umount_scripts()
558
559     def create_manifest(self):
560         if self.compress_image:
561             self.image_files.update({'compress': self.compress_image})
562         super(LoopImageCreator, self).create_manifest()