3 # Copyright (c) 2011 Intel, Inc.
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
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
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.
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
29 # The maximum string length supported for LoopImageCreator.fslabel
31 # Support for Read-only file system list
32 AFTER_MNT_FS = {"squashfs":".sqsh", "vdfs":".vdfs"}
34 def save_mountpoints(fpath, loops, arch = None):
35 """Save mount points mapping to file
37 :fpath, the xml file to store partition info
38 :loops, dict of partition info
42 if not fpath or not loops:
45 from xml.dom import minidom
46 doc = minidom.Document()
47 imgroot = doc.createElement("image")
48 doc.appendChild(imgroot)
50 imgroot.setAttribute('arch', arch)
52 part = doc.createElement("partition")
53 imgroot.appendChild(part)
54 for (key, val) in loop.items():
55 if isinstance(val, fs.Mount):
57 part.setAttribute(key, str(val))
59 with open(fpath, 'w') as wf:
60 wf.write(doc.toprettyxml(indent=' '))
64 def load_mountpoints(fpath):
65 """Load mount points mapping from file
67 :fpath, file path to load
73 from xml.dom import minidom
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())
82 mp = (p['mountpoint'], p['label'], p['name'],
83 int(p['size']), p['fstype'])
85 msger.warning("Wrong format line in file: %s" % fpath)
87 msger.warning("Invalid size '%s' in file: %s" % (p['size'], fpath))
93 class LoopImageCreator(BaseImageCreator):
94 """Installs a system into a loopback-mountable filesystem image.
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.
100 When specifying multiple partitions in kickstart file, each partition
101 will be created as a separated loop image.
105 def __init__(self, creatoropts=None, pkgmgr=None,
108 """Initialize a LoopImageCreator instance.
110 This method takes the same arguments as ImageCreator.__init__()
111 with the addition of:
113 fslabel -- A string used as a label for any filesystems created.
116 BaseImageCreator.__init__(self, creatoropts, pkgmgr)
118 self.compress_image = compress_image
119 self.shrink_image = shrink_image
121 self.__fslabel = None
122 self.fslabel = self.name
124 self.__blocksize = 4096
126 self.__fstype = kickstart.get_image_fstype(self.ks,
128 self.__fsopts = kickstart.get_image_fsopts(self.ks,
130 if self.__fstype in AFTER_MNT_FS.keys():
131 self.__fstype = "ext4"
134 for part in sorted(kickstart.get_partitions(self.ks),
135 key=lambda p: p.mountpoint):
137 if part.fstype == "swap":
139 elif part.fstype in AFTER_MNT_FS.keys():
140 aft_fstype = part.fstype
152 msger.warning('no "label" specified for loop img at %s'
153 ', use the mountpoint as the name' % mp)
154 label = mp.split('/')[-1]
156 imgname = misc.strip_end(label, '.img') + '.img'
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,
171 'exclude_image' : part.exclude_image or None,
172 'no_shrink': part.no_shrink or False,
173 'init_expand': part.init_expand or False,
175 self._instloops = allloops
183 self._umountdir = None
186 self.__image_size = kickstart.get_image_size(self.ks,
189 self.__image_size = 0
191 self._img_name = self.name + ".img"
193 def get_image_names(self):
194 if not self._instloops:
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))
204 def _set_fstype(self, fstype):
205 self.__fstype = fstype
207 def _set_image_size(self, imgsize):
208 self.__image_size = imgsize
214 def __get_fslabel(self):
215 if self.__fslabel is None:
218 return self.__fslabel
219 def __set_fslabel(self, val):
221 self.__fslabel = None
223 self.__fslabel = val[:FSLABEL_MAXLEN]
224 #A string used to label any filesystems created.
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.
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)
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.
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().
244 #Note, this directory does not exist before ImageCreator.mount() is called.
246 #Note also, this is a read-only attribute.
247 _image = property(__get_image)
249 def __get_blocksize(self):
250 return self.__blocksize
251 def __set_blocksize(self, val):
253 raise CreatorError("_blocksize must be set before calling mount()")
255 self.__blocksize = int(val)
257 raise CreatorError("'%s' is not a valid integer value "
258 "for _blocksize" % val)
259 #The block size used by the image's filesystem.
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.
264 #Note, this attribute may only be set before calling mount().
265 _blocksize = property(__get_blocksize, __set_blocksize)
267 def __get_fstype(self):
269 def __set_fstype(self, val):
270 if val != "ext2" and val != "ext3":
271 raise CreatorError("Unknown _fstype '%s' supplied" % val)
273 #The type of filesystem used for the image.
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.
278 #Note, only ext2 and ext3 are currently supported.
280 #Note also, this attribute may only be set before calling mount().
281 _fstype = property(__get_fstype, __set_fstype)
283 def __get_fsopts(self):
285 def __set_fsopts(self, val):
287 #Mount options of filesystem used for the image.
289 #This can be specified by --fsoptions="xxx,yyy" in part command in
291 _fsopts = property(__get_fsopts, __set_fsopts)
295 # Helpers for subclasses
297 def _resparse(self, size=None):
298 """Rebuild the filesystem image to be as sparse as possible.
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.
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.
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).
314 for item in self._instloops:
315 if not item['cpioopts']:
316 if item['no_shrink']:
317 item['loop'].resparse()
319 if item['name'] == self._img_name:
320 minsize = item['loop'].resparse(size)
322 item['loop'].resparse(size)
326 def _base_on(self, base_on=None):
327 if base_on and self._image != base_on:
328 shutil.copyfile(base_on, self._image)
330 def _check_imgdir(self):
331 if self._imgdir is None:
332 self._imgdir = self._mkdtemp()
336 # Actual implementation
338 def _mount_instroot(self, base_on=None):
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))
346 # here, self._instloops must be []
347 self._instloops.append({
351 "size": self.__image_size or 4096L,
352 "fstype": self.__fstype or "ext3",
357 "exclude_image" : None
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']
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
375 raise MountError('Cannot support fstype: %s' % fstype)
377 loop['loop'] = MyDiskMount(fs.SparseLoopbackDisk(
378 os.path.join(self._imgdir, imgname),
384 fsuuid = loop['uuid'])
386 if fstype in ("ext2", "ext3", "ext4"):
387 loop['loop'].extopts = loop['extopts']
390 msger.verbose('Mounting image "%s" on "%s"' % (imgname, 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 \
396 loop['kspart'].uuid = loop['loop'].uuid
398 except MountError, e:
401 def _unmount_instroot(self):
402 for item in reversed(self._instloops):
404 item['loop'].cleanup()
408 def _get_sign_scripts_env(self):
409 env = BaseImageCreator._get_sign_scripts_env(self)
411 # Directory path of %post-umounts scripts
413 env['UMOUNT_SCRIPTS_PATH'] = str(self._umountdir)
417 def _stage_final_image(self):
419 if self.pack_to or self.shrink_image:
424 for item in self._instloops:
425 imgfile = os.path.join(self._imgdir, item['name'])
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('/'))
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))
444 runner.show("%s " % args)
446 if item['aft_fstype'] == "vdfs":
447 ##FIXME temporary code - replace this with fs.mkvdfs()
449 vdfsopts=item['vdfsopts'].replace(',', ' ')
451 vdfsopts="-i -z 1024M"
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))
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)
463 if item['fstype'] == "ext4":
464 if not item['cpioopts']:
465 runner.show('/sbin/tune2fs -O ^huge_file,extents,uninit_bg %s '
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]))
475 self.image_files.setdefault('image_files', []).append(item['name'])
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))
482 self.run_sign_scripts()
484 for item in os.listdir(self._imgdir):
485 shutil.move(os.path.join(self._imgdir, item),
486 os.path.join(self._outdir, item))
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]
495 mountfp_xml = os.path.splitext(self.pack_to)[0]
496 mountfp_xml = misc.strip_end(mountfp_xml, '.tar') + ".xml"
498 mountfp_xml = self.name + ".xml"
499 # save mount points mapping file to xml
500 save_mountpoints(os.path.join(self._outdir, mountfp_xml),
504 def copy_attachment(self):
505 if not hasattr(self, '_attachment') or not self._attachment:
510 msger.info("Copying attachment files...")
511 for item in self._attachment:
512 if not os.path.exists(item):
514 dpath = os.path.join(self._imgdir, os.path.basename(item))
515 msger.verbose("Copy attachment %s to %s" % (item, dpath))
516 shutil.copy(item, dpath)
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):
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)
531 def postinstall(self):
532 BaseImageCreator.postinstall(self)
533 self.move_post_umount_scripts()
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()