2 # creator.py : ImageCreator and LoopImageCreator base classes
4 # Copyright 2007, Red Hat Inc.
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; version 2 of the License.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU Library General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
33 from micng.utils.errors import *
34 from micng.utils.fs_related import *
35 from micng.utils import kickstart
36 from micng.utils import pkgmanagers
37 from micng.utils.rpmmisc import *
38 from micng.utils.misc import *
41 """The maximum string length supported for LoopImageCreator.fslabel."""
43 class ImageCreator(object):
44 """Installs a system to a chroot directory.
46 ImageCreator is the simplest creator class available; it will install and
47 configure a system image according to the supplied kickstart file.
51 import micng.imgcreate as imgcreate
52 ks = imgcreate.read_kickstart("foo.ks")
53 imgcreate.ImageCreator(ks, "foo").create()
57 def __init__(self, ks, name):
58 """Initialize an ImageCreator instance.
60 ks -- a pykickstart.KickstartParser instance; this instance will be
61 used to drive the install by e.g. providing the list of packages
62 to be installed, the system configuration and %post scripts
64 name -- a name for the image; used for e.g. image filenames or
69 """ Initialize package managers """
70 #package plugin manager
71 self.pkgmgr = pkgmanagers.pkgManager()
72 self.pkgmgr.load_pkg_managers()
75 """A pykickstart.KickstartParser instance."""
78 """A name for the image."""
80 self.distro_name = "MeeGo"
82 """Output image file names"""
85 """A flag to generate checksum"""
86 self._genchecksum = False
88 self.tmpdir = "/var/tmp"
89 """The directory in which all temporary files will be created."""
93 self._alt_initrd_name = None
95 self.__builddir = None
96 self.__bindmounts = []
98 """ Contains the compression method that is used to compress
99 the disk image after creation, e.g., bz2.
100 This value is set with compression_method function. """
101 self.__img_compression_method = None
103 # dependent commands to check
104 self._dep_checks = ["ls", "bash", "cp", "echo", "modprobe", "passwd"]
106 self._recording_pkgs = None
108 self._include_src = None
110 self._local_pkgs_path = None
112 # available size in root fs, init to 0
113 self._root_fs_avail = 0
115 # target arch for non-x86 image
116 self.target_arch = None
118 """ Name of the disk image file that is created. """
119 self._img_name = None
122 self.image_format = None
124 """ Save qemu emulator file name in order to clean up it finally """
125 self.qemu_emulator = None
127 """ No ks provided when called by convertor, so skip the dependency check """
129 """ If we have btrfs partition we need to check that we have toosl for those """
130 for part in self.ks.handler.partition.partitions:
131 if part.fstype and part.fstype == "btrfs":
132 self._dep_checks.append("mkfs.btrfs")
135 def set_target_arch(self, arch):
136 if arch not in arches.keys():
138 self.target_arch = arch
139 if self.target_arch.startswith("arm"):
140 for dep in self._dep_checks:
141 if dep == "extlinux":
142 self._dep_checks.remove(dep)
144 if not os.path.exists("/usr/bin/qemu-arm") or not is_statically_linked("/usr/bin/qemu-arm"):
145 self._dep_checks.append("qemu-arm-static")
147 if os.path.exists("/proc/sys/vm/vdso_enabled"):
148 vdso_fh = open("/proc/sys/vm/vdso_enabled","r")
149 vdso_value = vdso_fh.read().strip()
151 if (int)(vdso_value) == 1:
152 print "\n= WARNING ="
153 print "vdso is enabled on your host, which might cause problems with arm emulations."
154 print "You can disable vdso with following command before starting image build:"
155 print "echo 0 | sudo tee /proc/sys/vm/vdso_enabled"
156 print "= WARNING =\n"
167 def __get_instroot(self):
168 if self.__builddir is None:
169 raise CreatorError("_instroot is not valid before calling mount()")
170 return self.__builddir + "/install_root"
171 _instroot = property(__get_instroot)
172 """The location of the install root directory.
174 This is the directory into which the system is installed. Subclasses may
175 mount a filesystem image here or copy files to/from here.
177 Note, this directory does not exist before ImageCreator.mount() is called.
179 Note also, this is a read-only attribute.
183 def __get_outdir(self):
184 if self.__builddir is None:
185 raise CreatorError("_outdir is not valid before calling mount()")
186 return self.__builddir + "/out"
187 _outdir = property(__get_outdir)
188 """The staging location for the final image.
190 This is where subclasses should stage any files that are part of the final
191 image. ImageCreator.package() will copy any files found here into the
192 requested destination directory.
194 Note, this directory does not exist before ImageCreator.mount() is called.
196 Note also, this is a read-only attribute.
201 # Hooks for subclasses
203 def _mount_instroot(self, base_on = None):
204 """Mount or prepare the install root directory.
206 This is the hook where subclasses may prepare the install root by e.g.
207 mounting creating and loopback mounting a filesystem image to
210 There is no default implementation.
212 base_on -- this is the value passed to mount() and can be interpreted
213 as the subclass wishes; it might e.g. be the location of
214 a previously created ISO containing a system image.
219 def _unmount_instroot(self):
220 """Undo anything performed in _mount_instroot().
222 This is the hook where subclasses must undo anything which was done
223 in _mount_instroot(). For example, if a filesystem image was mounted
224 onto _instroot, it should be unmounted here.
226 There is no default implementation.
231 def _create_bootconfig(self):
232 """Configure the image so that it's bootable.
234 This is the hook where subclasses may prepare the image for booting by
235 e.g. creating an initramfs and bootloader configuration.
237 This hook is called while the install root is still mounted, after the
238 packages have been installed and the kickstart configuration has been
239 applied, but before the %post scripts have been executed.
241 There is no default implementation.
246 def _stage_final_image(self):
247 """Stage the final system image in _outdir.
249 This is the hook where subclasses should place the image in _outdir
250 so that package() can copy it to the requested destination directory.
252 By default, this moves the install root into _outdir.
255 shutil.move(self._instroot, self._outdir + "/" + self.name)
257 def get_installed_packages(self):
258 return self._pkgs_content.keys()
260 def _save_recording_pkgs(self, destdir):
261 """Save the list or content of installed packages to file.
263 if self._recording_pkgs not in ('content', 'name'):
266 pkgs = self._pkgs_content.keys()
267 pkgs.sort() # inplace op
269 # save package name list anyhow
270 if not os.path.exists(destdir):
273 namefile = os.path.join(destdir, self.name + '-pkgs.txt')
274 f = open(namefile, "w")
275 content = '\n'.join(pkgs)
278 self.outimage.append(namefile);
280 # if 'content', save more details
281 if self._recording_pkgs == 'content':
282 contfile = os.path.join(destdir, self.name + '-pkgs-content.txt')
283 f = open(contfile, "w")
288 pkgcont = self._pkgs_content[pkg]
290 if pkgcont.has_key('dir'):
291 items = map(lambda x:x+'/', pkgcont['dir'])
292 if pkgcont.has_key('file'):
293 items.extend(pkgcont['file'])
297 content += '\n '.join(items)
303 self.outimage.append(contfile)
305 def _get_required_packages(self):
306 """Return a list of required packages.
308 This is the hook where subclasses may specify a set of packages which
309 it requires to be installed.
311 This returns an empty list by default.
313 Note, subclasses should usually chain up to the base class
314 implementation of this hook.
319 def _get_excluded_packages(self):
320 """Return a list of excluded packages.
322 This is the hook where subclasses may specify a set of packages which
323 it requires _not_ to be installed.
325 This returns an empty list by default.
327 Note, subclasses should usually chain up to the base class
328 implementation of this hook.
331 excluded_packages = []
332 for rpm_path in self._get_local_packages():
333 rpm_name = os.path.basename(rpm_path)
334 package_name = splitFilename(rpm_name)[0]
335 excluded_packages += [package_name]
336 return excluded_packages
338 def _get_local_packages(self):
339 """Return a list of rpm path to be local installed.
341 This is the hook where subclasses may specify a set of rpms which
342 it requires to be installed locally.
344 This returns an empty list by default.
346 Note, subclasses should usually chain up to the base class
347 implementation of this hook.
350 if self._local_pkgs_path:
351 if os.path.isdir(self._local_pkgs_path):
353 os.path.join(self._local_pkgs_path, '*.rpm'))
354 elif os.path.splitext(self._local_pkgs_path)[-1] == '.rpm':
355 return [self._local_pkgs_path]
359 def _get_fstab(self):
360 """Return the desired contents of /etc/fstab.
362 This is the hook where subclasses may specify the contents of
363 /etc/fstab by returning a string containing the desired contents.
365 A sensible default implementation is provided.
368 s = "/dev/root / %s %s 0 0\n" % (self._fstype, "defaults,noatime" if not self._fsopts else self._fsopts)
369 s += self._get_fstab_special()
372 def _get_fstab_special(self):
373 s = "devpts /dev/pts devpts gid=5,mode=620 0 0\n"
374 s += "tmpfs /dev/shm tmpfs defaults 0 0\n"
375 s += "proc /proc proc defaults 0 0\n"
376 s += "sysfs /sys sysfs defaults 0 0\n"
379 def _get_post_scripts_env(self, in_chroot):
380 """Return an environment dict for %post scripts.
382 This is the hook where subclasses may specify some environment
383 variables for %post scripts by return a dict containing the desired
386 By default, this returns an empty dict.
388 in_chroot -- whether this %post script is to be executed chroot()ed
394 def __get_imgname(self):
396 _name = property(__get_imgname)
397 """The name of the image file.
401 def _get_kernel_versions(self):
402 """Return a dict detailing the available kernel types/versions.
404 This is the hook where subclasses may override what kernel types and
405 versions should be available for e.g. creating the booloader
408 A dict should be returned mapping the available kernel types to a list
409 of the available versions for those kernels.
411 The default implementation uses rpm to iterate over everything
412 providing 'kernel', finds /boot/vmlinuz-* and returns the version
413 obtained from the vmlinuz filename. (This can differ from the kernel
414 RPM's n-v-r in the case of e.g. xen)
417 def get_version(header):
419 for f in header['filenames']:
420 if f.startswith('/boot/vmlinuz-'):
424 ts = rpm.TransactionSet(self._instroot)
427 for header in ts.dbMatch('provides', 'kernel'):
428 version = get_version(header)
432 name = header['name']
434 ret[name] = [version]
435 elif not version in ret[name]:
436 ret[name].append(version)
441 # Helpers for subclasses
443 def _do_bindmounts(self):
444 """Mount various system directories onto _instroot.
446 This method is called by mount(), but may also be used by subclasses
447 in order to re-mount the bindmounts after modifying the underlying
451 for b in self.__bindmounts:
454 def _undo_bindmounts(self):
455 """Unmount the bind-mounted system directories from _instroot.
457 This method is usually only called by unmount(), but may also be used
458 by subclasses in order to gain access to the filesystem obscured by
459 the bindmounts - e.g. in order to create device nodes on the image
463 self.__bindmounts.reverse()
464 for b in self.__bindmounts:
468 """Chroot into the install root.
470 This method may be used by subclasses when executing programs inside
471 the install root e.g.
473 subprocess.call(["/bin/ls"], preexec_fn = self.chroot)
476 os.chroot(self._instroot)
479 def _mkdtemp(self, prefix = "tmp-"):
480 """Create a temporary directory.
482 This method may be used by subclasses to create a temporary directory
483 for use in building the final image - e.g. a subclass might create
484 a temporary directory in order to bundle a set of files into a package.
486 The subclass may delete this directory if it wishes, but it will be
487 automatically deleted by cleanup().
489 The absolute path to the temporary directory is returned.
491 Note, this method should only be called after mount() has been called.
493 prefix -- a prefix which should be used when creating the directory;
497 self.__ensure_builddir()
498 return tempfile.mkdtemp(dir = self.__builddir, prefix = prefix)
500 def _mkstemp(self, prefix = "tmp-"):
501 """Create a temporary file.
503 This method may be used by subclasses to create a temporary file
504 for use in building the final image - e.g. a subclass might need
505 a temporary location to unpack a compressed file.
507 The subclass may delete this file if it wishes, but it will be
508 automatically deleted by cleanup().
510 A tuple containing a file descriptor (returned from os.open() and the
511 absolute path to the temporary directory is returned.
513 Note, this method should only be called after mount() has been called.
515 prefix -- a prefix which should be used when creating the file;
519 self.__ensure_builddir()
520 return tempfile.mkstemp(dir = self.__builddir, prefix = prefix)
522 def _mktemp(self, prefix = "tmp-"):
523 """Create a temporary file.
525 This method simply calls _mkstemp() and closes the returned file
528 The absolute path to the temporary file is returned.
530 Note, this method should only be called after mount() has been called.
532 prefix -- a prefix which should be used when creating the file;
537 (f, path) = self._mkstemp(prefix)
542 # Actual implementation
544 def __ensure_builddir(self):
545 if not self.__builddir is None:
549 self.__builddir = tempfile.mkdtemp(dir = self.tmpdir,
550 prefix = "imgcreate-")
551 except OSError, (err, msg):
552 raise CreatorError("Failed create build directory in %s: %s" %
555 def get_cachedir(self, cachedir = None):
559 self.__ensure_builddir()
561 self.cachedir = cachedir
563 self.cachedir = self.__builddir + "/yum-cache"
564 makedirs(self.cachedir)
567 def __sanity_check(self):
568 """Ensure that the config we've been given is sane."""
569 if not (kickstart.get_packages(self.ks) or
570 kickstart.get_groups(self.ks)):
571 raise CreatorError("No packages or groups specified")
573 kickstart.convert_method_to_repo(self.ks)
575 if not kickstart.get_repos(self.ks):
576 raise CreatorError("No repositories specified")
578 def __write_fstab(self):
579 fstab = open(self._instroot + "/etc/fstab", "w")
580 fstab.write(self._get_fstab())
583 def __create_minimal_dev(self):
584 """Create a minimal /dev so that we don't corrupt the host /dev"""
585 origumask = os.umask(0000)
586 devices = (('null', 1, 3, 0666),
587 ('urandom',1, 9, 0666),
588 ('random', 1, 8, 0666),
589 ('full', 1, 7, 0666),
590 ('ptmx', 5, 2, 0666),
592 ('zero', 1, 5, 0666))
593 links = (("/proc/self/fd", "/dev/fd"),
594 ("/proc/self/fd/0", "/dev/stdin"),
595 ("/proc/self/fd/1", "/dev/stdout"),
596 ("/proc/self/fd/2", "/dev/stderr"))
598 for (node, major, minor, perm) in devices:
599 if not os.path.exists(self._instroot + "/dev/" + node):
600 os.mknod(self._instroot + "/dev/" + node, perm | stat.S_IFCHR, os.makedev(major,minor))
601 for (src, dest) in links:
602 if not os.path.exists(self._instroot + dest):
603 os.symlink(src, self._instroot + dest)
607 def mount(self, base_on = None, cachedir = None):
608 """Setup the target filesystem in preparation for an install.
610 This function sets up the filesystem which the ImageCreator will
611 install into and configure. The ImageCreator class merely creates an
612 install root directory, bind mounts some system directories (e.g. /dev)
613 and writes out /etc/fstab. Other subclasses may also e.g. create a
614 sparse file, format it and loopback mount it to the install root.
616 base_on -- a previous install on which to base this install; defaults
617 to None, causing a new image to be created
619 cachedir -- a directory in which to store the Yum cache; defaults to
620 None, causing a new cache to be created; by setting this
621 to another directory, the same cache can be reused across
625 self.__ensure_builddir()
627 makedirs(self._instroot)
628 makedirs(self._outdir)
630 self._mount_instroot(base_on)
632 for d in ("/dev/pts", "/etc", "/boot", "/var/log", "/var/cache/yum", "/sys", "/proc", "/usr/bin"):
633 makedirs(self._instroot + d)
635 if self.target_arch and self.target_arch.startswith("arm"):
636 self.qemu_emulator = setup_qemu_emulator(self._instroot, self.target_arch)
638 self.get_cachedir(cachedir)
640 # bind mount system directories into _instroot
641 for (f, dest) in [("/sys", None), ("/proc", None), ("/proc/sys/fs/binfmt_misc", None),
643 (self.get_cachedir(), "/var/cache/yum")]:
644 self.__bindmounts.append(BindChrootMount(f, self._instroot, dest))
647 self._do_bindmounts()
649 self.__create_minimal_dev()
651 if os.path.exists(self._instroot + "/etc/mtab"):
652 os.unlink(self._instroot + "/etc/mtab")
653 os.symlink("../proc/mounts", self._instroot + "/etc/mtab")
657 # get size of available space in 'instroot' fs
658 self._root_fs_avail = get_filesystem_avail(self._instroot)
661 """Unmounts the target filesystem.
663 The ImageCreator class detaches the system from the install root, but
664 other subclasses may also detach the loopback mounted filesystem image
665 from the install root.
669 os.unlink(self._instroot + "/etc/mtab")
670 if self.qemu_emulator:
671 os.unlink(self._instroot + self.qemu_emulator)
672 """ Clean up yum garbage """
673 instroot_pdir = os.path.dirname(self._instroot + self._instroot)
674 if os.path.exists(instroot_pdir):
675 shutil.rmtree(instroot_pdir, ignore_errors = True)
680 self._undo_bindmounts()
682 self._unmount_instroot()
685 """Unmounts the target filesystem and deletes temporary files.
687 This method calls unmount() and then deletes any temporary files and
688 directories that were created on the host system while building the
691 Note, make sure to call this method once finished with the creator
692 instance in order to ensure no stale files are left on the host e.g.:
694 creator = ImageCreator(ks, name)
701 if not self.__builddir:
706 shutil.rmtree(self.__builddir, ignore_errors = True)
707 self.__builddir = None
709 def __is_excluded_pkg(self, pkg):
710 if pkg in self._excluded_pkgs:
711 self._excluded_pkgs.remove(pkg)
714 for xpkg in self._excluded_pkgs:
715 if xpkg.endswith('*'):
716 if pkg.startswith(xpkg[:-1]):
718 elif xpkg.startswith('*'):
719 if pkg.endswith(xpkg[1:]):
724 def __select_packages(self, pkg_manager):
726 for pkg in self._required_pkgs:
727 e = pkg_manager.selectPackage(pkg)
729 if kickstart.ignore_missing(self.ks):
730 skipped_pkgs.append(pkg)
731 elif self.__is_excluded_pkg(pkg):
732 skipped_pkgs.append(pkg)
734 raise CreatorError("Failed to find package '%s' : %s" %
737 for pkg in skipped_pkgs:
738 logging.warn("Skipping missing package '%s'" % (pkg,))
740 def __select_groups(self, pkg_manager):
742 for group in self._required_groups:
743 e = pkg_manager.selectGroup(group.name, group.include)
745 if kickstart.ignore_missing(self.ks):
746 skipped_groups.append(group)
748 raise CreatorError("Failed to find group '%s' : %s" %
751 for group in skipped_groups:
752 logging.warn("Skipping missing group '%s'" % (group.name,))
754 def __deselect_packages(self, pkg_manager):
755 for pkg in self._excluded_pkgs:
756 pkg_manager.deselectPackage(pkg)
758 def __localinst_packages(self, pkg_manager):
759 for rpm_path in self._get_local_packages():
760 pkg_manager.installLocal(rpm_path)
762 def install(self, repo_urls = {}):
763 """Install packages into the install root.
765 This function installs the packages listed in the supplied kickstart
766 into the install root. By default, the packages are installed from the
767 repository URLs specified in the kickstart.
769 repo_urls -- a dict which maps a repository name to a repository URL;
770 if supplied, this causes any repository URLs specified in
771 the kickstart to be overridden.
776 # initialize pkg list to install
780 self.__sanity_check()
782 self._required_pkgs = \
783 kickstart.get_packages(self.ks, self._get_required_packages())
784 self._excluded_pkgs = \
785 kickstart.get_excluded(self.ks, self._get_excluded_packages())
786 self._required_groups = kickstart.get_groups(self.ks)
788 self._required_pkgs = None
789 self._excluded_pkgs = None
790 self._required_groups = None
792 yum_conf = self._mktemp(prefix = "yum.conf-")
795 if self._include_src:
796 keep_record = 'include_src'
797 if self._recording_pkgs in ('name', 'content'):
798 keep_record = self._recording_pkgs
800 pkg_manager = self.get_pkg_manager(keep_record)
801 pkg_manager.setup(yum_conf, self._instroot)
803 for repo in kickstart.get_repos(self.ks, repo_urls):
804 (name, baseurl, mirrorlist, inc, exc, proxy, proxy_username, proxy_password, debuginfo, source, gpgkey, disable) = repo
806 yr = pkg_manager.addRepository(name, baseurl, mirrorlist, proxy, proxy_username, proxy_password, inc, exc)
808 if kickstart.exclude_docs(self.ks):
809 rpm.addMacro("_excludedocs", "1")
810 rpm.addMacro("__file_context_path", "%{nil}")
811 if kickstart.inst_langs(self.ks) != None:
812 rpm.addMacro("_install_langs", kickstart.inst_langs(self.ks))
818 self.__select_packages(pkg_manager)
819 self.__select_groups(pkg_manager)
820 self.__deselect_packages(pkg_manager)
821 self.__localinst_packages(pkg_manager)
823 BOOT_SAFEGUARD = 256L * 1024 * 1024 # 256M
824 checksize = self._root_fs_avail
826 checksize -= BOOT_SAFEGUARD
828 pkg_manager._add_prob_flags(rpm.RPMPROB_FILTER_IGNOREARCH)
829 pkg_manager.runInstall(checksize)
830 except CreatorError, e:
831 raise CreatorError("%s" % (e,))
834 self._pkgs_content = pkg_manager.getAllContent()
836 pkg_manager.closeRpmDB()
840 # do some clean up to avoid lvm info leakage. this sucks.
841 for subdir in ("cache", "backup", "archive"):
842 lvmdir = self._instroot + "/etc/lvm/" + subdir
844 for f in os.listdir(lvmdir):
845 os.unlink(lvmdir + "/" + f)
849 def __run_post_scripts(self):
850 print "Running scripts"
851 for s in kickstart.get_post_scripts(self.ks):
852 (fd, path) = tempfile.mkstemp(prefix = "ks-script-",
853 dir = self._instroot + "/tmp")
855 s.script = s.script.replace("\r", "")
856 os.write(fd, s.script)
860 env = self._get_post_scripts_env(s.inChroot)
863 env["INSTALL_ROOT"] = self._instroot
864 env["IMG_NAME"] = self._name
868 preexec = self._chroot
869 script = "/tmp/" + os.path.basename(path)
873 subprocess.call([s.interp, script],
874 preexec_fn = preexec, env = env, stdout = sys.stdout, stderr = sys.stderr)
875 except OSError, (err, msg):
876 raise CreatorError("Failed to execute %%post script "
877 "with '%s' : %s" % (s.interp, msg))
881 def __save_repo_keys(self, repodata):
884 gpgkeydir = "/etc/pki/rpm-gpg"
885 makedirs(self._instroot + gpgkeydir)
886 for repo in repodata:
888 repokey = gpgkeydir + "/RPM-GPG-KEY-%s" % repo["name"]
889 shutil.copy(repo["repokey"], self._instroot + repokey)
891 def configure(self, repodata = None):
892 """Configure the system image according to the kickstart.
894 This method applies the (e.g. keyboard or network) configuration
895 specified in the kickstart and executes the kickstart %post scripts.
897 If neccessary, it also prepares the image to be bootable by e.g.
898 creating an initrd and bootloader configuration.
901 ksh = self.ks.handler
904 kickstart.LanguageConfig(self._instroot).apply(ksh.lang)
905 kickstart.KeyboardConfig(self._instroot).apply(ksh.keyboard)
906 kickstart.TimezoneConfig(self._instroot).apply(ksh.timezone)
907 #kickstart.AuthConfig(self._instroot).apply(ksh.authconfig)
908 kickstart.FirewallConfig(self._instroot).apply(ksh.firewall)
909 kickstart.RootPasswordConfig(self._instroot).apply(ksh.rootpw)
910 kickstart.UserConfig(self._instroot).apply(ksh.user)
911 kickstart.ServicesConfig(self._instroot).apply(ksh.services)
912 kickstart.XConfig(self._instroot).apply(ksh.xconfig)
913 kickstart.NetworkConfig(self._instroot).apply(ksh.network)
914 kickstart.RPMMacroConfig(self._instroot).apply(self.ks)
915 kickstart.DesktopConfig(self._instroot).apply(ksh.desktop)
916 self.__save_repo_keys(repodata)
917 kickstart.MoblinRepoConfig(self._instroot).apply(ksh.repo, repodata)
919 print "Failed to apply configuration to image"
922 self._create_bootconfig()
923 self.__run_post_scripts()
925 def launch_shell(self, launch):
926 """Launch a shell in the install root.
928 This method is launches a bash shell chroot()ed in the install root;
929 this can be useful for debugging.
933 print "Launching shell. Exit to continue."
934 print "----------------------------------"
935 subprocess.call(["/bin/bash"], preexec_fn = self._chroot)
937 def do_genchecksum(self, image_name):
938 if not self._genchecksum:
941 """ Generate md5sum if /usr/bin/md5sum is available """
942 if os.path.exists("/usr/bin/md5sum"):
943 p = subprocess.Popen(["/usr/bin/md5sum", "-b", image_name],
944 stdout=subprocess.PIPE)
945 (md5sum, errorstr) = p.communicate()
946 if p.returncode != 0:
947 logging.warning("Can't generate md5sum for image %s" % image_name)
949 pattern = re.compile("\*.*$")
950 md5sum = pattern.sub("*" + os.path.basename(image_name), md5sum)
951 fd = open(image_name + ".md5sum", "w")
954 self.outimage.append(image_name+".md5sum")
956 def package(self, destdir = "."):
957 """Prepares the created image for final delivery.
959 In its simplest form, this method merely copies the install root to the
960 supplied destination directory; other subclasses may choose to package
961 the image by e.g. creating a bootable ISO containing the image and
962 bootloader configuration.
964 destdir -- the directory into which the final image should be moved;
965 this defaults to the current directory.
968 self._stage_final_image()
970 if self.__img_compression_method:
971 if not self._img_name:
972 raise CreatorError("Image name not set.")
974 img_location = os.path.join(self._outdir,self._img_name)
975 if self.__img_compression_method == "bz2":
976 bzip2 = find_binary_path('bzip2')
977 print "Compressing %s with bzip2. Please wait..." % img_location
978 rc = subprocess.call([bzip2, "-f", img_location])
980 raise CreatorError("Failed to compress image %s with %s." % (img_location, self.__img_compression_method))
981 for bootimg in glob.glob(os.path.dirname(img_location) + "/*-boot.bin"):
982 print "Compressing %s with bzip2. Please wait..." % bootimg
983 rc = subprocess.call([bzip2, "-f", bootimg])
985 raise CreatorError("Failed to compress image %s with %s." % (bootimg, self.__img_compression_method))
987 if self._recording_pkgs:
988 self._save_recording_pkgs(destdir)
990 """ For image formats with two or multiple image files, it will be better to put them under a directory """
991 if self.image_format in ("raw", "vmdk", "vdi", "nand", "mrstnand"):
992 destdir = os.path.join(destdir, "%s-%s" % (self.name, self.image_format))
993 logging.debug("creating destination dir: %s" % destdir)
996 # Ensure all data is flushed to _outdir
997 synccmd = find_binary_path("sync")
998 subprocess.call([synccmd])
1000 for f in os.listdir(self._outdir):
1001 shutil.move(os.path.join(self._outdir, f),
1002 os.path.join(destdir, f))
1003 self.outimage.append(os.path.join(destdir, f))
1004 self.do_genchecksum(os.path.join(destdir, f))
1007 """Install, configure and package an image.
1009 This method is a utility method which creates and image by calling some
1010 of the other methods in the following order - mount(), install(),
1011 configure(), unmount and package().
1020 def print_outimage_info(self):
1021 print "Your new image can be found here:"
1022 self.outimage.sort()
1023 for file in self.outimage:
1024 print os.path.abspath(file)
1026 def check_depend_tools(self):
1027 for tool in self._dep_checks:
1028 find_binary_path(tool)
1030 def package_output(self, image_format, destdir = ".", package="none"):
1031 if not package or package == "none":
1034 destdir = os.path.abspath(os.path.expanduser(destdir))
1035 (pkg, comp) = os.path.splitext(package)
1037 comp=comp.lstrip(".")
1041 dst = "%s/%s-%s.tar.%s" % (destdir, self.name, image_format, comp)
1043 dst = "%s/%s-%s.tar" % (destdir, self.name, image_format)
1044 print "creating %s" % dst
1045 tar = tarfile.open(dst, "w:" + comp)
1047 for file in self.outimage:
1048 print "adding %s to %s" % (file, dst)
1049 tar.add(file, arcname=os.path.join("%s-%s" % (self.name, image_format), os.path.basename(file)))
1050 if os.path.isdir(file):
1051 shutil.rmtree(file, ignore_errors = True)
1058 '''All the file in outimage has been packaged into tar.* file'''
1059 self.outimage = [dst]
1061 def release_output(self, config, destdir, name, release):
1062 self.outimage = create_release(config, destdir, name, self.outimage, release)
1064 def save_kernel(self, destdir):
1065 if not os.path.exists(destdir):
1067 for kernel in glob.glob("%s/boot/vmlinuz-*" % self._instroot):
1068 kernelfilename = "%s/%s-%s" % (destdir, self.name, os.path.basename(kernel))
1069 shutil.copy(kernel, kernelfilename)
1070 self.outimage.append(kernelfilename)
1072 def compress_disk_image(self, compression_method):
1074 With this you can set the method that is used to compress the disk
1075 image after it is created.
1078 if compression_method not in ('bz2'):
1079 raise CreatorError("Given disk image compression method ('%s') is not valid." % (compression_method))
1081 self.__img_compression_method = compression_method
1083 def set_pkg_manager(self, name):
1084 self.pkgmgr.set_default_pkg_manager(name)
1086 def get_pkg_manager(self, recording_pkgs=None):
1087 pkgmgr_instance = self.pkgmgr.get_default_pkg_manager()
1088 if not pkgmgr_instance:
1089 raise CreatorError("No package manager available")
1090 return pkgmgr_instance(creator = self, recording_pkgs = recording_pkgs)
1092 class LoopImageCreator(ImageCreator):
1093 """Installs a system into a loopback-mountable filesystem image.
1095 LoopImageCreator is a straightforward ImageCreator subclass; the system
1096 is installed into an ext3 filesystem on a sparse file which can be
1097 subsequently loopback-mounted.
1101 def __init__(self, ks, name, fslabel = None):
1102 """Initialize a LoopImageCreator instance.
1104 This method takes the same arguments as ImageCreator.__init__() with
1107 fslabel -- A string used as a label for any filesystems created.
1110 ImageCreator.__init__(self, ks, name)
1112 self.__fslabel = None
1113 self.fslabel = fslabel
1115 self.__minsize_KB = 0
1116 self.__blocksize = 4096
1118 self.__fstype = kickstart.get_image_fstype(self.ks, "ext3")
1119 self.__fsopts = kickstart.get_image_fsopts(self.ks, "defaults,noatime")
1121 self.__fstype = None
1122 self.__fsopts = None
1124 self.__instloop = None
1125 self.__imgdir = None
1128 self.__image_size = kickstart.get_image_size(self.ks,
1129 4096L * 1024 * 1024)
1131 self.__image_size = 0
1133 self._img_name = self.name + ".img"
1135 def _set_fstype(self, fstype):
1136 self.__fstype = fstype
1138 def _set_image_size(self, imgsize):
1139 self.__image_size = imgsize
1144 def __get_fslabel(self):
1145 if self.__fslabel is None:
1148 return self.__fslabel
1149 def __set_fslabel(self, val):
1151 self.__fslabel = None
1153 self.__fslabel = val[:FSLABEL_MAXLEN]
1154 fslabel = property(__get_fslabel, __set_fslabel)
1155 """A string used to label any filesystems created.
1157 Some filesystems impose a constraint on the maximum allowed size of the
1158 filesystem label. In the case of ext3 it's 16 characters, but in the case
1159 of ISO9660 it's 32 characters.
1161 mke2fs silently truncates the label, but mkisofs aborts if the label is too
1162 long. So, for convenience sake, any string assigned to this attribute is
1163 silently truncated to FSLABEL_MAXLEN (32) characters.
1167 def __get_image(self):
1168 if self.__imgdir is None:
1169 raise CreatorError("_image is not valid before calling mount()")
1170 return self.__imgdir + "/meego.img"
1171 _image = property(__get_image)
1172 """The location of the image file.
1174 This is the path to the filesystem image. Subclasses may use this path
1175 in order to package the image in _stage_final_image().
1177 Note, this directory does not exist before ImageCreator.mount() is called.
1179 Note also, this is a read-only attribute.
1183 def __get_blocksize(self):
1184 return self.__blocksize
1185 def __set_blocksize(self, val):
1187 raise CreatorError("_blocksize must be set before calling mount()")
1189 self.__blocksize = int(val)
1191 raise CreatorError("'%s' is not a valid integer value "
1192 "for _blocksize" % val)
1193 _blocksize = property(__get_blocksize, __set_blocksize)
1194 """The block size used by the image's filesystem.
1196 This is the block size used when creating the filesystem image. Subclasses
1197 may change this if they wish to use something other than a 4k block size.
1199 Note, this attribute may only be set before calling mount().
1203 def __get_fstype(self):
1204 return self.__fstype
1205 def __set_fstype(self, val):
1206 if val != "ext2" and val != "ext3":
1207 raise CreatorError("Unknown _fstype '%s' supplied" % val)
1209 _fstype = property(__get_fstype, __set_fstype)
1210 """The type of filesystem used for the image.
1212 This is the filesystem type used when creating the filesystem image.
1213 Subclasses may change this if they wish to use something other ext3.
1215 Note, only ext2 and ext3 are currently supported.
1217 Note also, this attribute may only be set before calling mount().
1221 def __get_fsopts(self):
1222 return self.__fsopts
1223 def __set_fsopts(self, val):
1225 _fsopts = property(__get_fsopts, __set_fsopts)
1226 """Mount options of filesystem used for the image.
1228 This can be specified by --fsoptions="xxx,yyy" in part command in
1233 # Helpers for subclasses
1235 def _resparse(self, size = None):
1236 """Rebuild the filesystem image to be as sparse as possible.
1238 This method should be used by subclasses when staging the final image
1239 in order to reduce the actual space taken up by the sparse image file
1240 to be as little as possible.
1242 This is done by resizing the filesystem to the minimal size (thereby
1243 eliminating any space taken up by deleted files) and then resizing it
1244 back to the supplied size.
1246 size -- the size in, in bytes, which the filesystem image should be
1247 resized to after it has been minimized; this defaults to None,
1248 causing the original size specified by the kickstart file to
1249 be used (or 4GiB if not specified in the kickstart).
1252 return self.__instloop.resparse(size)
1254 def _base_on(self, base_on):
1255 shutil.copyfile(base_on, self._image)
1258 # Actual implementation
1260 def _mount_instroot(self, base_on = None):
1261 self.__imgdir = self._mkdtemp()
1263 if not base_on is None:
1264 self._base_on(base_on)
1266 if self.__fstype in ("ext2", "ext3", "ext4"):
1267 MyDiskMount = ExtDiskMount
1268 elif self.__fstype == "btrfs":
1269 MyDiskMount = BtrfsDiskMount
1271 self.__instloop = MyDiskMount(SparseLoopbackDisk(self._image, self.__image_size),
1278 self.__instloop.mount()
1279 except MountError, e:
1280 raise CreatorError("Failed to loopback mount '%s' : %s" %
1283 def _unmount_instroot(self):
1284 if not self.__instloop is None:
1285 self.__instloop.cleanup()
1287 def _stage_final_image(self):
1289 shutil.move(self._image, self._outdir + "/" + self._img_name)
1291 class LiveImageCreatorBase(LoopImageCreator):
1292 """A base class for LiveCD image creators.
1294 This class serves as a base class for the architecture-specific LiveCD
1295 image creator subclass, LiveImageCreator.
1297 LiveImageCreator creates a bootable ISO containing the system image,
1298 bootloader, bootloader configuration, kernel and initramfs.
1302 def __init__(self, *args):
1303 """Initialise a LiveImageCreator instance.
1305 This method takes the same arguments as ImageCreator.__init__().
1308 LoopImageCreator.__init__(self, *args)
1310 self.skip_compression = False
1311 """Controls whether to use squashfs to compress the image."""
1313 self.skip_minimize = False
1314 """Controls whether an image minimizing snapshot should be created.
1316 This snapshot can be used when copying the system image from the ISO in
1317 order to minimize the amount of data that needs to be copied; simply,
1318 it makes it possible to create a version of the image's filesystem with
1323 self.actasconvertor = False
1324 """A flag which indicates i act as a convertor"""
1327 self._timeout = kickstart.get_timeout(self.ks, 10)
1330 """The bootloader timeout from kickstart."""
1333 self._default_kernel = kickstart.get_default_kernel(self.ks, "kernel")
1335 self._default_kernel = None
1336 """The default kernel type from kickstart."""
1338 self.__isodir = None
1340 self.__modules = ["=ata", "sym53c8xx", "aic7xxx", "=usb", "=firewire", "=mmc", "=pcmcia", "mptsas"]
1342 self.__modules.extend(kickstart.get_modules(self.ks))
1344 self._dep_checks.extend(["isohybrid", "unsquashfs", "mksquashfs", "dd", "genisoimage"])
1347 # Hooks for subclasses
1349 def _configure_bootloader(self, isodir):
1350 """Create the architecture specific booloader configuration.
1352 This is the hook where subclasses must create the booloader
1353 configuration in order to allow a bootable ISO to be built.
1355 isodir -- the directory where the contents of the ISO are to be staged
1358 raise CreatorError("Bootloader configuration is arch-specific, "
1359 "but not implemented for this arch!")
1360 def _get_menu_options(self):
1361 """Return a menu options string for syslinux configuration.
1364 r = kickstart.get_menu_args(self.ks)
1367 def _get_kernel_options(self):
1368 """Return a kernel options string for bootloader configuration.
1370 This is the hook where subclasses may specify a set of kernel options
1371 which should be included in the images bootloader configuration.
1373 A sensible default implementation is provided.
1376 r = kickstart.get_kernel_args(self.ks)
1377 if os.path.exists(self._instroot + "/usr/bin/rhgb") or \
1378 os.path.exists(self._instroot + "/usr/bin/plymouth"):
1382 def _get_mkisofs_options(self, isodir):
1383 """Return the architecture specific mkisosfs options.
1385 This is the hook where subclasses may specify additional arguments to
1386 mkisofs, e.g. to enable a bootable ISO to be built.
1388 By default, an empty list is returned.
1394 # Helpers for subclasses
1396 def _has_checkisomd5(self):
1397 """Check whether checkisomd5 is available in the install root."""
1398 def exists(instroot, path):
1399 return os.path.exists(instroot + path)
1401 if (exists(self._instroot, "/usr/lib/moblin-installer-runtime/checkisomd5") or
1402 exists(self._instroot, "/usr/bin/checkisomd5")):
1403 if (os.path.exists("/usr/bin/implantisomd5") or
1404 os.path.exists("/usr/lib/anaconda-runtime/implantisomd5")):
1409 def _uncompress_squashfs(self, squashfsimg, outdir):
1410 """Uncompress file system from squshfs image"""
1411 unsquashfs = find_binary_path("unsquashfs")
1412 args = [unsquashfs, "-d", outdir, squashfsimg ]
1413 rc = subprocess.call(args)
1415 raise CreatorError("Failed to uncompress %s." % squashfsimg)
1417 # Actual implementation
1419 def _base_on(self, base_on):
1420 """Support Image Convertor"""
1421 if self.actasconvertor:
1422 if os.path.exists(base_on) and not os.path.isfile(base_on):
1423 ddcmd = find_binary_path("dd")
1424 args = [ ddcmd, "if=%s" % base_on, "of=%s" % self._image ]
1425 print "dd %s -> %s" % (base_on, self._image)
1426 rc = subprocess.call(args)
1428 raise CreatorError("Failed to dd from %s to %s" % (base_on, self._image))
1429 self._set_image_size(get_file_size(self._image) * 1024L * 1024L)
1430 if os.path.isfile(base_on):
1431 print "Copying file system..."
1432 shutil.copyfile(base_on, self._image)
1433 self._set_image_size(get_file_size(self._image) * 1024L * 1024L)
1436 """helper function to extract ext3 file system from a live CD ISO"""
1437 isoloop = DiskMount(LoopbackDisk(base_on, 0), self._mkdtemp())
1441 except MountError, e:
1442 raise CreatorError("Failed to loopback mount '%s' : %s" %
1445 # legacy LiveOS filesystem layout support, remove for F9 or F10
1446 if os.path.exists(isoloop.mountdir + "/squashfs.img"):
1447 squashimg = isoloop.mountdir + "/squashfs.img"
1449 squashimg = isoloop.mountdir + "/LiveOS/squashfs.img"
1451 tmpoutdir = self._mkdtemp()
1452 # unsquashfs requires outdir mustn't exist
1453 shutil.rmtree(tmpoutdir, ignore_errors = True)
1454 self._uncompress_squashfs(squashimg, tmpoutdir)
1457 # legacy LiveOS filesystem layout support, remove for F9 or F10
1458 if os.path.exists(tmpoutdir + "/os.img"):
1459 os_image = tmpoutdir + "/os.img"
1461 os_image = tmpoutdir + "/LiveOS/ext3fs.img"
1463 if not os.path.exists(os_image):
1464 raise CreatorError("'%s' is not a valid live CD ISO : neither "
1465 "LiveOS/ext3fs.img nor os.img exist" %
1468 print "Copying file system..."
1469 shutil.copyfile(os_image, self._image)
1470 self._set_image_size(get_file_size(self._image) * 1024L * 1024L)
1472 shutil.rmtree(tmpoutdir, ignore_errors = True)
1475 def _mount_instroot(self, base_on = None):
1476 LoopImageCreator._mount_instroot(self, base_on)
1477 self.__write_initrd_conf(self._instroot + "/etc/sysconfig/mkinitrd")
1479 def _unmount_instroot(self):
1481 os.unlink(self._instroot + "/etc/sysconfig/mkinitrd")
1484 LoopImageCreator._unmount_instroot(self)
1486 def __ensure_isodir(self):
1487 if self.__isodir is None:
1488 self.__isodir = self._mkdtemp("iso-")
1489 return self.__isodir
1491 def _get_isodir(self):
1492 return self.__ensure_isodir()
1494 def _set_isodir(self, isodir = None):
1495 self.__isodir = isodir
1497 def _create_bootconfig(self):
1498 """Configure the image so that it's bootable."""
1499 self._configure_bootloader(self.__ensure_isodir())
1501 def _get_post_scripts_env(self, in_chroot):
1502 env = LoopImageCreator._get_post_scripts_env(self, in_chroot)
1505 env["LIVE_ROOT"] = self.__ensure_isodir()
1509 def __write_initrd_conf(self, path):
1511 if not os.path.exists(os.path.dirname(path)):
1512 makedirs(os.path.dirname(path))
1515 content += 'LIVEOS="yes"\n'
1516 content += 'PROBE="no"\n'
1517 content += 'MODULES+="squashfs ext3 ext2 vfat msdos "\n'
1518 content += 'MODULES+="sr_mod sd_mod ide-cd cdrom "\n'
1520 for module in self.__modules:
1521 if module == "=usb":
1522 content += 'MODULES+="ehci_hcd uhci_hcd ohci_hcd "\n'
1523 content += 'MODULES+="usb_storage usbhid "\n'
1524 elif module == "=firewire":
1525 content += 'MODULES+="firewire-sbp2 firewire-ohci "\n'
1526 content += 'MODULES+="sbp2 ohci1394 ieee1394 "\n'
1527 elif module == "=mmc":
1528 content += 'MODULES+="mmc_block sdhci sdhci-pci "\n'
1529 elif module == "=pcmcia":
1530 content += 'MODULES+="pata_pcmcia "\n'
1532 content += 'MODULES+="' + module + ' "\n'
1536 def __create_iso(self, isodir):
1537 iso = self._outdir + "/" + self.name + ".iso"
1538 genisoimage = find_binary_path("genisoimage")
1539 args = [genisoimage,
1541 "-hide-rr-moved", "-hide-joliet-trans-tbl",
1545 args.extend(self._get_mkisofs_options(isodir))
1549 if subprocess.call(args) != 0:
1550 raise CreatorError("ISO creation failed!")
1552 """ It should be ok still even if you haven't isohybrid """
1555 isohybrid = find_binary_path("isohybrid")
1560 args = [isohybrid, "-partok", iso ]
1561 if subprocess.call(args) != 0:
1562 raise CreatorError("Hybrid ISO creation failed!")
1564 self.__implant_md5sum(iso)
1566 def __implant_md5sum(self, iso):
1567 """Implant an isomd5sum."""
1568 if os.path.exists("/usr/bin/implantisomd5"):
1569 implantisomd5 = "/usr/bin/implantisomd5"
1570 elif os.path.exists("/usr/lib/anaconda-runtime/implantisomd5"):
1571 implantisomd5 = "/usr/lib/anaconda-runtime/implantisomd5"
1573 logging.warn("isomd5sum not installed; not setting up mediacheck")
1577 subprocess.call([implantisomd5, iso], stdout=sys.stdout, stderr=sys.stderr)
1579 def _stage_final_image(self):
1581 makedirs(self.__ensure_isodir() + "/LiveOS")
1583 minimal_size = self._resparse()
1585 if not self.skip_minimize:
1586 create_image_minimizer(self.__isodir + "/LiveOS/osmin.img",
1587 self._image, minimal_size)
1589 if self.skip_compression:
1590 shutil.move(self._image, self.__isodir + "/LiveOS/ext3fs.img")
1592 makedirs(os.path.join(os.path.dirname(self._image), "LiveOS"))
1593 shutil.move(self._image,
1594 os.path.join(os.path.dirname(self._image),
1595 "LiveOS", "ext3fs.img"))
1596 mksquashfs(os.path.dirname(self._image),
1597 self.__isodir + "/LiveOS/squashfs.img")
1599 self.__create_iso(self.__isodir)
1601 shutil.rmtree(self.__isodir, ignore_errors = True)
1602 self.__isodir = None