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,
173 self._instloops = allloops
183 self.__image_size = kickstart.get_image_size(self.ks,
186 self.__image_size = 0
188 self._img_name = self.name + ".img"
190 def get_image_names(self):
191 if not self._instloops:
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))
201 def _set_fstype(self, fstype):
202 self.__fstype = fstype
204 def _set_image_size(self, imgsize):
205 self.__image_size = imgsize
211 def __get_fslabel(self):
212 if self.__fslabel is None:
215 return self.__fslabel
216 def __set_fslabel(self, val):
218 self.__fslabel = None
220 self.__fslabel = val[:FSLABEL_MAXLEN]
221 #A string used to label any filesystems created.
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.
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)
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.
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().
241 #Note, this directory does not exist before ImageCreator.mount() is called.
243 #Note also, this is a read-only attribute.
244 _image = property(__get_image)
246 def __get_blocksize(self):
247 return self.__blocksize
248 def __set_blocksize(self, val):
250 raise CreatorError("_blocksize must be set before calling mount()")
252 self.__blocksize = int(val)
254 raise CreatorError("'%s' is not a valid integer value "
255 "for _blocksize" % val)
256 #The block size used by the image's filesystem.
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.
261 #Note, this attribute may only be set before calling mount().
262 _blocksize = property(__get_blocksize, __set_blocksize)
264 def __get_fstype(self):
266 def __set_fstype(self, val):
267 if val != "ext2" and val != "ext3":
268 raise CreatorError("Unknown _fstype '%s' supplied" % val)
270 #The type of filesystem used for the image.
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.
275 #Note, only ext2 and ext3 are currently supported.
277 #Note also, this attribute may only be set before calling mount().
278 _fstype = property(__get_fstype, __set_fstype)
280 def __get_fsopts(self):
282 def __set_fsopts(self, val):
284 #Mount options of filesystem used for the image.
286 #This can be specified by --fsoptions="xxx,yyy" in part command in
288 _fsopts = property(__get_fsopts, __set_fsopts)
292 # Helpers for subclasses
294 def _resparse(self, size=None):
295 """Rebuild the filesystem image to be as sparse as possible.
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.
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.
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).
311 for item in self._instloops:
312 if not item['cpioopts']:
313 if item['name'] == self._img_name:
314 minsize = item['loop'].resparse(size)
316 item['loop'].resparse(size)
320 def _base_on(self, base_on=None):
321 if base_on and self._image != base_on:
322 shutil.copyfile(base_on, self._image)
324 def _check_imgdir(self):
325 if self._imgdir is None:
326 self._imgdir = self._mkdtemp()
330 # Actual implementation
332 def _mount_instroot(self, base_on=None):
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))
340 # here, self._instloops must be []
341 self._instloops.append({
345 "size": self.__image_size or 4096L,
346 "fstype": self.__fstype or "ext3",
351 "exclude_image" : None
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']
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
369 raise MountError('Cannot support fstype: %s' % fstype)
371 loop['loop'] = MyDiskMount(fs.SparseLoopbackDisk(
372 os.path.join(self._imgdir, imgname),
378 fsuuid = loop['uuid'])
380 if fstype in ("ext2", "ext3", "ext4"):
381 loop['loop'].extopts = loop['extopts']
384 msger.verbose('Mounting image "%s" on "%s"' % (imgname, mp))
387 # Make an autogenerated uuid avaialble in _get_post_scripts_env()
388 if loop['kspart'] and loop['kspart'].uuid is None and \
390 loop['kspart'].uuid = loop['loop'].uuid
392 except MountError, e:
395 def _unmount_instroot(self):
396 for item in reversed(self._instloops):
398 item['loop'].cleanup()
402 def _stage_final_image(self):
404 if self.pack_to or self.shrink_image:
409 for item in self._instloops:
410 imgfile = os.path.join(self._imgdir, item['name'])
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('/'))
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))
429 runner.show("%s " % args)
431 if item['aft_fstype'] == "vdfs":
432 ##FIXME temporary code - replace this with fs.mkvdfs()
434 vdfsopts=item['vdfsopts'].replace(',', ' ')
436 vdfsopts="-i -z 1024M"
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))
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)
448 if item['fstype'] == "ext4":
449 if not item['cpioopts']:
450 runner.show('/sbin/tune2fs -O ^huge_file,extents,uninit_bg %s '
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]))
460 self.image_files.setdefault('image_files', []).append(item['name'])
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))
467 self.run_sign_scripts()
469 for item in os.listdir(self._imgdir):
470 shutil.move(os.path.join(self._imgdir, item),
471 os.path.join(self._outdir, item))
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]
480 mountfp_xml = os.path.splitext(self.pack_to)[0]
481 mountfp_xml = misc.strip_end(mountfp_xml, '.tar') + ".xml"
483 mountfp_xml = self.name + ".xml"
484 # save mount points mapping file to xml
485 save_mountpoints(os.path.join(self._outdir, mountfp_xml),
489 def copy_attachment(self):
490 if not hasattr(self, '_attachment') or not self._attachment:
495 msger.info("Copying attachment files...")
496 for item in self._attachment:
497 if not os.path.exists(item):
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)
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()