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