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
185 self.__image_size = kickstart.get_image_size(self.ks,
188 self.__image_size = 0
190 self._img_name = self.name + ".img"
192 def get_image_names(self):
193 if not self._instloops:
197 for lo in self._instloops :
198 names.append(lo['name'])
199 for ro in AFTER_MNT_FS.values():
200 names.append(lo['name'].replace('.img',ro))
201 return list(set(names))
203 def _set_fstype(self, fstype):
204 self.__fstype = fstype
206 def _set_image_size(self, imgsize):
207 self.__image_size = imgsize
213 def __get_fslabel(self):
214 if self.__fslabel is None:
217 return self.__fslabel
218 def __set_fslabel(self, val):
220 self.__fslabel = None
222 self.__fslabel = val[:FSLABEL_MAXLEN]
223 #A string used to label any filesystems created.
225 #Some filesystems impose a constraint on the maximum allowed size of the
226 #filesystem label. In the case of ext3 it's 16 characters, but in the case
227 #of ISO9660 it's 32 characters.
229 #mke2fs silently truncates the label, but mkisofs aborts if the label is
230 #too long. So, for convenience sake, any string assigned to this attribute
231 #is silently truncated to FSLABEL_MAXLEN (32) characters.
232 fslabel = property(__get_fslabel, __set_fslabel)
234 def __get_image(self):
235 if self._imgdir is None:
236 raise CreatorError("_image is not valid before calling mount()")
237 return os.path.join(self._imgdir, self._img_name)
238 #The location of the image file.
240 #This is the path to the filesystem image. Subclasses may use this path
241 #in order to package the image in _stage_final_image().
243 #Note, this directory does not exist before ImageCreator.mount() is called.
245 #Note also, this is a read-only attribute.
246 _image = property(__get_image)
248 def __get_blocksize(self):
249 return self.__blocksize
250 def __set_blocksize(self, val):
252 raise CreatorError("_blocksize must be set before calling mount()")
254 self.__blocksize = int(val)
256 raise CreatorError("'%s' is not a valid integer value "
257 "for _blocksize" % val)
258 #The block size used by the image's filesystem.
260 #This is the block size used when creating the filesystem image. Subclasses
261 #may change this if they wish to use something other than a 4k block size.
263 #Note, this attribute may only be set before calling mount().
264 _blocksize = property(__get_blocksize, __set_blocksize)
266 def __get_fstype(self):
268 def __set_fstype(self, val):
269 if val != "ext2" and val != "ext3":
270 raise CreatorError("Unknown _fstype '%s' supplied" % val)
272 #The type of filesystem used for the image.
274 #This is the filesystem type used when creating the filesystem image.
275 #Subclasses may change this if they wish to use something other ext3.
277 #Note, only ext2 and ext3 are currently supported.
279 #Note also, this attribute may only be set before calling mount().
280 _fstype = property(__get_fstype, __set_fstype)
282 def __get_fsopts(self):
284 def __set_fsopts(self, val):
286 #Mount options of filesystem used for the image.
288 #This can be specified by --fsoptions="xxx,yyy" in part command in
290 _fsopts = property(__get_fsopts, __set_fsopts)
294 # Helpers for subclasses
296 def _resparse(self, size=None):
297 """Rebuild the filesystem image to be as sparse as possible.
299 This method should be used by subclasses when staging the final image
300 in order to reduce the actual space taken up by the sparse image file
301 to be as little as possible.
303 This is done by resizing the filesystem to the minimal size (thereby
304 eliminating any space taken up by deleted files) and then resizing it
305 back to the supplied size.
307 size -- the size in, in bytes, which the filesystem image should be
308 resized to after it has been minimized; this defaults to None,
309 causing the original size specified by the kickstart file to
310 be used (or 4GiB if not specified in the kickstart).
313 for item in self._instloops:
314 if not item['cpioopts']:
315 if item['no_shrink']:
316 item['loop'].resparse()
318 if item['name'] == self._img_name:
319 minsize = item['loop'].resparse(size)
321 item['loop'].resparse(size)
325 def _base_on(self, base_on=None):
326 if base_on and self._image != base_on:
327 shutil.copyfile(base_on, self._image)
329 def _check_imgdir(self):
330 if self._imgdir is None:
331 self._imgdir = self._mkdtemp()
335 # Actual implementation
337 def _mount_instroot(self, base_on=None):
339 if base_on and os.path.isfile(base_on):
340 self._imgdir = os.path.dirname(base_on)
341 imgname = os.path.basename(base_on)
342 self._base_on(base_on)
343 self._set_image_size(misc.get_file_size(self._image))
345 # here, self._instloops must be []
346 self._instloops.append({
350 "size": self.__image_size or 4096L,
351 "fstype": self.__fstype or "ext3",
356 "exclude_image" : None
361 for loop in self._instloops:
362 fstype = loop['fstype']
363 mp = os.path.join(self._instroot, loop['mountpoint'].lstrip('/'))
364 size = loop['size'] * 1024L * 1024L
365 imgname = loop['name']
367 if fstype in ("ext2", "ext3", "ext4"):
368 MyDiskMount = fs.ExtDiskMount
369 elif fstype == "btrfs":
370 MyDiskMount = fs.BtrfsDiskMount
371 elif fstype in ("vfat", "msdos"):
372 MyDiskMount = fs.VfatDiskMount
374 raise MountError('Cannot support fstype: %s' % fstype)
376 loop['loop'] = MyDiskMount(fs.SparseLoopbackDisk(
377 os.path.join(self._imgdir, imgname),
383 fsuuid = loop['uuid'])
385 if fstype in ("ext2", "ext3", "ext4"):
386 loop['loop'].extopts = loop['extopts']
389 msger.verbose('Mounting image "%s" on "%s"' % (imgname, mp))
391 loop['loop'].mount(init_expand=loop['init_expand'])
392 # Make an autogenerated uuid avaialble in _get_post_scripts_env()
393 if loop['kspart'] and loop['kspart'].uuid is None and \
395 loop['kspart'].uuid = loop['loop'].uuid
397 except MountError, e:
400 def _unmount_instroot(self):
401 for item in reversed(self._instloops):
403 item['loop'].cleanup()
407 def _stage_final_image(self):
409 if self.pack_to or self.shrink_image:
414 for item in self._instloops:
415 imgfile = os.path.join(self._imgdir, item['name'])
417 if item['aft_fstype'] in AFTER_MNT_FS.keys():
418 mountpoint = misc.mkdtemp()
419 ext4img = os.path.join(self._imgdir, item['name'])
420 runner.show('mount -t ext4 %s %s' % (ext4img, mountpoint))
421 runner.show('ls -al %s' % (mountpoint))
422 # item['loop'].mount(None, 'not_create')
423 # point_mnt = os.path.join(self._instroot, item['mountpoint'].lstrip('/'))
425 fs_suffix = AFTER_MNT_FS[item['aft_fstype']]
426 if item['aft_fstype'] == "squashfs":
427 # fs.mksquashfs(mountpoint, self._outdir+"/"+item['label']+fs_suffix)
428 args = "mksquashfs " + mountpoint + " " + self._imgdir+"/"+item['label']+fs_suffix
429 if item['squashfsopts']:
430 squashfsopts=item['squashfsopts'].replace(',', ' ')
431 runner.show("mksquashfs --help")
432 runner.show("%s %s" % (args, squashfsopts))
434 runner.show("%s " % args)
436 if item['aft_fstype'] == "vdfs":
437 ##FIXME temporary code - replace this with fs.mkvdfs()
439 vdfsopts=item['vdfsopts'].replace(',', ' ')
441 vdfsopts="-i -z 1024M"
443 fullpathmkvdfs = "mkfs.vdfs" #find_binary_path("mkfs.vdfs")
444 runner.show("%s --help" % fullpathmkvdfs)
445 # fs.mkvdfs(mountpoint, self._outdir+"/"+item['label']+fs_suffix, vdfsopts)
446 runner.show('%s %s -r %s %s' % (fullpathmkvdfs, vdfsopts, mountpoint, self._imgdir+"/"+item['label']+fs_suffix))
448 runner.show('umount %s' % mountpoint)
449 # os.unlink(mountpoint)
450 runner.show('mv %s %s' % (self._imgdir+"/"+item['label']+fs_suffix, self._imgdir+"/"+item['label']+".img") )
451 runner.show('ls -al %s' % self._imgdir)
453 if item['fstype'] == "ext4":
454 if not item['cpioopts']:
455 runner.show('/sbin/tune2fs -O ^huge_file,extents,uninit_bg %s '
457 runner.quiet(["/sbin/e2fsck", "-f", "-y", imgfile])
458 self.image_files.setdefault('partitions', {}).update(
459 {item['mountpoint']: item['label']})
460 if self.compress_image:
461 compressing(imgfile, self.compress_image)
462 self.image_files.setdefault('image_files', []).append(
463 '.'.join([item['name'], self.compress_image]))
465 self.image_files.setdefault('image_files', []).append(item['name'])
467 for item in os.listdir(self._imgdir):
468 imgfile = os.path.join(self._imgdir, item)
469 imgsize = os.path.getsize(imgfile)
470 msger.info("filesystem size of %s : %s bytes" % (item, imgsize))
472 self.run_sign_scripts()
474 for item in os.listdir(self._imgdir):
475 shutil.move(os.path.join(self._imgdir, item),
476 os.path.join(self._outdir, item))
478 msger.info("Pack all loop images together to %s" % self.pack_to)
479 dstfile = os.path.join(self._outdir, self.pack_to)
480 packing(dstfile, self._imgdir)
481 self.image_files['image_files'] = [self.pack_to]
485 mountfp_xml = os.path.splitext(self.pack_to)[0]
486 mountfp_xml = misc.strip_end(mountfp_xml, '.tar') + ".xml"
488 mountfp_xml = self.name + ".xml"
489 # save mount points mapping file to xml
490 save_mountpoints(os.path.join(self._outdir, mountfp_xml),
494 def copy_attachment(self):
495 if not hasattr(self, '_attachment') or not self._attachment:
500 msger.info("Moving attachment files...")
501 for item in self._attachment:
502 if not os.path.exists(item):
504 dpath = os.path.join(self._imgdir, os.path.basename(item))
505 msger.verbose("Move attachment %s to %s" % (item, dpath))
506 shutil.move(item, dpath)
508 def create_manifest(self):
509 if self.compress_image:
510 self.image_files.update({'compress': self.compress_image})
511 super(LoopImageCreator, self).create_manifest()