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