eab83b2134c630a0642fb9ce29823a85225046be
[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                     })
173             self._instloops = allloops
174
175         else:
176             self.__fstype = None
177             self.__fsopts = None
178             self._instloops = []
179
180         self._imgdir = None
181
182         if self.ks:
183             self.__image_size = kickstart.get_image_size(self.ks,
184                                                          4096L * 1024 * 1024)
185         else:
186             self.__image_size = 0
187
188         self._img_name = self.name + ".img"
189
190     def get_image_names(self):
191         if not self._instloops:
192             return None
193
194         names = []
195         for lo in self._instloops :
196             names.append(lo['name'])
197             for ro in AFTER_MNT_FS.values():
198                 names.append(lo['name'].replace('.img',ro))
199         return list(set(names))
200
201     def _set_fstype(self, fstype):
202         self.__fstype = fstype
203
204     def _set_image_size(self, imgsize):
205         self.__image_size = imgsize
206
207
208     #
209     # Properties
210     #
211     def __get_fslabel(self):
212         if self.__fslabel is None:
213             return self.name
214         else:
215             return self.__fslabel
216     def __set_fslabel(self, val):
217         if val is None:
218             self.__fslabel = None
219         else:
220             self.__fslabel = val[:FSLABEL_MAXLEN]
221     #A string used to label any filesystems created.
222     #
223     #Some filesystems impose a constraint on the maximum allowed size of the
224     #filesystem label. In the case of ext3 it's 16 characters, but in the case
225     #of ISO9660 it's 32 characters.
226     #
227     #mke2fs silently truncates the label, but mkisofs aborts if the label is
228     #too long. So, for convenience sake, any string assigned to this attribute
229     #is silently truncated to FSLABEL_MAXLEN (32) characters.
230     fslabel = property(__get_fslabel, __set_fslabel)
231
232     def __get_image(self):
233         if self._imgdir is None:
234             raise CreatorError("_image is not valid before calling mount()")
235         return os.path.join(self._imgdir, self._img_name)
236     #The location of the image file.
237     #
238     #This is the path to the filesystem image. Subclasses may use this path
239     #in order to package the image in _stage_final_image().
240     #
241     #Note, this directory does not exist before ImageCreator.mount() is called.
242     #
243     #Note also, this is a read-only attribute.
244     _image = property(__get_image)
245
246     def __get_blocksize(self):
247         return self.__blocksize
248     def __set_blocksize(self, val):
249         if self._instloops:
250             raise CreatorError("_blocksize must be set before calling mount()")
251         try:
252             self.__blocksize = int(val)
253         except ValueError:
254             raise CreatorError("'%s' is not a valid integer value "
255                                "for _blocksize" % val)
256     #The block size used by the image's filesystem.
257     #
258     #This is the block size used when creating the filesystem image. Subclasses
259     #may change this if they wish to use something other than a 4k block size.
260     #
261     #Note, this attribute may only be set before calling mount().
262     _blocksize = property(__get_blocksize, __set_blocksize)
263
264     def __get_fstype(self):
265         return self.__fstype
266     def __set_fstype(self, val):
267         if val != "ext2" and val != "ext3":
268             raise CreatorError("Unknown _fstype '%s' supplied" % val)
269         self.__fstype = val
270     #The type of filesystem used for the image.
271     #
272     #This is the filesystem type used when creating the filesystem image.
273     #Subclasses may change this if they wish to use something other ext3.
274     #
275     #Note, only ext2 and ext3 are currently supported.
276     #
277     #Note also, this attribute may only be set before calling mount().
278     _fstype = property(__get_fstype, __set_fstype)
279
280     def __get_fsopts(self):
281         return self.__fsopts
282     def __set_fsopts(self, val):
283         self.__fsopts = val
284     #Mount options of filesystem used for the image.
285     #
286     #This can be specified by --fsoptions="xxx,yyy" in part command in
287     #kickstart file.
288     _fsopts = property(__get_fsopts, __set_fsopts)
289
290
291     #
292     # Helpers for subclasses
293     #
294     def _resparse(self, size=None):
295         """Rebuild the filesystem image to be as sparse as possible.
296
297         This method should be used by subclasses when staging the final image
298         in order to reduce the actual space taken up by the sparse image file
299         to be as little as possible.
300
301         This is done by resizing the filesystem to the minimal size (thereby
302         eliminating any space taken up by deleted files) and then resizing it
303         back to the supplied size.
304
305         size -- the size in, in bytes, which the filesystem image should be
306                 resized to after it has been minimized; this defaults to None,
307                 causing the original size specified by the kickstart file to
308                 be used (or 4GiB if not specified in the kickstart).
309         """
310         minsize = 0
311         for item in self._instloops:
312             if not item['cpioopts']:
313                 if item['name'] == self._img_name:
314                     minsize = item['loop'].resparse(size)
315                 else:
316                     item['loop'].resparse(size)
317
318         return minsize
319
320     def _base_on(self, base_on=None):
321         if base_on and self._image != base_on:
322             shutil.copyfile(base_on, self._image)
323
324     def _check_imgdir(self):
325         if self._imgdir is None:
326             self._imgdir = self._mkdtemp()
327
328
329     #
330     # Actual implementation
331     #
332     def _mount_instroot(self, base_on=None):
333
334         if base_on and os.path.isfile(base_on):
335             self._imgdir = os.path.dirname(base_on)
336             imgname = os.path.basename(base_on)
337             self._base_on(base_on)
338             self._set_image_size(misc.get_file_size(self._image))
339
340             # here, self._instloops must be []
341             self._instloops.append({
342                  "mountpoint": "/",
343                  "label": self.name,
344                  "name": imgname,
345                  "size": self.__image_size or 4096L,
346                  "fstype": self.__fstype or "ext3",
347                  "extopts": None,
348                  "loop": None,
349                  "uuid": None,
350                  "kspart": None,
351                  "exclude_image" : None
352                  })
353
354         self._check_imgdir()
355
356         for loop in self._instloops:
357             fstype = loop['fstype']
358             mp = os.path.join(self._instroot, loop['mountpoint'].lstrip('/'))
359             size = loop['size'] * 1024L * 1024L
360             imgname = loop['name']
361
362             if fstype in ("ext2", "ext3", "ext4"):
363                 MyDiskMount = fs.ExtDiskMount
364             elif fstype == "btrfs":
365                 MyDiskMount = fs.BtrfsDiskMount
366             elif fstype in ("vfat", "msdos"):
367                 MyDiskMount = fs.VfatDiskMount
368             else:
369                 raise MountError('Cannot support fstype: %s' % fstype)
370
371             loop['loop'] = MyDiskMount(fs.SparseLoopbackDisk(
372                                            os.path.join(self._imgdir, imgname),
373                                            size),
374                                        mp,
375                                        fstype,
376                                        self._blocksize,
377                                        loop['label'],
378                                        fsuuid = loop['uuid'])
379
380             if fstype in ("ext2", "ext3", "ext4"):
381                 loop['loop'].extopts = loop['extopts']
382
383             try:
384                 msger.verbose('Mounting image "%s" on "%s"' % (imgname, mp))
385                 fs.makedirs(mp)
386                 loop['loop'].mount()
387                 # Make an autogenerated uuid avaialble in _get_post_scripts_env()
388                 if loop['kspart'] and loop['kspart'].uuid is None and \
389                    loop['loop'].uuid:
390                     loop['kspart'].uuid = loop['loop'].uuid
391
392             except MountError, e:
393                 raise
394
395     def _unmount_instroot(self):
396         for item in reversed(self._instloops):
397             try:
398                 item['loop'].cleanup()
399             except:
400                 pass
401
402     def _stage_final_image(self):
403
404         if self.pack_to or self.shrink_image:
405             self._resparse(0)
406         else:
407             self._resparse()
408
409         for item in self._instloops:
410             imgfile = os.path.join(self._imgdir, item['name'])
411
412             if item['aft_fstype'] in AFTER_MNT_FS.keys():
413                 mountpoint = misc.mkdtemp()
414                 ext4img = os.path.join(self._imgdir, item['name'])
415                 runner.show('mount -t ext4 %s %s' % (ext4img, mountpoint))
416                 runner.show('ls -al %s' % (mountpoint))
417 #                item['loop'].mount(None, 'not_create')
418 #                point_mnt = os.path.join(self._instroot, item['mountpoint'].lstrip('/'))
419
420                 fs_suffix = AFTER_MNT_FS[item['aft_fstype']]
421                 if item['aft_fstype'] == "squashfs":
422 #                    fs.mksquashfs(mountpoint, self._outdir+"/"+item['label']+fs_suffix)
423                     args = "mksquashfs " + mountpoint + " " + self._imgdir+"/"+item['label']+fs_suffix
424                     if item['squashfsopts']:
425                         squashfsopts=item['squashfsopts'].replace(',', ' ')
426                         runner.show("mksquashfs --help")
427                         runner.show("%s %s" % (args, squashfsopts))
428                     else:
429                         runner.show("%s " % args)
430
431                 if item['aft_fstype'] == "vdfs":
432                     ##FIXME temporary code - replace this with fs.mkvdfs()
433                     if item['vdfsopts']:
434                         vdfsopts=item['vdfsopts'].replace(',', ' ')
435                     else:
436                         vdfsopts="-i -z 1024M"
437
438                     fullpathmkvdfs = "mkfs.vdfs" #find_binary_path("mkfs.vdfs")
439                     runner.show("%s --help" % fullpathmkvdfs)
440 #                    fs.mkvdfs(mountpoint, self._outdir+"/"+item['label']+fs_suffix, vdfsopts)
441                     runner.show('%s %s -r %s %s' % (fullpathmkvdfs, vdfsopts, mountpoint, self._imgdir+"/"+item['label']+fs_suffix))
442
443                 runner.show('umount %s' % mountpoint)
444 #               os.unlink(mountpoint)
445                 runner.show('mv %s %s' % (self._imgdir+"/"+item['label']+fs_suffix, self._imgdir+"/"+item['label']+".img") )
446                 runner.show('ls -al %s' % self._imgdir)
447
448             if item['fstype'] == "ext4":
449                 if not item['cpioopts']:
450                     runner.show('/sbin/tune2fs -O ^huge_file,extents,uninit_bg %s '
451                             % imgfile)
452                     runner.quiet(["/sbin/e2fsck", "-f", "-y", imgfile])
453             self.image_files.setdefault('partitions', {}).update(
454                     {item['mountpoint']: item['label']})
455             if self.compress_image:
456                 compressing(imgfile, self.compress_image)
457                 self.image_files.setdefault('image_files', []).append(
458                                 '.'.join([item['name'], self.compress_image]))
459             else:
460                 self.image_files.setdefault('image_files', []).append(item['name'])
461
462         for item in os.listdir(self._imgdir):
463             imgfile = os.path.join(self._imgdir, item)
464             imgsize = os.path.getsize(imgfile)
465             msger.info("filesystem size of %s : %s bytes" % (item, imgsize))
466
467         self.run_sign_scripts()
468         if not self.pack_to:
469             for item in os.listdir(self._imgdir):
470                 shutil.move(os.path.join(self._imgdir, item),
471                             os.path.join(self._outdir, item))
472         else:
473             msger.info("Pack all loop images together to %s" % self.pack_to)
474             dstfile = os.path.join(self._outdir, self.pack_to)
475             packing(dstfile, self._imgdir)
476             self.image_files['image_files'] = [self.pack_to]
477
478
479         if self.pack_to:
480             mountfp_xml = os.path.splitext(self.pack_to)[0]
481             mountfp_xml = misc.strip_end(mountfp_xml, '.tar') + ".xml"
482         else:
483             mountfp_xml = self.name + ".xml"
484         # save mount points mapping file to xml
485         save_mountpoints(os.path.join(self._outdir, mountfp_xml),
486                          self._instloops,
487                          self.target_arch)
488
489     def copy_attachment(self):
490         if not hasattr(self, '_attachment') or not self._attachment:
491             return
492
493         self._check_imgdir()
494
495         msger.info("Copying attachment files...")
496         for item in self._attachment:
497             if not os.path.exists(item):
498                 continue
499             dpath = os.path.join(self._imgdir, os.path.basename(item))
500             msger.verbose("Copy attachment %s to %s" % (item, dpath))
501             shutil.copy(item, dpath)
502
503     def create_manifest(self):
504         if self.compress_image:
505             self.image_files.update({'compress': self.compress_image})
506         super(LoopImageCreator, self).create_manifest()