diff options
Diffstat (limited to 'scripts/lib/mic/imager/loop.py')
-rw-r--r-- | scripts/lib/mic/imager/loop.py | 418 |
1 files changed, 418 insertions, 0 deletions
diff --git a/scripts/lib/mic/imager/loop.py b/scripts/lib/mic/imager/loop.py new file mode 100644 index 0000000000..4d05ef271d --- /dev/null +++ b/scripts/lib/mic/imager/loop.py @@ -0,0 +1,418 @@ +#!/usr/bin/python -tt +# +# Copyright (c) 2011 Intel, Inc. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; version 2 of the License +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., 59 +# Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +import os +import glob +import shutil + +from mic import kickstart, msger +from mic.utils.errors import CreatorError, MountError +from mic.utils import misc, runner, fs_related as fs +from mic.imager.baseimager import BaseImageCreator + + +# The maximum string length supported for LoopImageCreator.fslabel +FSLABEL_MAXLEN = 32 + + +def save_mountpoints(fpath, loops, arch = None): + """Save mount points mapping to file + + :fpath, the xml file to store partition info + :loops, dict of partition info + :arch, image arch + """ + + if not fpath or not loops: + return + + from xml.dom import minidom + doc = minidom.Document() + imgroot = doc.createElement("image") + doc.appendChild(imgroot) + if arch: + imgroot.setAttribute('arch', arch) + for loop in loops: + part = doc.createElement("partition") + imgroot.appendChild(part) + for (key, val) in loop.items(): + if isinstance(val, fs.Mount): + continue + part.setAttribute(key, str(val)) + + with open(fpath, 'w') as wf: + wf.write(doc.toprettyxml(indent=' ')) + + return + +def load_mountpoints(fpath): + """Load mount points mapping from file + + :fpath, file path to load + """ + + if not fpath: + return + + from xml.dom import minidom + mount_maps = [] + with open(fpath, 'r') as rf: + dom = minidom.parse(rf) + imgroot = dom.documentElement + for part in imgroot.getElementsByTagName("partition"): + p = dict(part.attributes.items()) + + try: + mp = (p['mountpoint'], p['label'], p['name'], + int(p['size']), p['fstype']) + except KeyError: + msger.warning("Wrong format line in file: %s" % fpath) + except ValueError: + msger.warning("Invalid size '%s' in file: %s" % (p['size'], fpath)) + else: + mount_maps.append(mp) + + return mount_maps + +class LoopImageCreator(BaseImageCreator): + """Installs a system into a loopback-mountable filesystem image. + + LoopImageCreator is a straightforward ImageCreator subclass; the system + is installed into an ext3 filesystem on a sparse file which can be + subsequently loopback-mounted. + + When specifying multiple partitions in kickstart file, each partition + will be created as a separated loop image. + """ + + def __init__(self, creatoropts=None, pkgmgr=None, + compress_image=None, + shrink_image=False): + """Initialize a LoopImageCreator instance. + + This method takes the same arguments as ImageCreator.__init__() + with the addition of: + + fslabel -- A string used as a label for any filesystems created. + """ + + BaseImageCreator.__init__(self, creatoropts, pkgmgr) + + self.compress_image = compress_image + self.shrink_image = shrink_image + + self.__fslabel = None + self.fslabel = self.name + + self.__blocksize = 4096 + if self.ks: + self.__fstype = kickstart.get_image_fstype(self.ks, + "ext3") + self.__fsopts = kickstart.get_image_fsopts(self.ks, + "defaults,noatime") + + allloops = [] + for part in sorted(kickstart.get_partitions(self.ks), + key=lambda p: p.mountpoint): + if part.fstype == "swap": + continue + + label = part.label + mp = part.mountpoint + if mp == '/': + # the base image + if not label: + label = self.name + else: + mp = mp.rstrip('/') + if not label: + msger.warning('no "label" specified for loop img at %s' + ', use the mountpoint as the name' % mp) + label = mp.split('/')[-1] + + imgname = misc.strip_end(label, '.img') + '.img' + allloops.append({ + 'mountpoint': mp, + 'label': label, + 'name': imgname, + 'size': part.size or 4096L * 1024 * 1024, + 'fstype': part.fstype or 'ext3', + 'extopts': part.extopts or None, + 'loop': None, # to be created in _mount_instroot + }) + self._instloops = allloops + + else: + self.__fstype = None + self.__fsopts = None + self._instloops = [] + + self.__imgdir = None + + if self.ks: + self.__image_size = kickstart.get_image_size(self.ks, + 4096L * 1024 * 1024) + else: + self.__image_size = 0 + + self._img_name = self.name + ".img" + + def get_image_names(self): + if not self._instloops: + return None + + return [lo['name'] for lo in self._instloops] + + def _set_fstype(self, fstype): + self.__fstype = fstype + + def _set_image_size(self, imgsize): + self.__image_size = imgsize + + + # + # Properties + # + def __get_fslabel(self): + if self.__fslabel is None: + return self.name + else: + return self.__fslabel + def __set_fslabel(self, val): + if val is None: + self.__fslabel = None + else: + self.__fslabel = val[:FSLABEL_MAXLEN] + #A string used to label any filesystems created. + # + #Some filesystems impose a constraint on the maximum allowed size of the + #filesystem label. In the case of ext3 it's 16 characters, but in the case + #of ISO9660 it's 32 characters. + # + #mke2fs silently truncates the label, but mkisofs aborts if the label is + #too long. So, for convenience sake, any string assigned to this attribute + #is silently truncated to FSLABEL_MAXLEN (32) characters. + fslabel = property(__get_fslabel, __set_fslabel) + + def __get_image(self): + if self.__imgdir is None: + raise CreatorError("_image is not valid before calling mount()") + return os.path.join(self.__imgdir, self._img_name) + #The location of the image file. + # + #This is the path to the filesystem image. Subclasses may use this path + #in order to package the image in _stage_final_image(). + # + #Note, this directory does not exist before ImageCreator.mount() is called. + # + #Note also, this is a read-only attribute. + _image = property(__get_image) + + def __get_blocksize(self): + return self.__blocksize + def __set_blocksize(self, val): + if self._instloops: + raise CreatorError("_blocksize must be set before calling mount()") + try: + self.__blocksize = int(val) + except ValueError: + raise CreatorError("'%s' is not a valid integer value " + "for _blocksize" % val) + #The block size used by the image's filesystem. + # + #This is the block size used when creating the filesystem image. Subclasses + #may change this if they wish to use something other than a 4k block size. + # + #Note, this attribute may only be set before calling mount(). + _blocksize = property(__get_blocksize, __set_blocksize) + + def __get_fstype(self): + return self.__fstype + def __set_fstype(self, val): + if val != "ext2" and val != "ext3": + raise CreatorError("Unknown _fstype '%s' supplied" % val) + self.__fstype = val + #The type of filesystem used for the image. + # + #This is the filesystem type used when creating the filesystem image. + #Subclasses may change this if they wish to use something other ext3. + # + #Note, only ext2 and ext3 are currently supported. + # + #Note also, this attribute may only be set before calling mount(). + _fstype = property(__get_fstype, __set_fstype) + + def __get_fsopts(self): + return self.__fsopts + def __set_fsopts(self, val): + self.__fsopts = val + #Mount options of filesystem used for the image. + # + #This can be specified by --fsoptions="xxx,yyy" in part command in + #kickstart file. + _fsopts = property(__get_fsopts, __set_fsopts) + + + # + # Helpers for subclasses + # + def _resparse(self, size=None): + """Rebuild the filesystem image to be as sparse as possible. + + This method should be used by subclasses when staging the final image + in order to reduce the actual space taken up by the sparse image file + to be as little as possible. + + This is done by resizing the filesystem to the minimal size (thereby + eliminating any space taken up by deleted files) and then resizing it + back to the supplied size. + + size -- the size in, in bytes, which the filesystem image should be + resized to after it has been minimized; this defaults to None, + causing the original size specified by the kickstart file to + be used (or 4GiB if not specified in the kickstart). + """ + minsize = 0 + for item in self._instloops: + if item['name'] == self._img_name: + minsize = item['loop'].resparse(size) + else: + item['loop'].resparse(size) + + return minsize + + def _base_on(self, base_on=None): + if base_on and self._image != base_on: + shutil.copyfile(base_on, self._image) + + def _check_imgdir(self): + if self.__imgdir is None: + self.__imgdir = self._mkdtemp() + + + # + # Actual implementation + # + def _mount_instroot(self, base_on=None): + + if base_on and os.path.isfile(base_on): + self.__imgdir = os.path.dirname(base_on) + imgname = os.path.basename(base_on) + self._base_on(base_on) + self._set_image_size(misc.get_file_size(self._image)) + + # here, self._instloops must be [] + self._instloops.append({ + "mountpoint": "/", + "label": self.name, + "name": imgname, + "size": self.__image_size or 4096L, + "fstype": self.__fstype or "ext3", + "extopts": None, + "loop": None + }) + + self._check_imgdir() + + for loop in self._instloops: + fstype = loop['fstype'] + mp = os.path.join(self._instroot, loop['mountpoint'].lstrip('/')) + size = loop['size'] * 1024L * 1024L + imgname = loop['name'] + + if fstype in ("ext2", "ext3", "ext4"): + MyDiskMount = fs.ExtDiskMount + elif fstype == "btrfs": + MyDiskMount = fs.BtrfsDiskMount + elif fstype in ("vfat", "msdos"): + MyDiskMount = fs.VfatDiskMount + else: + msger.error('Cannot support fstype: %s' % fstype) + + loop['loop'] = MyDiskMount(fs.SparseLoopbackDisk( + os.path.join(self.__imgdir, imgname), + size), + mp, + fstype, + self._blocksize, + loop['label']) + + if fstype in ("ext2", "ext3", "ext4"): + loop['loop'].extopts = loop['extopts'] + + try: + msger.verbose('Mounting image "%s" on "%s"' % (imgname, mp)) + fs.makedirs(mp) + loop['loop'].mount() + except MountError, e: + raise + + def _unmount_instroot(self): + for item in reversed(self._instloops): + try: + item['loop'].cleanup() + except: + pass + + def _stage_final_image(self): + + if self.pack_to or self.shrink_image: + self._resparse(0) + else: + self._resparse() + + for item in self._instloops: + imgfile = os.path.join(self.__imgdir, item['name']) + if item['fstype'] == "ext4": + runner.show('/sbin/tune2fs -O ^huge_file,extents,uninit_bg %s ' + % imgfile) + if self.compress_image: + misc.compressing(imgfile, self.compress_image) + + if not self.pack_to: + for item in os.listdir(self.__imgdir): + shutil.move(os.path.join(self.__imgdir, item), + os.path.join(self._outdir, item)) + else: + msger.info("Pack all loop images together to %s" % self.pack_to) + dstfile = os.path.join(self._outdir, self.pack_to) + misc.packing(dstfile, self.__imgdir) + + if self.pack_to: + mountfp_xml = os.path.splitext(self.pack_to)[0] + mountfp_xml = misc.strip_end(mountfp_xml, '.tar') + ".xml" + else: + mountfp_xml = self.name + ".xml" + # save mount points mapping file to xml + save_mountpoints(os.path.join(self._outdir, mountfp_xml), + self._instloops, + self.target_arch) + + def copy_attachment(self): + if not hasattr(self, '_attachment') or not self._attachment: + return + + self._check_imgdir() + + msger.info("Copying attachment files...") + for item in self._attachment: + if not os.path.exists(item): + continue + dpath = os.path.join(self.__imgdir, os.path.basename(item)) + msger.verbose("Copy attachment %s to %s" % (item, dpath)) + shutil.copy(item, dpath) + |