diff options
Diffstat (limited to 'scripts/lib/mic/imager/baseimager.py')
-rw-r--r-- | scripts/lib/mic/imager/baseimager.py | 1335 |
1 files changed, 1335 insertions, 0 deletions
diff --git a/scripts/lib/mic/imager/baseimager.py b/scripts/lib/mic/imager/baseimager.py new file mode 100644 index 0000000000..6efc6c1294 --- /dev/null +++ b/scripts/lib/mic/imager/baseimager.py @@ -0,0 +1,1335 @@ + +#!/usr/bin/python -tt +# +# Copyright (c) 2007 Red Hat Inc. +# Copyright (c) 2009, 2010, 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. + +from __future__ import with_statement +import os, sys +import stat +import tempfile +import shutil +import subprocess +import re +import tarfile +import glob + +import rpm + +from mic import kickstart +from mic import msger +from mic.utils.errors import CreatorError, Abort +from mic.utils import misc, grabber, runner, fs_related as fs + +class BaseImageCreator(object): + """Installs a system to a chroot directory. + + ImageCreator is the simplest creator class available; it will install and + configure a system image according to the supplied kickstart file. + + e.g. + + import mic.imgcreate as imgcreate + ks = imgcreate.read_kickstart("foo.ks") + imgcreate.ImageCreator(ks, "foo").create() + + """ + + def __del__(self): + self.cleanup() + + def __init__(self, createopts = None, pkgmgr = None): + """Initialize an ImageCreator instance. + + ks -- a pykickstart.KickstartParser instance; this instance will be + used to drive the install by e.g. providing the list of packages + to be installed, the system configuration and %post scripts + + name -- a name for the image; used for e.g. image filenames or + filesystem labels + """ + + self.pkgmgr = pkgmgr + + self.__builddir = None + self.__bindmounts = [] + + self.ks = None + self.name = "target" + self.tmpdir = "/var/tmp/mic" + self.cachedir = "/var/tmp/mic/cache" + self.workdir = "/var/tmp/mic/build" + self.destdir = "." + self.installerfw_prefix = "INSTALLERFW_" + self.target_arch = "noarch" + self._local_pkgs_path = None + self.pack_to = None + self.repourl = {} + + # If the kernel is save to the destdir when copy_kernel cmd is called. + self._need_copy_kernel = False + # setup tmpfs tmpdir when enabletmpfs is True + self.enabletmpfs = False + + if createopts: + # Mapping table for variables that have different names. + optmap = {"pkgmgr" : "pkgmgr_name", + "outdir" : "destdir", + "arch" : "target_arch", + "local_pkgs_path" : "_local_pkgs_path", + "copy_kernel" : "_need_copy_kernel", + } + + # update setting from createopts + for key in createopts.keys(): + if key in optmap: + option = optmap[key] + else: + option = key + setattr(self, option, createopts[key]) + + self.destdir = os.path.abspath(os.path.expanduser(self.destdir)) + + if 'release' in createopts and createopts['release']: + self.name = createopts['release'] + '_' + self.name + + if self.pack_to: + if '@NAME@' in self.pack_to: + self.pack_to = self.pack_to.replace('@NAME@', self.name) + (tar, ext) = os.path.splitext(self.pack_to) + if ext in (".gz", ".bz2") and tar.endswith(".tar"): + ext = ".tar" + ext + if ext not in misc.pack_formats: + self.pack_to += ".tar" + + self._dep_checks = ["ls", "bash", "cp", "echo", "modprobe"] + + # Output image file names + self.outimage = [] + + # A flag to generate checksum + self._genchecksum = False + + self._alt_initrd_name = None + + self._recording_pkgs = [] + + # available size in root fs, init to 0 + self._root_fs_avail = 0 + + # Name of the disk image file that is created. + self._img_name = None + + self.image_format = None + + # Save qemu emulator file name in order to clean up it finally + self.qemu_emulator = None + + # No ks provided when called by convertor, so skip the dependency check + if self.ks: + # If we have btrfs partition we need to check necessary tools + for part in self.ks.handler.partition.partitions: + if part.fstype and part.fstype == "btrfs": + self._dep_checks.append("mkfs.btrfs") + break + + if self.target_arch and self.target_arch.startswith("arm"): + for dep in self._dep_checks: + if dep == "extlinux": + self._dep_checks.remove(dep) + + if not os.path.exists("/usr/bin/qemu-arm") or \ + not misc.is_statically_linked("/usr/bin/qemu-arm"): + self._dep_checks.append("qemu-arm-static") + + if os.path.exists("/proc/sys/vm/vdso_enabled"): + vdso_fh = open("/proc/sys/vm/vdso_enabled","r") + vdso_value = vdso_fh.read().strip() + vdso_fh.close() + if (int)(vdso_value) == 1: + msger.warning("vdso is enabled on your host, which might " + "cause problems with arm emulations.\n" + "\tYou can disable vdso with following command before " + "starting image build:\n" + "\techo 0 | sudo tee /proc/sys/vm/vdso_enabled") + + # make sure the specified tmpdir and cachedir exist + if not os.path.exists(self.tmpdir): + os.makedirs(self.tmpdir) + if not os.path.exists(self.cachedir): + os.makedirs(self.cachedir) + + + # + # Properties + # + def __get_instroot(self): + if self.__builddir is None: + raise CreatorError("_instroot is not valid before calling mount()") + return self.__builddir + "/install_root" + _instroot = property(__get_instroot) + """The location of the install root directory. + + This is the directory into which the system is installed. Subclasses may + mount a filesystem image here or copy files to/from here. + + Note, this directory does not exist before ImageCreator.mount() is called. + + Note also, this is a read-only attribute. + + """ + + def __get_outdir(self): + if self.__builddir is None: + raise CreatorError("_outdir is not valid before calling mount()") + return self.__builddir + "/out" + _outdir = property(__get_outdir) + """The staging location for the final image. + + This is where subclasses should stage any files that are part of the final + image. ImageCreator.package() will copy any files found here into the + requested destination directory. + + Note, this directory does not exist before ImageCreator.mount() is called. + + Note also, this is a read-only attribute. + + """ + + + # + # Hooks for subclasses + # + def _mount_instroot(self, base_on = None): + """Mount or prepare the install root directory. + + This is the hook where subclasses may prepare the install root by e.g. + mounting creating and loopback mounting a filesystem image to + _instroot. + + There is no default implementation. + + base_on -- this is the value passed to mount() and can be interpreted + as the subclass wishes; it might e.g. be the location of + a previously created ISO containing a system image. + + """ + pass + + def _unmount_instroot(self): + """Undo anything performed in _mount_instroot(). + + This is the hook where subclasses must undo anything which was done + in _mount_instroot(). For example, if a filesystem image was mounted + onto _instroot, it should be unmounted here. + + There is no default implementation. + + """ + pass + + def _create_bootconfig(self): + """Configure the image so that it's bootable. + + This is the hook where subclasses may prepare the image for booting by + e.g. creating an initramfs and bootloader configuration. + + This hook is called while the install root is still mounted, after the + packages have been installed and the kickstart configuration has been + applied, but before the %post scripts have been executed. + + There is no default implementation. + + """ + pass + + def _stage_final_image(self): + """Stage the final system image in _outdir. + + This is the hook where subclasses should place the image in _outdir + so that package() can copy it to the requested destination directory. + + By default, this moves the install root into _outdir. + + """ + shutil.move(self._instroot, self._outdir + "/" + self.name) + + def get_installed_packages(self): + return self._pkgs_content.keys() + + def _save_recording_pkgs(self, destdir): + """Save the list or content of installed packages to file. + """ + pkgs = self._pkgs_content.keys() + pkgs.sort() # inplace op + + if not os.path.exists(destdir): + os.makedirs(destdir) + + content = None + if 'vcs' in self._recording_pkgs: + vcslst = ["%s %s" % (k, v) for (k, v) in self._pkgs_vcsinfo.items()] + content = '\n'.join(sorted(vcslst)) + elif 'name' in self._recording_pkgs: + content = '\n'.join(pkgs) + if content: + namefile = os.path.join(destdir, self.name + '.packages') + f = open(namefile, "w") + f.write(content) + f.close() + self.outimage.append(namefile); + + # if 'content', save more details + if 'content' in self._recording_pkgs: + contfile = os.path.join(destdir, self.name + '.files') + f = open(contfile, "w") + + for pkg in pkgs: + content = pkg + '\n' + + pkgcont = self._pkgs_content[pkg] + content += ' ' + content += '\n '.join(pkgcont) + content += '\n' + + content += '\n' + f.write(content) + f.close() + self.outimage.append(contfile) + + if 'license' in self._recording_pkgs: + licensefile = os.path.join(destdir, self.name + '.license') + f = open(licensefile, "w") + + f.write('Summary:\n') + for license in reversed(sorted(self._pkgs_license, key=\ + lambda license: len(self._pkgs_license[license]))): + f.write(" - %s: %s\n" \ + % (license, len(self._pkgs_license[license]))) + + f.write('\nDetails:\n') + for license in reversed(sorted(self._pkgs_license, key=\ + lambda license: len(self._pkgs_license[license]))): + f.write(" - %s:\n" % (license)) + for pkg in sorted(self._pkgs_license[license]): + f.write(" - %s\n" % (pkg)) + f.write('\n') + + f.close() + self.outimage.append(licensefile) + + def _get_required_packages(self): + """Return a list of required packages. + + This is the hook where subclasses may specify a set of packages which + it requires to be installed. + + This returns an empty list by default. + + Note, subclasses should usually chain up to the base class + implementation of this hook. + + """ + return [] + + def _get_excluded_packages(self): + """Return a list of excluded packages. + + This is the hook where subclasses may specify a set of packages which + it requires _not_ to be installed. + + This returns an empty list by default. + + Note, subclasses should usually chain up to the base class + implementation of this hook. + + """ + return [] + + def _get_local_packages(self): + """Return a list of rpm path to be local installed. + + This is the hook where subclasses may specify a set of rpms which + it requires to be installed locally. + + This returns an empty list by default. + + Note, subclasses should usually chain up to the base class + implementation of this hook. + + """ + if self._local_pkgs_path: + if os.path.isdir(self._local_pkgs_path): + return glob.glob( + os.path.join(self._local_pkgs_path, '*.rpm')) + elif os.path.splitext(self._local_pkgs_path)[-1] == '.rpm': + return [self._local_pkgs_path] + + return [] + + def _get_fstab(self): + """Return the desired contents of /etc/fstab. + + This is the hook where subclasses may specify the contents of + /etc/fstab by returning a string containing the desired contents. + + A sensible default implementation is provided. + + """ + s = "/dev/root / %s %s 0 0\n" \ + % (self._fstype, + "defaults,noatime" if not self._fsopts else self._fsopts) + s += self._get_fstab_special() + return s + + def _get_fstab_special(self): + s = "devpts /dev/pts devpts gid=5,mode=620 0 0\n" + s += "tmpfs /dev/shm tmpfs defaults 0 0\n" + s += "proc /proc proc defaults 0 0\n" + s += "sysfs /sys sysfs defaults 0 0\n" + return s + + def _set_part_env(self, pnum, prop, value): + """ This is a helper function which generates an environment variable + for a property "prop" with value "value" of a partition number "pnum". + + The naming convention is: + * Variables start with INSTALLERFW_PART + * Then goes the partition number, the order is the same as + specified in the KS file + * Then goes the property name + """ + + if value == None: + value = "" + else: + value = str(value) + + name = self.installerfw_prefix + ("PART%d_" % pnum) + prop + return { name : value } + + def _get_post_scripts_env(self, in_chroot): + """Return an environment dict for %post scripts. + + This is the hook where subclasses may specify some environment + variables for %post scripts by return a dict containing the desired + environment. + + in_chroot -- whether this %post script is to be executed chroot()ed + into _instroot. + """ + + env = {} + pnum = 0 + + for p in kickstart.get_partitions(self.ks): + env.update(self._set_part_env(pnum, "SIZE", p.size)) + env.update(self._set_part_env(pnum, "MOUNTPOINT", p.mountpoint)) + env.update(self._set_part_env(pnum, "FSTYPE", p.fstype)) + env.update(self._set_part_env(pnum, "LABEL", p.label)) + env.update(self._set_part_env(pnum, "FSOPTS", p.fsopts)) + env.update(self._set_part_env(pnum, "BOOTFLAG", p.active)) + env.update(self._set_part_env(pnum, "ALIGN", p.align)) + env.update(self._set_part_env(pnum, "TYPE_ID", p.part_type)) + env.update(self._set_part_env(pnum, "DEVNODE", + "/dev/%s%d" % (p.disk, pnum + 1))) + pnum += 1 + + # Count of paritions + env[self.installerfw_prefix + "PART_COUNT"] = str(pnum) + + # Partition table format + ptable_format = self.ks.handler.bootloader.ptable + env[self.installerfw_prefix + "PTABLE_FORMAT"] = ptable_format + + # The kerned boot parameters + kernel_opts = self.ks.handler.bootloader.appendLine + env[self.installerfw_prefix + "KERNEL_OPTS"] = kernel_opts + + # Name of the distribution + env[self.installerfw_prefix + "DISTRO_NAME"] = self.distro_name + + # Name of the image creation tool + env[self.installerfw_prefix + "INSTALLER_NAME"] = "mic" + + # The real current location of the mounted file-systems + if in_chroot: + mount_prefix = "/" + else: + mount_prefix = self._instroot + env[self.installerfw_prefix + "MOUNT_PREFIX"] = mount_prefix + + # These are historical variables which lack the common name prefix + if not in_chroot: + env["INSTALL_ROOT"] = self._instroot + env["IMG_NAME"] = self._name + + return env + + def __get_imgname(self): + return self.name + _name = property(__get_imgname) + """The name of the image file. + + """ + + def _get_kernel_versions(self): + """Return a dict detailing the available kernel types/versions. + + This is the hook where subclasses may override what kernel types and + versions should be available for e.g. creating the booloader + configuration. + + A dict should be returned mapping the available kernel types to a list + of the available versions for those kernels. + + The default implementation uses rpm to iterate over everything + providing 'kernel', finds /boot/vmlinuz-* and returns the version + obtained from the vmlinuz filename. (This can differ from the kernel + RPM's n-v-r in the case of e.g. xen) + + """ + def get_kernel_versions(instroot): + ret = {} + versions = set() + files = glob.glob(instroot + "/boot/vmlinuz-*") + for file in files: + version = os.path.basename(file)[8:] + if version is None: + continue + versions.add(version) + ret["kernel"] = list(versions) + return ret + + def get_version(header): + version = None + for f in header['filenames']: + if f.startswith('/boot/vmlinuz-'): + version = f[14:] + return version + + if self.ks is None: + return get_kernel_versions(self._instroot) + + ts = rpm.TransactionSet(self._instroot) + + ret = {} + for header in ts.dbMatch('provides', 'kernel'): + version = get_version(header) + if version is None: + continue + + name = header['name'] + if not name in ret: + ret[name] = [version] + elif not version in ret[name]: + ret[name].append(version) + + return ret + + + # + # Helpers for subclasses + # + def _do_bindmounts(self): + """Mount various system directories onto _instroot. + + This method is called by mount(), but may also be used by subclasses + in order to re-mount the bindmounts after modifying the underlying + filesystem. + + """ + for b in self.__bindmounts: + b.mount() + + def _undo_bindmounts(self): + """Unmount the bind-mounted system directories from _instroot. + + This method is usually only called by unmount(), but may also be used + by subclasses in order to gain access to the filesystem obscured by + the bindmounts - e.g. in order to create device nodes on the image + filesystem. + + """ + self.__bindmounts.reverse() + for b in self.__bindmounts: + b.unmount() + + def _chroot(self): + """Chroot into the install root. + + This method may be used by subclasses when executing programs inside + the install root e.g. + + subprocess.call(["/bin/ls"], preexec_fn = self.chroot) + + """ + os.chroot(self._instroot) + os.chdir("/") + + def _mkdtemp(self, prefix = "tmp-"): + """Create a temporary directory. + + This method may be used by subclasses to create a temporary directory + for use in building the final image - e.g. a subclass might create + a temporary directory in order to bundle a set of files into a package. + + The subclass may delete this directory if it wishes, but it will be + automatically deleted by cleanup(). + + The absolute path to the temporary directory is returned. + + Note, this method should only be called after mount() has been called. + + prefix -- a prefix which should be used when creating the directory; + defaults to "tmp-". + + """ + self.__ensure_builddir() + return tempfile.mkdtemp(dir = self.__builddir, prefix = prefix) + + def _mkstemp(self, prefix = "tmp-"): + """Create a temporary file. + + This method may be used by subclasses to create a temporary file + for use in building the final image - e.g. a subclass might need + a temporary location to unpack a compressed file. + + The subclass may delete this file if it wishes, but it will be + automatically deleted by cleanup(). + + A tuple containing a file descriptor (returned from os.open() and the + absolute path to the temporary directory is returned. + + Note, this method should only be called after mount() has been called. + + prefix -- a prefix which should be used when creating the file; + defaults to "tmp-". + + """ + self.__ensure_builddir() + return tempfile.mkstemp(dir = self.__builddir, prefix = prefix) + + def _mktemp(self, prefix = "tmp-"): + """Create a temporary file. + + This method simply calls _mkstemp() and closes the returned file + descriptor. + + The absolute path to the temporary file is returned. + + Note, this method should only be called after mount() has been called. + + prefix -- a prefix which should be used when creating the file; + defaults to "tmp-". + + """ + + (f, path) = self._mkstemp(prefix) + os.close(f) + return path + + + # + # Actual implementation + # + def __ensure_builddir(self): + if not self.__builddir is None: + return + + try: + self.workdir = os.path.join(self.tmpdir, "build") + if not os.path.exists(self.workdir): + os.makedirs(self.workdir) + self.__builddir = tempfile.mkdtemp(dir = self.workdir, + prefix = "imgcreate-") + except OSError, (err, msg): + raise CreatorError("Failed create build directory in %s: %s" % + (self.tmpdir, msg)) + + def get_cachedir(self, cachedir = None): + if self.cachedir: + return self.cachedir + + self.__ensure_builddir() + if cachedir: + self.cachedir = cachedir + else: + self.cachedir = self.__builddir + "/mic-cache" + fs.makedirs(self.cachedir) + return self.cachedir + + def __sanity_check(self): + """Ensure that the config we've been given is sane.""" + if not (kickstart.get_packages(self.ks) or + kickstart.get_groups(self.ks)): + raise CreatorError("No packages or groups specified") + + kickstart.convert_method_to_repo(self.ks) + + if not kickstart.get_repos(self.ks): + raise CreatorError("No repositories specified") + + def __write_fstab(self): + fstab_contents = self._get_fstab() + if fstab_contents: + fstab = open(self._instroot + "/etc/fstab", "w") + fstab.write(fstab_contents) + fstab.close() + + def __create_minimal_dev(self): + """Create a minimal /dev so that we don't corrupt the host /dev""" + origumask = os.umask(0000) + devices = (('null', 1, 3, 0666), + ('urandom',1, 9, 0666), + ('random', 1, 8, 0666), + ('full', 1, 7, 0666), + ('ptmx', 5, 2, 0666), + ('tty', 5, 0, 0666), + ('zero', 1, 5, 0666)) + + links = (("/proc/self/fd", "/dev/fd"), + ("/proc/self/fd/0", "/dev/stdin"), + ("/proc/self/fd/1", "/dev/stdout"), + ("/proc/self/fd/2", "/dev/stderr")) + + for (node, major, minor, perm) in devices: + if not os.path.exists(self._instroot + "/dev/" + node): + os.mknod(self._instroot + "/dev/" + node, + perm | stat.S_IFCHR, + os.makedev(major,minor)) + + for (src, dest) in links: + if not os.path.exists(self._instroot + dest): + os.symlink(src, self._instroot + dest) + + os.umask(origumask) + + def __setup_tmpdir(self): + if not self.enabletmpfs: + return + + runner.show('mount -t tmpfs -o size=4G tmpfs %s' % self.workdir) + + def __clean_tmpdir(self): + if not self.enabletmpfs: + return + + runner.show('umount -l %s' % self.workdir) + + def mount(self, base_on = None, cachedir = None): + """Setup the target filesystem in preparation for an install. + + This function sets up the filesystem which the ImageCreator will + install into and configure. The ImageCreator class merely creates an + install root directory, bind mounts some system directories (e.g. /dev) + and writes out /etc/fstab. Other subclasses may also e.g. create a + sparse file, format it and loopback mount it to the install root. + + base_on -- a previous install on which to base this install; defaults + to None, causing a new image to be created + + cachedir -- a directory in which to store the Yum cache; defaults to + None, causing a new cache to be created; by setting this + to another directory, the same cache can be reused across + multiple installs. + + """ + self.__setup_tmpdir() + self.__ensure_builddir() + + # prevent popup dialog in Ubuntu(s) + misc.hide_loopdev_presentation() + + fs.makedirs(self._instroot) + fs.makedirs(self._outdir) + + self._mount_instroot(base_on) + + for d in ("/dev/pts", + "/etc", + "/boot", + "/var/log", + "/sys", + "/proc", + "/usr/bin"): + fs.makedirs(self._instroot + d) + + if self.target_arch and self.target_arch.startswith("arm"): + self.qemu_emulator = misc.setup_qemu_emulator(self._instroot, + self.target_arch) + + + self.get_cachedir(cachedir) + + # bind mount system directories into _instroot + for (f, dest) in [("/sys", None), + ("/proc", None), + ("/proc/sys/fs/binfmt_misc", None), + ("/dev/pts", None)]: + self.__bindmounts.append( + fs.BindChrootMount( + f, self._instroot, dest)) + + self._do_bindmounts() + + self.__create_minimal_dev() + + if os.path.exists(self._instroot + "/etc/mtab"): + os.unlink(self._instroot + "/etc/mtab") + os.symlink("../proc/mounts", self._instroot + "/etc/mtab") + + self.__write_fstab() + + # get size of available space in 'instroot' fs + self._root_fs_avail = misc.get_filesystem_avail(self._instroot) + + def unmount(self): + """Unmounts the target filesystem. + + The ImageCreator class detaches the system from the install root, but + other subclasses may also detach the loopback mounted filesystem image + from the install root. + + """ + try: + mtab = self._instroot + "/etc/mtab" + if not os.path.islink(mtab): + os.unlink(self._instroot + "/etc/mtab") + + if self.qemu_emulator: + os.unlink(self._instroot + self.qemu_emulator) + except OSError: + pass + + self._undo_bindmounts() + + """ Clean up yum garbage """ + try: + instroot_pdir = os.path.dirname(self._instroot + self._instroot) + if os.path.exists(instroot_pdir): + shutil.rmtree(instroot_pdir, ignore_errors = True) + yumlibdir = self._instroot + "/var/lib/yum" + if os.path.exists(yumlibdir): + shutil.rmtree(yumlibdir, ignore_errors = True) + except OSError: + pass + + self._unmount_instroot() + + # reset settings of popup dialog in Ubuntu(s) + misc.unhide_loopdev_presentation() + + + def cleanup(self): + """Unmounts the target filesystem and deletes temporary files. + + This method calls unmount() and then deletes any temporary files and + directories that were created on the host system while building the + image. + + Note, make sure to call this method once finished with the creator + instance in order to ensure no stale files are left on the host e.g.: + + creator = ImageCreator(ks, name) + try: + creator.create() + finally: + creator.cleanup() + + """ + if not self.__builddir: + return + + self.unmount() + + shutil.rmtree(self.__builddir, ignore_errors = True) + self.__builddir = None + + self.__clean_tmpdir() + + def __is_excluded_pkg(self, pkg): + if pkg in self._excluded_pkgs: + self._excluded_pkgs.remove(pkg) + return True + + for xpkg in self._excluded_pkgs: + if xpkg.endswith('*'): + if pkg.startswith(xpkg[:-1]): + return True + elif xpkg.startswith('*'): + if pkg.endswith(xpkg[1:]): + return True + + return None + + def __select_packages(self, pkg_manager): + skipped_pkgs = [] + for pkg in self._required_pkgs: + e = pkg_manager.selectPackage(pkg) + if e: + if kickstart.ignore_missing(self.ks): + skipped_pkgs.append(pkg) + elif self.__is_excluded_pkg(pkg): + skipped_pkgs.append(pkg) + else: + raise CreatorError("Failed to find package '%s' : %s" % + (pkg, e)) + + for pkg in skipped_pkgs: + msger.warning("Skipping missing package '%s'" % (pkg,)) + + def __select_groups(self, pkg_manager): + skipped_groups = [] + for group in self._required_groups: + e = pkg_manager.selectGroup(group.name, group.include) + if e: + if kickstart.ignore_missing(self.ks): + skipped_groups.append(group) + else: + raise CreatorError("Failed to find group '%s' : %s" % + (group.name, e)) + + for group in skipped_groups: + msger.warning("Skipping missing group '%s'" % (group.name,)) + + def __deselect_packages(self, pkg_manager): + for pkg in self._excluded_pkgs: + pkg_manager.deselectPackage(pkg) + + def __localinst_packages(self, pkg_manager): + for rpm_path in self._get_local_packages(): + pkg_manager.installLocal(rpm_path) + + def __preinstall_packages(self, pkg_manager): + if not self.ks: + return + + self._preinstall_pkgs = kickstart.get_pre_packages(self.ks) + for pkg in self._preinstall_pkgs: + pkg_manager.preInstall(pkg) + + def __attachment_packages(self, pkg_manager): + if not self.ks: + return + + self._attachment = [] + for item in kickstart.get_attachment(self.ks): + if item.startswith('/'): + fpaths = os.path.join(self._instroot, item.lstrip('/')) + for fpath in glob.glob(fpaths): + self._attachment.append(fpath) + continue + + filelist = pkg_manager.getFilelist(item) + if filelist: + # found rpm in rootfs + for pfile in pkg_manager.getFilelist(item): + fpath = os.path.join(self._instroot, pfile.lstrip('/')) + self._attachment.append(fpath) + continue + + # try to retrieve rpm file + (url, proxies) = pkg_manager.package_url(item) + if not url: + msger.warning("Can't get url from repo for %s" % item) + continue + fpath = os.path.join(self.cachedir, os.path.basename(url)) + if not os.path.exists(fpath): + # download pkgs + try: + fpath = grabber.myurlgrab(url, fpath, proxies, None) + except CreatorError: + raise + + tmpdir = self._mkdtemp() + misc.extract_rpm(fpath, tmpdir) + for (root, dirs, files) in os.walk(tmpdir): + for fname in files: + fpath = os.path.join(root, fname) + self._attachment.append(fpath) + + def install(self, repo_urls=None): + """Install packages into the install root. + + This function installs the packages listed in the supplied kickstart + into the install root. By default, the packages are installed from the + repository URLs specified in the kickstart. + + repo_urls -- a dict which maps a repository name to a repository URL; + if supplied, this causes any repository URLs specified in + the kickstart to be overridden. + + """ + + # initialize pkg list to install + if self.ks: + self.__sanity_check() + + self._required_pkgs = \ + kickstart.get_packages(self.ks, self._get_required_packages()) + self._excluded_pkgs = \ + kickstart.get_excluded(self.ks, self._get_excluded_packages()) + self._required_groups = kickstart.get_groups(self.ks) + else: + self._required_pkgs = None + self._excluded_pkgs = None + self._required_groups = None + + pkg_manager = self.get_pkg_manager() + pkg_manager.setup() + + if hasattr(self, 'install_pkgs') and self.install_pkgs: + if 'debuginfo' in self.install_pkgs: + pkg_manager.install_debuginfo = True + + for repo in kickstart.get_repos(self.ks, repo_urls): + (name, baseurl, mirrorlist, inc, exc, + proxy, proxy_username, proxy_password, debuginfo, + source, gpgkey, disable, ssl_verify, nocache, + cost, priority) = repo + + yr = pkg_manager.addRepository(name, baseurl, mirrorlist, proxy, + proxy_username, proxy_password, inc, exc, ssl_verify, + nocache, cost, priority) + + if kickstart.exclude_docs(self.ks): + rpm.addMacro("_excludedocs", "1") + rpm.addMacro("_dbpath", "/var/lib/rpm") + rpm.addMacro("__file_context_path", "%{nil}") + if kickstart.inst_langs(self.ks) != None: + rpm.addMacro("_install_langs", kickstart.inst_langs(self.ks)) + + try: + self.__preinstall_packages(pkg_manager) + self.__select_packages(pkg_manager) + self.__select_groups(pkg_manager) + self.__deselect_packages(pkg_manager) + self.__localinst_packages(pkg_manager) + + BOOT_SAFEGUARD = 256L * 1024 * 1024 # 256M + checksize = self._root_fs_avail + if checksize: + checksize -= BOOT_SAFEGUARD + if self.target_arch: + pkg_manager._add_prob_flags(rpm.RPMPROB_FILTER_IGNOREARCH) + pkg_manager.runInstall(checksize) + except CreatorError, e: + raise + except KeyboardInterrupt: + raise + else: + self._pkgs_content = pkg_manager.getAllContent() + self._pkgs_license = pkg_manager.getPkgsLicense() + self._pkgs_vcsinfo = pkg_manager.getVcsInfo() + self.__attachment_packages(pkg_manager) + finally: + pkg_manager.close() + + # hook post install + self.postinstall() + + # do some clean up to avoid lvm info leakage. this sucks. + for subdir in ("cache", "backup", "archive"): + lvmdir = self._instroot + "/etc/lvm/" + subdir + try: + for f in os.listdir(lvmdir): + os.unlink(lvmdir + "/" + f) + except: + pass + + def postinstall(self): + self.copy_attachment() + + def __run_post_scripts(self): + msger.info("Running scripts ...") + if os.path.exists(self._instroot + "/tmp"): + shutil.rmtree(self._instroot + "/tmp") + os.mkdir (self._instroot + "/tmp", 0755) + for s in kickstart.get_post_scripts(self.ks): + (fd, path) = tempfile.mkstemp(prefix = "ks-script-", + dir = self._instroot + "/tmp") + + s.script = s.script.replace("\r", "") + os.write(fd, s.script) + os.close(fd) + os.chmod(path, 0700) + + env = self._get_post_scripts_env(s.inChroot) + + if not s.inChroot: + preexec = None + script = path + else: + preexec = self._chroot + script = "/tmp/" + os.path.basename(path) + + try: + try: + subprocess.call([s.interp, script], + preexec_fn = preexec, + env = env, + stdout = sys.stdout, + stderr = sys.stderr) + except OSError, (err, msg): + raise CreatorError("Failed to execute %%post script " + "with '%s' : %s" % (s.interp, msg)) + finally: + os.unlink(path) + + def __save_repo_keys(self, repodata): + if not repodata: + return None + + gpgkeydir = "/etc/pki/rpm-gpg" + fs.makedirs(self._instroot + gpgkeydir) + for repo in repodata: + if repo["repokey"]: + repokey = gpgkeydir + "/RPM-GPG-KEY-%s" % repo["name"] + shutil.copy(repo["repokey"], self._instroot + repokey) + + def configure(self, repodata = None): + """Configure the system image according to the kickstart. + + This method applies the (e.g. keyboard or network) configuration + specified in the kickstart and executes the kickstart %post scripts. + + If necessary, it also prepares the image to be bootable by e.g. + creating an initrd and bootloader configuration. + + """ + ksh = self.ks.handler + + msger.info('Applying configurations ...') + try: + kickstart.LanguageConfig(self._instroot).apply(ksh.lang) + kickstart.KeyboardConfig(self._instroot).apply(ksh.keyboard) + kickstart.TimezoneConfig(self._instroot).apply(ksh.timezone) + #kickstart.AuthConfig(self._instroot).apply(ksh.authconfig) + kickstart.FirewallConfig(self._instroot).apply(ksh.firewall) + kickstart.RootPasswordConfig(self._instroot).apply(ksh.rootpw) + kickstart.UserConfig(self._instroot).apply(ksh.user) + kickstart.ServicesConfig(self._instroot).apply(ksh.services) + kickstart.XConfig(self._instroot).apply(ksh.xconfig) + kickstart.NetworkConfig(self._instroot).apply(ksh.network) + kickstart.RPMMacroConfig(self._instroot).apply(self.ks) + kickstart.DesktopConfig(self._instroot).apply(ksh.desktop) + self.__save_repo_keys(repodata) + kickstart.MoblinRepoConfig(self._instroot).apply(ksh.repo, repodata, self.repourl) + except: + msger.warning("Failed to apply configuration to image") + raise + + self._create_bootconfig() + self.__run_post_scripts() + + def launch_shell(self, launch): + """Launch a shell in the install root. + + This method is launches a bash shell chroot()ed in the install root; + this can be useful for debugging. + + """ + if launch: + msger.info("Launching shell. Exit to continue.") + subprocess.call(["/bin/bash"], preexec_fn = self._chroot) + + def do_genchecksum(self, image_name): + if not self._genchecksum: + return + + md5sum = misc.get_md5sum(image_name) + with open(image_name + ".md5sum", "w") as f: + f.write("%s %s" % (md5sum, os.path.basename(image_name))) + self.outimage.append(image_name+".md5sum") + + def package(self, destdir = "."): + """Prepares the created image for final delivery. + + In its simplest form, this method merely copies the install root to the + supplied destination directory; other subclasses may choose to package + the image by e.g. creating a bootable ISO containing the image and + bootloader configuration. + + destdir -- the directory into which the final image should be moved; + this defaults to the current directory. + + """ + self._stage_final_image() + + if not os.path.exists(destdir): + fs.makedirs(destdir) + + if self._recording_pkgs: + self._save_recording_pkgs(destdir) + + # For image formats with two or multiple image files, it will be + # better to put them under a directory + if self.image_format in ("raw", "vmdk", "vdi", "nand", "mrstnand"): + destdir = os.path.join(destdir, "%s-%s" \ + % (self.name, self.image_format)) + msger.debug("creating destination dir: %s" % destdir) + fs.makedirs(destdir) + + # Ensure all data is flushed to _outdir + runner.quiet('sync') + + misc.check_space_pre_cp(self._outdir, destdir) + for f in os.listdir(self._outdir): + shutil.move(os.path.join(self._outdir, f), + os.path.join(destdir, f)) + self.outimage.append(os.path.join(destdir, f)) + self.do_genchecksum(os.path.join(destdir, f)) + + def print_outimage_info(self): + msg = "The new image can be found here:\n" + self.outimage.sort() + for file in self.outimage: + msg += ' %s\n' % os.path.abspath(file) + + msger.info(msg) + + def check_depend_tools(self): + for tool in self._dep_checks: + fs.find_binary_path(tool) + + def package_output(self, image_format, destdir = ".", package="none"): + if not package or package == "none": + return + + destdir = os.path.abspath(os.path.expanduser(destdir)) + (pkg, comp) = os.path.splitext(package) + if comp: + comp=comp.lstrip(".") + + if pkg == "tar": + if comp: + dst = "%s/%s-%s.tar.%s" %\ + (destdir, self.name, image_format, comp) + else: + dst = "%s/%s-%s.tar" %\ + (destdir, self.name, image_format) + + msger.info("creating %s" % dst) + tar = tarfile.open(dst, "w:" + comp) + + for file in self.outimage: + msger.info("adding %s to %s" % (file, dst)) + tar.add(file, + arcname=os.path.join("%s-%s" \ + % (self.name, image_format), + os.path.basename(file))) + if os.path.isdir(file): + shutil.rmtree(file, ignore_errors = True) + else: + os.remove(file) + + tar.close() + + '''All the file in outimage has been packaged into tar.* file''' + self.outimage = [dst] + + def release_output(self, config, destdir, release): + """ Create release directory and files + """ + + def _rpath(fn): + """ release path """ + return os.path.join(destdir, fn) + + outimages = self.outimage + + # new ks + new_kspath = _rpath(self.name+'.ks') + with open(config) as fr: + with open(new_kspath, "w") as wf: + # When building a release we want to make sure the .ks + # file generates the same build even when --release not used. + wf.write(fr.read().replace("@BUILD_ID@", release)) + outimages.append(new_kspath) + + # save log file, logfile is only available in creator attrs + if hasattr(self, 'logfile') and not self.logfile: + log_path = _rpath(self.name + ".log") + # touch the log file, else outimages will filter it out + with open(log_path, 'w') as wf: + wf.write('') + msger.set_logfile(log_path) + outimages.append(_rpath(self.name + ".log")) + + # rename iso and usbimg + for f in os.listdir(destdir): + if f.endswith(".iso"): + newf = f[:-4] + '.img' + elif f.endswith(".usbimg"): + newf = f[:-7] + '.img' + else: + continue + os.rename(_rpath(f), _rpath(newf)) + outimages.append(_rpath(newf)) + + # generate MD5SUMS + with open(_rpath("MD5SUMS"), "w") as wf: + for f in os.listdir(destdir): + if f == "MD5SUMS": + continue + + if os.path.isdir(os.path.join(destdir, f)): + continue + + md5sum = misc.get_md5sum(_rpath(f)) + # There needs to be two spaces between the sum and + # filepath to match the syntax with md5sum. + # This way also md5sum -c MD5SUMS can be used by users + wf.write("%s *%s\n" % (md5sum, f)) + + outimages.append("%s/MD5SUMS" % destdir) + + # Filter out the nonexist file + for fp in outimages[:]: + if not os.path.exists("%s" % fp): + outimages.remove(fp) + + def copy_kernel(self): + """ Copy kernel files to the outimage directory. + NOTE: This needs to be called before unmounting the instroot. + """ + + if not self._need_copy_kernel: + return + + if not os.path.exists(self.destdir): + os.makedirs(self.destdir) + + for kernel in glob.glob("%s/boot/vmlinuz-*" % self._instroot): + kernelfilename = "%s/%s-%s" % (self.destdir, + self.name, + os.path.basename(kernel)) + msger.info('copy kernel file %s as %s' % (os.path.basename(kernel), + kernelfilename)) + shutil.copy(kernel, kernelfilename) + self.outimage.append(kernelfilename) + + def copy_attachment(self): + """ Subclass implement it to handle attachment files + NOTE: This needs to be called before unmounting the instroot. + """ + pass + + def get_pkg_manager(self): + return self.pkgmgr(target_arch = self.target_arch, + instroot = self._instroot, + cachedir = self.cachedir) |