Add support for timeout and totaltimeout to livecd-iso-to-disk (#531566)
[livecd.git] / imgcreate / creator.py
blob20be1485f437a56c86ef6e0adb1bd9497719d763
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.
19 import os
20 import os.path
21 import stat
22 import sys
23 import tempfile
24 import shutil
25 import logging
26 import subprocess
28 import selinux
29 import yum
30 import rpm
32 from imgcreate.errors import *
33 from imgcreate.fs import *
34 from imgcreate.yuminst import *
35 from imgcreate import kickstart
37 FSLABEL_MAXLEN = 32
38 """The maximum string length supported for LoopImageCreator.fslabel."""
40 class ImageCreator(object):
41 """Installs a system to a chroot directory.
43 ImageCreator is the simplest creator class available; it will install and
44 configure a system image according to the supplied kickstart file.
46 e.g.
48 import imgcreate
49 ks = imgcreate.read_kickstart("foo.ks")
50 imgcreate.ImageCreator(ks, "foo").create()
52 """
54 def __init__(self, ks, name):
55 """Initialize an ImageCreator instance.
57 ks -- a pykickstart.KickstartParser instance; this instance will be
58 used to drive the install by e.g. providing the list of packages
59 to be installed, the system configuration and %post scripts
61 name -- a name for the image; used for e.g. image filenames or
62 filesystem labels
64 """
65 self.ks = ks
66 """A pykickstart.KickstartParser instance."""
68 self.name = name
69 """A name for the image."""
71 self.tmpdir = "/var/tmp"
72 """The directory in which all temporary files will be created."""
74 self.__builddir = None
75 self.__bindmounts = []
77 self.__sanity_check()
79 def __del__(self):
80 self.cleanup()
83 # Properties
85 def __get_instroot(self):
86 if self.__builddir is None:
87 raise CreatorError("_instroot is not valid before calling mount()")
88 return self.__builddir + "/install_root"
89 _instroot = property(__get_instroot)
90 """The location of the install root directory.
92 This is the directory into which the system is installed. Subclasses may
93 mount a filesystem image here or copy files to/from here.
95 Note, this directory does not exist before ImageCreator.mount() is called.
97 Note also, this is a read-only attribute.
99 """
101 def __get_outdir(self):
102 if self.__builddir is None:
103 raise CreatorError("_outdir is not valid before calling mount()")
104 return self.__builddir + "/out"
105 _outdir = property(__get_outdir)
106 """The staging location for the final image.
108 This is where subclasses should stage any files that are part of the final
109 image. ImageCreator.package() will copy any files found here into the
110 requested destination directory.
112 Note, this directory does not exist before ImageCreator.mount() is called.
114 Note also, this is a read-only attribute.
119 # Hooks for subclasses
121 def _mount_instroot(self, base_on = None):
122 """Mount or prepare the install root directory.
124 This is the hook where subclasses may prepare the install root by e.g.
125 mounting creating and loopback mounting a filesystem image to
126 _instroot.
128 There is no default implementation.
130 base_on -- this is the value passed to mount() and can be interpreted
131 as the subclass wishes; it might e.g. be the location of
132 a previously created ISO containing a system image.
135 pass
137 def _unmount_instroot(self):
138 """Undo anything performed in _mount_instroot().
140 This is the hook where subclasses must undo anything which was done
141 in _mount_instroot(). For example, if a filesystem image was mounted
142 onto _instroot, it should be unmounted here.
144 There is no default implementation.
147 pass
149 def _create_bootconfig(self):
150 """Configure the image so that it's bootable.
152 This is the hook where subclasses may prepare the image for booting by
153 e.g. creating an initramfs and bootloader configuration.
155 This hook is called while the install root is still mounted, after the
156 packages have been installed and the kickstart configuration has been
157 applied, but before the %post scripts have been executed.
159 There is no default implementation.
162 pass
164 def _stage_final_image(self):
165 """Stage the final system image in _outdir.
167 This is the hook where subclasses should place the image in _outdir
168 so that package() can copy it to the requested destination directory.
170 By default, this moves the install root into _outdir.
173 shutil.move(self._instroot, self._outdir + "/" + self.name)
175 def _get_required_packages(self):
176 """Return a list of required packages.
178 This is the hook where subclasses may specify a set of packages which
179 it requires to be installed.
181 This returns an empty list by default.
183 Note, subclasses should usually chain up to the base class
184 implementation of this hook.
187 return []
189 def _get_excluded_packages(self):
190 """Return a list of excluded packages.
192 This is the hook where subclasses may specify a set of packages which
193 it requires _not_ to be installed.
195 This returns an empty list by default.
197 Note, subclasses should usually chain up to the base class
198 implementation of this hook.
201 return []
203 def _get_fstab(self):
204 """Return the desired contents of /etc/fstab.
206 This is the hook where subclasses may specify the contents of
207 /etc/fstab by returning a string containing the desired contents.
209 A sensible default implementation is provided.
212 s = "/dev/root / %s defaults,noatime 0 0\n" %(self._fstype)
213 s += self._get_fstab_special()
214 return s
216 def _get_fstab_special(self):
217 s = "devpts /dev/pts devpts gid=5,mode=620 0 0\n"
218 s += "tmpfs /dev/shm tmpfs defaults 0 0\n"
219 s += "proc /proc proc defaults 0 0\n"
220 s += "sysfs /sys sysfs defaults 0 0\n"
221 return s
223 def _get_post_scripts_env(self, in_chroot):
224 """Return an environment dict for %post scripts.
226 This is the hook where subclasses may specify some environment
227 variables for %post scripts by return a dict containing the desired
228 environment.
230 By default, this returns an empty dict.
232 in_chroot -- whether this %post script is to be executed chroot()ed
233 into _instroot.
236 return {}
238 def _get_kernel_versions(self):
239 """Return a dict detailing the available kernel types/versions.
241 This is the hook where subclasses may override what kernel types and
242 versions should be available for e.g. creating the booloader
243 configuration.
245 A dict should be returned mapping the available kernel types to a list
246 of the available versions for those kernels.
248 The default implementation uses rpm to iterate over everything
249 providing 'kernel', finds /boot/vmlinuz-* and returns the version
250 obtained from the vmlinuz filename. (This can differ from the kernel
251 RPM's n-v-r in the case of e.g. xen)
254 def get_version(header):
255 version = None
256 for f in header['filenames']:
257 if f.startswith('/boot/vmlinuz-'):
258 version = f[14:]
259 return version
261 ts = rpm.TransactionSet(self._instroot)
263 ret = {}
264 for header in ts.dbMatch('provides', 'kernel'):
265 version = get_version(header)
266 if version is None:
267 continue
269 name = header['name']
270 if not name in ret:
271 ret[name] = [version]
272 elif not version in ret[name]:
273 ret[name].append(version)
275 return ret
278 # Helpers for subclasses
280 def _do_bindmounts(self):
281 """Mount various system directories onto _instroot.
283 This method is called by mount(), but may also be used by subclasses
284 in order to re-mount the bindmounts after modifying the underlying
285 filesystem.
288 for b in self.__bindmounts:
289 b.mount()
291 def _undo_bindmounts(self):
292 """Unmount the bind-mounted system directories from _instroot.
294 This method is usually only called by unmount(), but may also be used
295 by subclasses in order to gain access to the filesystem obscured by
296 the bindmounts - e.g. in order to create device nodes on the image
297 filesystem.
300 self.__bindmounts.reverse()
301 for b in self.__bindmounts:
302 b.unmount()
304 def _chroot(self):
305 """Chroot into the install root.
307 This method may be used by subclasses when executing programs inside
308 the install root e.g.
310 subprocess.call(["/bin/ls"], preexec_fn = self.chroot)
313 os.chroot(self._instroot)
314 os.chdir("/")
316 def _mkdtemp(self, prefix = "tmp-"):
317 """Create a temporary directory.
319 This method may be used by subclasses to create a temporary directory
320 for use in building the final image - e.g. a subclass might create
321 a temporary directory in order to bundle a set of files into a package.
323 The subclass may delete this directory if it wishes, but it will be
324 automatically deleted by cleanup().
326 The absolute path to the temporary directory is returned.
328 Note, this method should only be called after mount() has been called.
330 prefix -- a prefix which should be used when creating the directory;
331 defaults to "tmp-".
334 self.__ensure_builddir()
335 return tempfile.mkdtemp(dir = self.__builddir, prefix = prefix)
337 def _mkstemp(self, prefix = "tmp-"):
338 """Create a temporary file.
340 This method may be used by subclasses to create a temporary file
341 for use in building the final image - e.g. a subclass might need
342 a temporary location to unpack a compressed file.
344 The subclass may delete this file if it wishes, but it will be
345 automatically deleted by cleanup().
347 A tuple containing a file descriptor (returned from os.open() and the
348 absolute path to the temporary directory is returned.
350 Note, this method should only be called after mount() has been called.
352 prefix -- a prefix which should be used when creating the file;
353 defaults to "tmp-".
356 self.__ensure_builddir()
357 return tempfile.mkstemp(dir = self.__builddir, prefix = prefix)
359 def _mktemp(self, prefix = "tmp-"):
360 """Create a temporary file.
362 This method simply calls _mkstemp() and closes the returned file
363 descriptor.
365 The absolute path to the temporary file is returned.
367 Note, this method should only be called after mount() has been called.
369 prefix -- a prefix which should be used when creating the file;
370 defaults to "tmp-".
374 (f, path) = self._mkstemp(prefix)
375 os.close(f)
376 return path
379 # Actual implementation
381 def __ensure_builddir(self):
382 if not self.__builddir is None:
383 return
385 try:
386 self.__builddir = tempfile.mkdtemp(dir = os.path.abspath(self.tmpdir),
387 prefix = "imgcreate-")
388 except OSError, e:
389 raise CreatorError("Failed create build directory in %s: %s" %
390 (self.tmpdir, e.strerror))
392 def __sanity_check(self):
393 """Ensure that the config we've been given is sane."""
394 if not (kickstart.get_packages(self.ks) or
395 kickstart.get_groups(self.ks)):
396 raise CreatorError("No packages or groups specified")
398 kickstart.convert_method_to_repo(self.ks)
400 if not kickstart.get_repos(self.ks):
401 raise CreatorError("No repositories specified")
403 def __write_fstab(self):
404 fstab = open(self._instroot + "/etc/fstab", "w")
405 fstab.write(self._get_fstab())
406 fstab.close()
408 def __create_minimal_dev(self):
409 """Create a minimal /dev so that we don't corrupt the host /dev"""
410 origumask = os.umask(0000)
411 devices = (('null', 1, 3, 0666),
412 ('urandom',1, 9, 0666),
413 ('random', 1, 8, 0666),
414 ('full', 1, 7, 0666),
415 ('ptmx', 5, 2, 0666),
416 ('tty', 5, 0, 0666),
417 ('zero', 1, 5, 0666))
418 links = (("/proc/self/fd", "/dev/fd"),
419 ("/proc/self/fd/0", "/dev/stdin"),
420 ("/proc/self/fd/1", "/dev/stdout"),
421 ("/proc/self/fd/2", "/dev/stderr"))
423 for (node, major, minor, perm) in devices:
424 if not os.path.exists(self._instroot + "/dev/" + node):
425 os.mknod(self._instroot + "/dev/" + node, perm | stat.S_IFCHR, os.makedev(major,minor))
426 for (src, dest) in links:
427 if not os.path.exists(self._instroot + dest):
428 os.symlink(src, self._instroot + dest)
429 os.umask(origumask)
431 def __getbooleans(self):
432 booleans = []
433 if not kickstart.selinux_enabled(self.ks) or not os.path.exists("/selinux/enforce"):
434 return booleans
435 for i in selinux.security_get_boolean_names()[1]:
436 on = selinux.security_get_boolean_active(i)
437 booleans.append(("/booleans/%s" % i, "%d %d" % (on, on)))
438 return booleans
440 def __create_selinuxfs(self):
441 # if selinux exists on the host we need to lie to the chroot
442 if os.path.exists("/selinux/enforce"):
443 selinux_dir = self._instroot + "/selinux"
445 # enforce=0 tells the chroot selinux is not enforcing
446 # policyvers=999 tell the chroot to make the highest version of policy it can
448 files = [('/enforce', '0'),
449 ('/policyvers', '999'),
450 ('/commit_pending_bools', ''),
451 ('/mls', str(selinux.is_selinux_mls_enabled()))]
453 for (file, value) in files + self.__getbooleans():
454 fd = os.open(selinux_dir + file, os.O_WRONLY | os.O_TRUNC | os.O_CREAT)
455 os.write(fd, value)
456 os.close(fd)
458 # we steal mls from the host system for now, might be best to always set it to 1????
459 # make /load -> /dev/null so chroot policy loads don't hurt anything
460 os.mknod(selinux_dir + "/load", 0666 | stat.S_IFCHR, os.makedev(1, 3))
462 # selinux is on in the kickstart, so clean up as best we can to start
463 if kickstart.selinux_enabled(self.ks):
464 # label the fs like it is a root before the bind mounting
465 arglist = ["/sbin/setfiles", "-F", "-r", self._instroot, selinux.selinux_file_context_path(), self._instroot]
466 subprocess.call(arglist, close_fds = True)
467 # these dumb things don't get magically fixed, so make the user generic
468 for f in ("/proc", "/sys", "/selinux"):
469 arglist = ["/usr/bin/chcon", "-u", "system_u", self._instroot + f]
470 subprocess.call(arglist, close_fds = True)
472 def __destroy_selinuxfs(self):
473 # if the system was running selinux clean up our lies
474 if os.path.exists("/selinux/enforce"):
475 for root, dirs, files in os.walk(self._instroot + "/selinux"):
476 for name in files:
477 try:
478 os.remove(os.path.join(root, name))
479 except OSError:
480 pass
481 for name in dirs:
482 if os.path.join(root, name) == self._instroot + "/selinux":
483 continue
484 try:
485 os.rmdir(os.path.join(root, name))
486 except OSError:
487 pass
489 def mount(self, base_on = None, cachedir = None):
490 """Setup the target filesystem in preparation for an install.
492 This function sets up the filesystem which the ImageCreator will
493 install into and configure. The ImageCreator class merely creates an
494 install root directory, bind mounts some system directories (e.g. /dev)
495 and writes out /etc/fstab. Other subclasses may also e.g. create a
496 sparse file, format it and loopback mount it to the install root.
498 base_on -- a previous install on which to base this install; defaults
499 to None, causing a new image to be created
501 cachedir -- a directory in which to store the Yum cache; defaults to
502 None, causing a new cache to be created; by setting this
503 to another directory, the same cache can be reused across
504 multiple installs.
507 self.__ensure_builddir()
509 makedirs(self._instroot)
510 makedirs(self._outdir)
512 self._mount_instroot(base_on)
514 for d in ("/dev/pts", "/etc", "/boot", "/var/log", "/var/cache/yum", "/sys", "/proc", "/selinux/booleans"):
515 makedirs(self._instroot + d)
517 cachesrc = cachedir or (self.__builddir + "/yum-cache")
518 makedirs(cachesrc)
520 # bind mount system directories into _instroot
521 for (f, dest) in [("/sys", None), ("/proc", None),
522 ("/dev/pts", None), ("/dev/shm", None),
523 (cachesrc, "/var/cache/yum")]:
524 self.__bindmounts.append(BindChrootMount(f, self._instroot, dest))
526 self.__create_selinuxfs()
528 self._do_bindmounts()
530 self.__create_minimal_dev()
532 os.symlink("../proc/mounts", self._instroot + "/etc/mtab")
534 self.__write_fstab()
536 def unmount(self):
537 """Unmounts the target filesystem.
539 The ImageCreator class detaches the system from the install root, but
540 other subclasses may also detach the loopback mounted filesystem image
541 from the install root.
544 try:
545 os.unlink(self._instroot + "/etc/mtab")
546 except OSError:
547 pass
549 self.__destroy_selinuxfs()
551 self._undo_bindmounts()
553 self._unmount_instroot()
555 def cleanup(self):
556 """Unmounts the target filesystem and deletes temporary files.
558 This method calls unmount() and then deletes any temporary files and
559 directories that were created on the host system while building the
560 image.
562 Note, make sure to call this method once finished with the creator
563 instance in order to ensure no stale files are left on the host e.g.:
565 creator = ImageCreator(ks, name)
566 try:
567 creator.create()
568 finally:
569 creator.cleanup()
572 if not self.__builddir:
573 return
575 self.unmount()
577 shutil.rmtree(self.__builddir, ignore_errors = True)
578 self.__builddir = None
580 def __select_packages(self, ayum):
581 skipped_pkgs = []
582 for pkg in kickstart.get_packages(self.ks,
583 self._get_required_packages()):
584 try:
585 ayum.selectPackage(pkg)
586 except yum.Errors.InstallError, e:
587 if kickstart.ignore_missing(self.ks):
588 skipped_pkgs.append(pkg)
589 else:
590 raise CreatorError("Failed to find package '%s' : %s" %
591 (pkg, e))
593 for pkg in skipped_pkgs:
594 logging.warn("Skipping missing package '%s'" % (pkg,))
596 def __select_groups(self, ayum):
597 skipped_groups = []
598 for group in kickstart.get_groups(self.ks):
599 try:
600 ayum.selectGroup(group.name, group.include)
601 except (yum.Errors.InstallError, yum.Errors.GroupsError), e:
602 if kickstart.ignore_missing(self.ks):
603 raise CreatorError("Failed to find group '%s' : %s" %
604 (group.name, e))
605 else:
606 skipped_groups.append(group)
608 for group in skipped_groups:
609 logging.warn("Skipping missing group '%s'" % (group.name,))
611 def __deselect_packages(self, ayum):
612 for pkg in kickstart.get_excluded(self.ks,
613 self._get_excluded_packages()):
614 ayum.deselectPackage(pkg)
616 # if the system is running selinux and the kickstart wants it disabled
617 # we need /usr/sbin/lokkit
618 def __can_handle_selinux(self, ayum):
619 file = "/usr/sbin/lokkit"
620 if not kickstart.selinux_enabled(self.ks) and os.path.exists("/selinux/enforce") and not ayum.installHasFile(file):
621 raise CreatorError("Unable to disable SELinux because the installed package set did not include the file %s" % (file))
623 def install(self, repo_urls = {}):
624 """Install packages into the install root.
626 This function installs the packages listed in the supplied kickstart
627 into the install root. By default, the packages are installed from the
628 repository URLs specified in the kickstart.
630 repo_urls -- a dict which maps a repository name to a repository URL;
631 if supplied, this causes any repository URLs specified in
632 the kickstart to be overridden.
635 yum_conf = self._mktemp(prefix = "yum.conf-")
637 ayum = LiveCDYum()
638 ayum.setup(yum_conf, self._instroot)
640 for repo in kickstart.get_repos(self.ks, repo_urls):
641 (name, baseurl, mirrorlist, inc, exc) = repo
643 yr = ayum.addRepository(name, baseurl, mirrorlist)
644 if inc:
645 yr.includepkgs = inc
646 if exc:
647 yr.exclude = exc
649 if kickstart.exclude_docs(self.ks):
650 rpm.addMacro("_excludedocs", "1")
651 if not kickstart.selinux_enabled(self.ks):
652 rpm.addMacro("__file_context_path", "%{nil}")
653 if kickstart.inst_langs(self.ks) != None:
654 rpm.addMacro("_install_langs", kickstart.inst_langs(self.ks))
656 try:
657 self.__select_packages(ayum)
658 self.__select_groups(ayum)
659 self.__deselect_packages(ayum)
661 self.__can_handle_selinux(ayum)
663 ayum.runInstall()
664 except yum.Errors.RepoError, e:
665 raise CreatorError("Unable to download from repo : %s" % (e,))
666 except yum.Errors.YumBaseError, e:
667 raise CreatorError("Unable to install: %s" % (e,))
668 finally:
669 ayum.closeRpmDB()
670 ayum.close()
671 os.unlink(yum_conf)
673 # do some clean up to avoid lvm info leakage. this sucks.
674 for subdir in ("cache", "backup", "archive"):
675 lvmdir = self._instroot + "/etc/lvm/" + subdir
676 try:
677 for f in os.listdir(lvmdir):
678 os.unlink(lvmdir + "/" + f)
679 except:
680 pass
682 def __run_post_scripts(self):
683 for s in kickstart.get_post_scripts(self.ks):
684 (fd, path) = tempfile.mkstemp(prefix = "ks-script-",
685 dir = self._instroot + "/tmp")
687 os.write(fd, s.script)
688 os.close(fd)
689 os.chmod(path, 0700)
691 env = self._get_post_scripts_env(s.inChroot)
693 if not s.inChroot:
694 env["INSTALL_ROOT"] = self._instroot
695 preexec = None
696 script = path
697 else:
698 preexec = self._chroot
699 script = "/tmp/" + os.path.basename(path)
701 try:
702 subprocess.check_call([s.interp, script],
703 preexec_fn = preexec, env = env)
704 except OSError, e:
705 raise CreatorError("Failed to execute %%post script "
706 "with '%s' : %s" % (s.interp, e.strerror))
707 except subprocess.CalledProcessError, err:
708 if s.errorOnFail:
709 raise CreatorError("%%post script failed with code %d "
710 % err.returncode)
711 logging.warning("ignoring %%post failure (code %d)"
712 % err.returncode)
713 finally:
714 os.unlink(path)
716 def configure(self):
717 """Configure the system image according to the kickstart.
719 This method applies the (e.g. keyboard or network) configuration
720 specified in the kickstart and executes the kickstart %post scripts.
722 If neccessary, it also prepares the image to be bootable by e.g.
723 creating an initrd and bootloader configuration.
726 ksh = self.ks.handler
728 kickstart.LanguageConfig(self._instroot).apply(ksh.lang)
729 kickstart.KeyboardConfig(self._instroot).apply(ksh.keyboard)
730 kickstart.TimezoneConfig(self._instroot).apply(ksh.timezone)
731 kickstart.AuthConfig(self._instroot).apply(ksh.authconfig)
732 kickstart.SelinuxConfig(self._instroot).apply(ksh.selinux)
733 kickstart.FirewallConfig(self._instroot).apply(ksh.firewall)
734 kickstart.RootPasswordConfig(self._instroot).apply(ksh.rootpw)
735 kickstart.ServicesConfig(self._instroot).apply(ksh.services)
736 kickstart.XConfig(self._instroot).apply(ksh.xconfig)
737 kickstart.NetworkConfig(self._instroot).apply(ksh.network)
738 kickstart.RPMMacroConfig(self._instroot).apply(self.ks)
740 self._create_bootconfig()
742 self.__run_post_scripts()
744 def launch_shell(self):
745 """Launch a shell in the install root.
747 This method is launches a bash shell chroot()ed in the install root;
748 this can be useful for debugging.
751 subprocess.call(["/bin/bash"], preexec_fn = self._chroot)
753 def package(self, destdir = "."):
754 """Prepares the created image for final delivery.
756 In its simplest form, this method merely copies the install root to the
757 supplied destination directory; other subclasses may choose to package
758 the image by e.g. creating a bootable ISO containing the image and
759 bootloader configuration.
761 destdir -- the directory into which the final image should be moved;
762 this defaults to the current directory.
765 self._stage_final_image()
767 for f in os.listdir(self._outdir):
768 shutil.move(os.path.join(self._outdir, f),
769 os.path.join(destdir, f))
771 def create(self):
772 """Install, configure and package an image.
774 This method is a utility method which creates and image by calling some
775 of the other methods in the following order - mount(), install(),
776 configure(), unmount and package().
779 self.mount()
780 self.install()
781 self.configure()
782 self.unmount()
783 self.package()
785 class LoopImageCreator(ImageCreator):
786 """Installs a system into a loopback-mountable filesystem image.
788 LoopImageCreator is a straightforward ImageCreator subclass; the system
789 is installed into an ext3 filesystem on a sparse file which can be
790 subsequently loopback-mounted.
794 def __init__(self, ks, name, fslabel = None):
795 """Initialize a LoopImageCreator instance.
797 This method takes the same arguments as ImageCreator.__init__() with
798 the addition of:
800 fslabel -- A string used as a label for any filesystems created.
803 ImageCreator.__init__(self, ks, name)
805 self.__fslabel = None
806 self.fslabel = fslabel
808 self.__minsize_KB = 0
809 self.__blocksize = 4096
810 self.__fstype = kickstart.get_image_fstype(self.ks, "ext3")
812 self.__instloop = None
813 self.__imgdir = None
815 self.__image_size = kickstart.get_image_size(self.ks,
816 4096L * 1024 * 1024)
819 # Properties
821 def __get_fslabel(self):
822 if self.__fslabel is None:
823 return self.name
824 else:
825 return self.__fslabel
826 def __set_fslabel(self, val):
827 if val is None:
828 self.__fslabel = None
829 else:
830 self.__fslabel = val[:FSLABEL_MAXLEN]
831 fslabel = property(__get_fslabel, __set_fslabel)
832 """A string used to label any filesystems created.
834 Some filesystems impose a constraint on the maximum allowed size of the
835 filesystem label. In the case of ext3 it's 16 characters, but in the case
836 of ISO9660 it's 32 characters.
838 mke2fs silently truncates the label, but mkisofs aborts if the label is too
839 long. So, for convenience sake, any string assigned to this attribute is
840 silently truncated to FSLABEL_MAXLEN (32) characters.
844 def __get_image(self):
845 if self.__imgdir is None:
846 raise CreatorError("_image is not valid before calling mount()")
847 return self.__imgdir + "/ext3fs.img"
848 _image = property(__get_image)
849 """The location of the image file.
851 This is the path to the filesystem image. Subclasses may use this path
852 in order to package the image in _stage_final_image().
854 Note, this directory does not exist before ImageCreator.mount() is called.
856 Note also, this is a read-only attribute.
860 def __get_blocksize(self):
861 return self.__blocksize
862 def __set_blocksize(self, val):
863 if self.__instloop:
864 raise CreatorError("_blocksize must be set before calling mount()")
865 try:
866 self.__blocksize = int(val)
867 except ValueError:
868 raise CreatorError("'%s' is not a valid integer value "
869 "for _blocksize" % val)
870 _blocksize = property(__get_blocksize, __set_blocksize)
871 """The block size used by the image's filesystem.
873 This is the block size used when creating the filesystem image. Subclasses
874 may change this if they wish to use something other than a 4k block size.
876 Note, this attribute may only be set before calling mount().
880 def __get_fstype(self):
881 return self.__fstype
882 def __set_fstype(self, val):
883 if val not in ("ext2", "ext3", "ext4"):
884 raise CreatorError("Unknown _fstype '%s' supplied" % val)
885 self.__fstype = val
886 _fstype = property(__get_fstype, __set_fstype)
887 """The type of filesystem used for the image.
889 This is the filesystem type used when creating the filesystem image.
890 Subclasses may change this if they wish to use something other ext3.
892 Note, only ext2, ext3, ext4 are currently supported.
894 Note also, this attribute may only be set before calling mount().
899 # Helpers for subclasses
901 def _resparse(self, size = None):
902 """Rebuild the filesystem image to be as sparse as possible.
904 This method should be used by subclasses when staging the final image
905 in order to reduce the actual space taken up by the sparse image file
906 to be as little as possible.
908 This is done by resizing the filesystem to the minimal size (thereby
909 eliminating any space taken up by deleted files) and then resizing it
910 back to the supplied size.
912 size -- the size in, in bytes, which the filesystem image should be
913 resized to after it has been minimized; this defaults to None,
914 causing the original size specified by the kickstart file to
915 be used (or 4GiB if not specified in the kickstart).
918 return self.__instloop.resparse(size)
920 def _base_on(self, base_on):
921 shutil.copyfile(base_on, self._image)
924 # Actual implementation
926 def _mount_instroot(self, base_on = None):
927 self.__imgdir = self._mkdtemp()
929 if not base_on is None:
930 self._base_on(base_on)
932 self.__instloop = ExtDiskMount(SparseLoopbackDisk(self._image, self.__image_size),
933 self._instroot,
934 self.__fstype,
935 self.__blocksize,
936 self.fslabel)
938 try:
939 self.__instloop.mount()
940 except MountError, e:
941 raise CreatorError("Failed to loopback mount '%s' : %s" %
942 (self._image, e))
944 def _unmount_instroot(self):
945 if not self.__instloop is None:
946 self.__instloop.cleanup()
948 def _stage_final_image(self):
949 self._resparse()
950 shutil.move(self._image, self._outdir + "/" + self.name + ".img")