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.
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
30 # The maximum string length supported for LoopImageCreator.fslabel
32 # Support for Read-only file system list
33 AFTER_MNT_FS = {"squashfs":".sqsh", "vdfs":".vdfs"}
35 def save_mountpoints(fpath, loops, arch = None):
36 """Save mount points mapping to file
38 :fpath, the xml file to store partition info
39 :loops, dict of partition info
43 if not fpath or not loops:
46 from xml.dom import minidom
47 doc = minidom.Document()
48 imgroot = doc.createElement("image")
49 doc.appendChild(imgroot)
51 imgroot.setAttribute('arch', arch)
53 part = doc.createElement("partition")
54 imgroot.appendChild(part)
55 for (key, val) in list(loop.items()):
56 if isinstance(val, fs.Mount):
58 part.setAttribute(key, str(val))
60 with open(fpath, 'w') as wf:
61 wf.write(doc.toprettyxml(indent=' '))
65 def load_mountpoints(fpath):
66 """Load mount points mapping from file
68 :fpath, file path to load
74 from xml.dom import minidom
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(list(part.attributes.items()))
83 mp = (p['mountpoint'], p['label'], p['name'],
84 int(p['size']), p['fstype'])
86 msger.warning("Wrong format line in file: %s" % fpath)
88 msger.warning("Invalid size '%s' in file: %s" % (p['size'], fpath))
94 class LoopImageCreator(BaseImageCreator):
95 """Installs a system into a loopback-mountable filesystem image.
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.
101 When specifying multiple partitions in kickstart file, each partition
102 will be created as a separated loop image.
106 def __init__(self, creatoropts=None, pkgmgr=None,
109 """Initialize a LoopImageCreator instance.
111 This method takes the same arguments as ImageCreator.__init__()
112 with the addition of:
114 fslabel -- A string used as a label for any filesystems created.
117 BaseImageCreator.__init__(self, creatoropts, pkgmgr)
119 self.compress_image = compress_image
120 self.shrink_image = shrink_image
122 self.__fslabel = None
123 self.fslabel = self.name
125 self.__blocksize = 4096
127 self.__fstype = kickstart.get_image_fstype(self.ks,
129 self.__fsopts = kickstart.get_image_fsopts(self.ks,
131 if self.__fstype in list(AFTER_MNT_FS.keys()):
132 self.__fstype = "ext4"
135 for part in sorted(kickstart.get_partitions(self.ks),
136 key=lambda p: p.mountpoint):
138 if part.fstype == "swap":
140 elif part.fstype in list(AFTER_MNT_FS.keys()):
141 aft_fstype = part.fstype
145 fslabel = part.fslabel
156 msger.warning('no "label" specified for loop img at %s'
157 ', use the mountpoint as the name' % mp)
158 label = mp.split('/')[-1]
160 imgname = misc.strip_end(label, '.img') + '.img'
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,
179 'exclude_image' : part.exclude_image or None,
180 'no_shrink': part.no_shrink or False,
181 'init_expand': part.init_expand or False,
183 self._instloops = allloops
191 self._umountdir = None
194 self.__image_size = kickstart.get_image_size(self.ks,
197 self.__image_size = 0
199 self._img_name = self.name + ".img"
201 def get_image_names(self):
202 if not self._instloops:
206 for lo in self._instloops :
207 names.append(lo['name'])
208 for ro in list(AFTER_MNT_FS.values()):
209 names.append(lo['name'].replace('.img',ro))
210 return list(set(names))
212 def _set_fstype(self, fstype):
213 self.__fstype = fstype
215 def _set_image_size(self, imgsize):
216 self.__image_size = imgsize
222 def __get_fslabel(self):
223 if self.__fslabel is None:
226 return self.__fslabel
227 def __set_fslabel(self, val):
229 self.__fslabel = None
231 self.__fslabel = val[:FSLABEL_MAXLEN]
232 #A string used to label any filesystems created.
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.
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)
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.
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().
252 #Note, this directory does not exist before ImageCreator.mount() is called.
254 #Note also, this is a read-only attribute.
255 _image = property(__get_image)
257 def __get_blocksize(self):
258 return self.__blocksize
259 def __set_blocksize(self, val):
261 raise CreatorError("_blocksize must be set before calling mount()")
263 self.__blocksize = int(val)
265 raise CreatorError("'%s' is not a valid integer value "
266 "for _blocksize" % val)
267 #The block size used by the image's filesystem.
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.
272 #Note, this attribute may only be set before calling mount().
273 _blocksize = property(__get_blocksize, __set_blocksize)
275 def __get_fstype(self):
277 def __set_fstype(self, val):
278 if val != "ext2" and val != "ext3":
279 raise CreatorError("Unknown _fstype '%s' supplied" % val)
281 #The type of filesystem used for the image.
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.
286 #Note, only ext2 and ext3 are currently supported.
288 #Note also, this attribute may only be set before calling mount().
289 _fstype = property(__get_fstype, __set_fstype)
291 def __get_fsopts(self):
293 def __set_fsopts(self, val):
295 #Mount options of filesystem used for the image.
297 #This can be specified by --fsoptions="xxx,yyy" in part command in
299 _fsopts = property(__get_fsopts, __set_fsopts)
303 # Helpers for subclasses
305 def _resparse(self, size=None):
306 """Rebuild the filesystem image to be as sparse as possible.
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.
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.
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).
322 for item in self._instloops:
323 if not item['cpioopts']:
324 if item['no_shrink']:
325 item['loop'].resparse()
327 if item['name'] == self._img_name:
328 minsize = item['loop'].resparse(size)
330 item['loop'].resparse(size)
334 def _base_on(self, base_on=None):
335 if base_on and self._image != base_on:
336 shutil.copyfile(base_on, self._image)
338 def _check_imgdir(self):
339 if self._imgdir is None:
340 self._imgdir = self._mkdtemp()
344 # Actual implementation
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))
353 # here, self._instloops must be []
354 self._instloops.append({
358 "size": self.__image_size or 4096,
359 "fstype": self.__fstype or "ext3",
365 "exclude_image" : None
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']
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
386 raise MountError('Cannot support fstype: %s' % fstype)
388 loop['loop'] = MyDiskMount(fs.SparseLoopbackDisk(
389 os.path.join(self._imgdir, imgname),
396 fsuuid = loop['uuid'])
398 if fstype in ("ext2", "ext3", "ext4"):
399 loop['loop'].extopts = loop['extopts']
400 elif fstype == "f2fs":
401 loop['loop'].f2fsopts = loop['f2fsopts']
404 msger.verbose('Mounting image "%s" on "%s"' % (imgname, 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 \
410 loop['kspart'].uuid = loop['loop'].uuid
412 except MountError as e:
415 def _unmount_instroot(self):
416 for item in reversed(self._instloops):
418 item['loop'].cleanup()
422 def _get_sign_scripts_env(self):
423 env = BaseImageCreator._get_sign_scripts_env(self)
425 # Directory path of %post-umounts scripts
427 env['UMOUNT_SCRIPTS_PATH'] = str(self._umountdir)
431 def _stage_final_image(self):
433 if self.pack_to or self.shrink_image:
438 for item in self._instloops:
439 imgfile = os.path.join(self._imgdir, item['name'])
441 if item['aft_fstype'] in list(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('/'))
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))
458 runner.show("%s " % args)
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))
467 if item['aft_fstype'] == "vdfs":
468 ##FIXME temporary code - replace this with fs.mkvdfs()
470 vdfsopts=item['vdfsopts'].replace(',', ' ')
472 vdfsopts="-i -z 1024M"
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))
479 runner.show("mkfs.vdfs return error")
480 raise VdfsError("' %s' exited with error (%d)" % (fullpathmkvdfs, ret))
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)
487 if item['fstype'] == "ext4":
488 if not item['cpioopts']:
489 runner.show('/sbin/tune2fs -O ^huge_file,extents,uninit_bg %s '
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]))
499 self.image_files.setdefault('image_files', []).append(item['name'])
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))
506 self.run_sign_scripts()
508 for item in os.listdir(self._imgdir):
509 shutil.move(os.path.join(self._imgdir, item),
510 os.path.join(self._outdir, item))
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]
519 mountfp_xml = os.path.splitext(self.pack_to)[0]
520 mountfp_xml = misc.strip_end(mountfp_xml, '.tar') + ".xml"
522 mountfp_xml = self.name + ".xml"
523 # save mount points mapping file to xml
524 save_mountpoints(os.path.join(self._outdir, mountfp_xml),
528 def copy_attachment(self):
529 if not hasattr(self, '_attachment') or not self._attachment:
534 msger.info("Copying attachment files...")
535 for item in self._attachment:
536 if not os.path.exists(item):
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)
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):
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)
555 def postinstall(self):
556 BaseImageCreator.postinstall(self)
557 self.move_post_umount_scripts()
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()