Add missing quote (#1044675)
[livecd.git] / imgcreate / creator.py
blob42faf6f7827037b1d321b0e9166949a766f24cfa
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, releasever=None, tmpdir="/tmp", useplugins=False,
55 cacheonly=False, docleanup=True):
56 """Initialize an ImageCreator instance.
58 ks -- a pykickstart.KickstartParser instance; this instance will be
59 used to drive the install by e.g. providing the list of packages
60 to be installed, the system configuration and %post scripts
62 name -- a name for the image; used for e.g. image filenames or
63 filesystem labels
65 releasever -- Value to substitute for $releasever in repo urls
67 tmpdir -- Top level directory to use for temporary files and dirs
69 cacheonly -- Only read from cache, work offline
70 """
71 self.ks = ks
72 """A pykickstart.KickstartParser instance."""
74 self.name = name
75 """A name for the image."""
77 self.releasever = releasever
78 self.useplugins = useplugins
80 self.tmpdir = tmpdir
81 """The directory in which all temporary files will be created."""
82 if not os.path.exists(self.tmpdir):
83 makedirs(self.tmpdir)
85 self.cacheonly = cacheonly
86 self.docleanup = docleanup
88 self.__builddir = None
89 self.__bindmounts = []
91 self.__sanity_check()
93 # get selinuxfs mountpoint
94 self.__selinux_mountpoint = "/sys/fs/selinux"
95 with open("/proc/self/mountinfo", "r") as f:
96 for line in f.readlines():
97 fields = line.split()
98 if fields[-2] == "selinuxfs":
99 self.__selinux_mountpoint = fields[4]
100 break
102 def __del__(self):
103 self.cleanup()
106 # Properties
108 def __get_instroot(self):
109 if self.__builddir is None:
110 raise CreatorError("_instroot is not valid before calling mount()")
111 return self.__builddir + "/install_root"
112 _instroot = property(__get_instroot)
113 """The location of the install root directory.
115 This is the directory into which the system is installed. Subclasses may
116 mount a filesystem image here or copy files to/from here.
118 Note, this directory does not exist before ImageCreator.mount() is called.
120 Note also, this is a read-only attribute.
124 def __get_outdir(self):
125 if self.__builddir is None:
126 raise CreatorError("_outdir is not valid before calling mount()")
127 return self.__builddir + "/out"
128 _outdir = property(__get_outdir)
129 """The staging location for the final image.
131 This is where subclasses should stage any files that are part of the final
132 image. ImageCreator.package() will copy any files found here into the
133 requested destination directory.
135 Note, this directory does not exist before ImageCreator.mount() is called.
137 Note also, this is a read-only attribute.
142 # Hooks for subclasses
144 def _mount_instroot(self, base_on = None):
145 """Mount or prepare the install root directory.
147 This is the hook where subclasses may prepare the install root by e.g.
148 mounting creating and loopback mounting a filesystem image to
149 _instroot.
151 There is no default implementation.
153 base_on -- this is the value passed to mount() and can be interpreted
154 as the subclass wishes; it might e.g. be the location of
155 a previously created ISO containing a system image.
158 pass
160 def _unmount_instroot(self):
161 """Undo anything performed in _mount_instroot().
163 This is the hook where subclasses must undo anything which was done
164 in _mount_instroot(). For example, if a filesystem image was mounted
165 onto _instroot, it should be unmounted here.
167 There is no default implementation.
170 pass
172 def _create_bootconfig(self):
173 """Configure the image so that it's bootable.
175 This is the hook where subclasses may prepare the image for booting by
176 e.g. creating an initramfs and bootloader configuration.
178 This hook is called while the install root is still mounted, after the
179 packages have been installed and the kickstart configuration has been
180 applied, but before the %post scripts have been executed.
182 There is no default implementation.
185 pass
187 def _stage_final_image(self):
188 """Stage the final system image in _outdir.
190 This is the hook where subclasses should place the image in _outdir
191 so that package() can copy it to the requested destination directory.
193 By default, this moves the install root into _outdir.
196 shutil.move(self._instroot, self._outdir + "/" + self.name)
198 def _get_required_packages(self):
199 """Return a list of required packages.
201 This is the hook where subclasses may specify a set of packages which
202 it requires to be installed.
204 This returns an empty list by default.
206 Note, subclasses should usually chain up to the base class
207 implementation of this hook.
210 return []
212 def _get_excluded_packages(self):
213 """Return a list of excluded packages.
215 This is the hook where subclasses may specify a set of packages which
216 it requires _not_ to be installed.
218 This returns an empty list by default.
220 Note, subclasses should usually chain up to the base class
221 implementation of this hook.
224 return []
226 def _get_fstab(self):
227 """Return the desired contents of /etc/fstab.
229 This is the hook where subclasses may specify the contents of
230 /etc/fstab by returning a string containing the desired contents.
232 A sensible default implementation is provided.
235 s = "/dev/root / %s defaults,noatime 0 0\n" %(self._fstype)
236 s += self._get_fstab_special()
237 return s
239 def _get_fstab_special(self):
240 s = "devpts /dev/pts devpts gid=5,mode=620 0 0\n"
241 s += "tmpfs /dev/shm tmpfs defaults 0 0\n"
242 s += "proc /proc proc defaults 0 0\n"
243 s += "sysfs /sys sysfs defaults 0 0\n"
244 return s
246 def _get_post_scripts_env(self, in_chroot):
247 """Return an environment dict for %post scripts.
249 This is the hook where subclasses may specify some environment
250 variables for %post scripts by return a dict containing the desired
251 environment.
253 By default, this returns an empty dict.
255 in_chroot -- whether this %post script is to be executed chroot()ed
256 into _instroot.
259 return {}
261 def _get_kernel_versions(self):
262 """Return a dict detailing the available kernel types/versions.
264 This is the hook where subclasses may override what kernel types and
265 versions should be available for e.g. creating the booloader
266 configuration.
268 A dict should be returned mapping the available kernel types to a list
269 of the available versions for those kernels.
271 The default implementation uses rpm to iterate over everything
272 providing 'kernel', finds /boot/vmlinuz-* and returns the version
273 obtained from the vmlinuz filename. (This can differ from the kernel
274 RPM's n-v-r in the case of e.g. xen)
277 def get_version(header):
278 version = None
279 for f in header['filenames']:
280 if f.startswith('/boot/vmlinuz-'):
281 version = f[14:]
282 return version
284 ts = rpm.TransactionSet(self._instroot)
286 ret = {}
287 for header in ts.dbMatch('provides', 'kernel'):
288 version = get_version(header)
289 if version is None:
290 continue
292 name = header['name']
293 if not name in ret:
294 ret[name] = [version]
295 elif not version in ret[name]:
296 ret[name].append(version)
298 return ret
301 # Helpers for subclasses
303 def _do_bindmounts(self):
304 """Mount various system directories onto _instroot.
306 This method is called by mount(), but may also be used by subclasses
307 in order to re-mount the bindmounts after modifying the underlying
308 filesystem.
311 for b in self.__bindmounts:
312 b.mount()
314 def _undo_bindmounts(self):
315 """Unmount the bind-mounted system directories from _instroot.
317 This method is usually only called by unmount(), but may also be used
318 by subclasses in order to gain access to the filesystem obscured by
319 the bindmounts - e.g. in order to create device nodes on the image
320 filesystem.
323 self.__bindmounts.reverse()
324 for b in self.__bindmounts:
325 b.unmount()
327 def _chroot(self):
328 """Chroot into the install root.
330 This method may be used by subclasses when executing programs inside
331 the install root e.g.
333 subprocess.call(["/bin/ls"], preexec_fn = self.chroot)
336 os.chroot(self._instroot)
337 os.chdir("/")
339 def _mkdtemp(self, prefix = "tmp-"):
340 """Create a temporary directory.
342 This method may be used by subclasses to create a temporary directory
343 for use in building the final image - e.g. a subclass might create
344 a temporary directory in order to bundle a set of files into a package.
346 The subclass may delete this directory if it wishes, but it will be
347 automatically deleted by cleanup().
349 The absolute path to the temporary directory is returned.
351 Note, this method should only be called after mount() has been called.
353 prefix -- a prefix which should be used when creating the directory;
354 defaults to "tmp-".
357 self.__ensure_builddir()
358 return tempfile.mkdtemp(dir = self.__builddir, prefix = prefix)
360 def _mkstemp(self, prefix = "tmp-"):
361 """Create a temporary file.
363 This method may be used by subclasses to create a temporary file
364 for use in building the final image - e.g. a subclass might need
365 a temporary location to unpack a compressed file.
367 The subclass may delete this file if it wishes, but it will be
368 automatically deleted by cleanup().
370 A tuple containing a file descriptor (returned from os.open() and the
371 absolute path to the temporary directory is returned.
373 Note, this method should only be called after mount() has been called.
375 prefix -- a prefix which should be used when creating the file;
376 defaults to "tmp-".
379 self.__ensure_builddir()
380 return tempfile.mkstemp(dir = self.__builddir, prefix = prefix)
382 def _mktemp(self, prefix = "tmp-"):
383 """Create a temporary file.
385 This method simply calls _mkstemp() and closes the returned file
386 descriptor.
388 The absolute path to the temporary file is returned.
390 Note, this method should only be called after mount() has been called.
392 prefix -- a prefix which should be used when creating the file;
393 defaults to "tmp-".
397 (f, path) = self._mkstemp(prefix)
398 os.close(f)
399 return path
402 # Actual implementation
404 def __ensure_builddir(self):
405 if not self.__builddir is None:
406 return
408 try:
409 self.__builddir = tempfile.mkdtemp(dir = os.path.abspath(self.tmpdir),
410 prefix = "imgcreate-")
411 except OSError, e:
412 raise CreatorError("Failed create build directory in %s: %s" %
413 (self.tmpdir, e.strerror))
415 def __sanity_check(self):
416 """Ensure that the config we've been given is sane."""
417 if not (kickstart.get_packages(self.ks) or
418 kickstart.get_groups(self.ks)):
419 raise CreatorError("No packages or groups specified")
421 kickstart.convert_method_to_repo(self.ks)
423 if not kickstart.get_repos(self.ks):
424 raise CreatorError("No repositories specified")
426 def __write_fstab(self):
427 fstab = open(self._instroot + "/etc/fstab", "w")
428 fstab.write(self._get_fstab())
429 fstab.close()
431 def __create_minimal_dev(self):
432 """Create a minimal /dev so that we don't corrupt the host /dev"""
433 origumask = os.umask(0000)
434 devices = (('null', 1, 3, 0666),
435 ('urandom',1, 9, 0666),
436 ('random', 1, 8, 0666),
437 ('full', 1, 7, 0666),
438 ('ptmx', 5, 2, 0666),
439 ('tty', 5, 0, 0666),
440 ('zero', 1, 5, 0666))
441 links = (("/proc/self/fd", "/dev/fd"),
442 ("/proc/self/fd/0", "/dev/stdin"),
443 ("/proc/self/fd/1", "/dev/stdout"),
444 ("/proc/self/fd/2", "/dev/stderr"))
446 for (node, major, minor, perm) in devices:
447 if not os.path.exists(self._instroot + "/dev/" + node):
448 os.mknod(self._instroot + "/dev/" + node, perm | stat.S_IFCHR, os.makedev(major,minor))
449 for (src, dest) in links:
450 if not os.path.exists(self._instroot + dest):
451 os.symlink(src, self._instroot + dest)
452 os.umask(origumask)
454 def __create_selinuxfs(self):
455 if not os.path.exists(self.__selinux_mountpoint):
456 return
458 arglist = ["/bin/mount", "--bind", "/dev/null", self._instroot + self.__selinux_mountpoint + "/load"]
459 subprocess.call(arglist, close_fds = True)
461 if kickstart.selinux_enabled(self.ks):
462 # label the fs like it is a root before the bind mounting
463 arglist = ["/sbin/setfiles", "-F", "-r", self._instroot, selinux.selinux_file_context_path(), self._instroot]
464 subprocess.call(arglist, close_fds = True)
465 # these dumb things don't get magically fixed, so make the user generic
466 # if selinux exists on the host we need to lie to the chroot
467 if selinux.is_selinux_enabled():
468 for f in ("/proc", "/sys"):
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 not os.path.exists(self.__selinux_mountpoint):
474 return
476 # if the system was running selinux clean up our lies
477 path = self._instroot + self.__selinux_mountpoint + "/load"
478 if os.path.exists(path):
479 arglist = ["/bin/umount", path]
480 subprocess.call(arglist, close_fds = True)
482 def mount(self, base_on = None, cachedir = None):
483 """Setup the target filesystem in preparation for an install.
485 This function sets up the filesystem which the ImageCreator will
486 install into and configure. The ImageCreator class merely creates an
487 install root directory, bind mounts some system directories (e.g. /dev)
488 and writes out /etc/fstab. Other subclasses may also e.g. create a
489 sparse file, format it and loopback mount it to the install root.
491 base_on -- a previous install on which to base this install; defaults
492 to None, causing a new image to be created
494 cachedir -- a directory in which to store the Yum cache; defaults to
495 None, causing a new cache to be created; by setting this
496 to another directory, the same cache can be reused across
497 multiple installs.
500 self.__ensure_builddir()
502 makedirs(self._instroot)
503 makedirs(self._outdir)
505 self._mount_instroot(base_on)
507 for d in ("/dev/pts", "/etc", "/boot", "/var/log", "/var/cache/yum", "/sys", "/proc"):
508 makedirs(self._instroot + d)
510 cachesrc = cachedir or (self.__builddir + "/yum-cache")
511 makedirs(cachesrc)
513 # bind mount system directories into _instroot
514 for (f, dest) in [("/sys", None), ("/proc", None),
515 ("/dev/pts", None), ("/dev/shm", None),
516 (self.__selinux_mountpoint, self.__selinux_mountpoint),
517 (cachesrc, "/var/cache/yum")]:
518 if os.path.exists(f):
519 self.__bindmounts.append(BindChrootMount(f, self._instroot, dest))
520 else:
521 logging.warn("Skipping (%s,%s) because source doesn't exist." % (f, dest))
523 self._do_bindmounts()
525 self.__create_selinuxfs()
527 self.__create_minimal_dev()
529 os.symlink("/proc/self/mounts", self._instroot + "/etc/mtab")
531 self.__write_fstab()
533 def unmount(self):
534 """Unmounts the target filesystem.
536 The ImageCreator class detaches the system from the install root, but
537 other subclasses may also detach the loopback mounted filesystem image
538 from the install root.
541 self.__destroy_selinuxfs()
543 self._undo_bindmounts()
545 self._unmount_instroot()
547 def cleanup(self):
548 """Unmounts the target filesystem and deletes temporary files.
550 This method calls unmount() and then deletes any temporary files and
551 directories that were created on the host system while building the
552 image.
554 Note, make sure to call this method once finished with the creator
555 instance in order to ensure no stale files are left on the host e.g.:
557 creator = ImageCreator(ks, name)
558 try:
559 creator.create()
560 finally:
561 creator.cleanup()
564 if not self.docleanup:
565 logging.warn("Skipping cleanup of temporary files")
566 return
568 if not self.__builddir:
569 return
571 self.unmount()
573 shutil.rmtree(self.__builddir, ignore_errors = True)
574 self.__builddir = None
576 def __select_packages(self, ayum):
577 skipped_pkgs = []
578 for pkg in kickstart.get_packages(self.ks,
579 self._get_required_packages()):
580 try:
581 ayum.selectPackage(pkg)
582 except yum.Errors.InstallError, e:
583 if kickstart.ignore_missing(self.ks):
584 skipped_pkgs.append(pkg)
585 else:
586 raise CreatorError("Failed to find package '%s' : %s" %
587 (pkg, e))
589 for pkg in skipped_pkgs:
590 logging.warn("Skipping missing package '%s'" % (pkg,))
592 def __select_groups(self, ayum):
593 skipped_groups = []
594 for group in kickstart.get_groups(self.ks):
595 try:
596 ayum.selectGroup(group.name, group.include)
597 except (yum.Errors.InstallError, yum.Errors.GroupsError), e:
598 if kickstart.ignore_missing(self.ks):
599 raise CreatorError("Failed to find group '%s' : %s" %
600 (group.name, e))
601 else:
602 skipped_groups.append(group)
604 for group in skipped_groups:
605 logging.warn("Skipping missing group '%s'" % (group.name,))
607 def __deselect_packages(self, ayum):
608 for pkg in kickstart.get_excluded(self.ks,
609 self._get_excluded_packages()):
610 ayum.deselectPackage(pkg)
612 def install(self, repo_urls = {}):
613 """Install packages into the install root.
615 This function installs the packages listed in the supplied kickstart
616 into the install root. By default, the packages are installed from the
617 repository URLs specified in the kickstart.
619 repo_urls -- a dict which maps a repository name to a repository URL;
620 if supplied, this causes any repository URLs specified in
621 the kickstart to be overridden.
624 yum_conf = self._mktemp(prefix = "yum.conf-")
626 ayum = LiveCDYum(releasever=self.releasever, useplugins=self.useplugins)
627 ayum.setup(yum_conf, self._instroot, cacheonly=self.cacheonly)
629 for repo in kickstart.get_repos(self.ks, repo_urls):
630 (name, baseurl, mirrorlist, proxy, inc, exc, cost, sslverify) = repo
632 yr = ayum.addRepository(name, baseurl, mirrorlist)
633 if inc:
634 yr.includepkgs = inc
635 if exc:
636 yr.exclude = exc
637 if proxy:
638 yr.proxy = proxy
639 if cost is not None:
640 yr.cost = cost
641 yr.sslverify = sslverify
642 ayum.setup(yum_conf, self._instroot)
644 if kickstart.exclude_docs(self.ks):
645 rpm.addMacro("_excludedocs", "1")
646 if not kickstart.selinux_enabled(self.ks):
647 rpm.addMacro("__file_context_path", "%{nil}")
648 if kickstart.inst_langs(self.ks) != None:
649 rpm.addMacro("_install_langs", kickstart.inst_langs(self.ks))
651 try:
652 self.__select_packages(ayum)
653 self.__select_groups(ayum)
654 self.__deselect_packages(ayum)
656 ayum.runInstall()
657 except yum.Errors.RepoError, e:
658 raise CreatorError("Unable to download from repo : %s" % (e,))
659 except yum.Errors.YumBaseError, e:
660 raise CreatorError("Unable to install: %s" % (e,))
661 finally:
662 ayum.closeRpmDB()
663 ayum.close()
664 os.unlink(yum_conf)
666 # do some clean up to avoid lvm info leakage. this sucks.
667 for subdir in ("cache", "backup", "archive"):
668 lvmdir = self._instroot + "/etc/lvm/" + subdir
669 try:
670 for f in os.listdir(lvmdir):
671 os.unlink(lvmdir + "/" + f)
672 except:
673 pass
675 def _run_post_scripts(self):
676 for s in kickstart.get_post_scripts(self.ks):
677 (fd, path) = tempfile.mkstemp(prefix = "ks-script-",
678 dir = self._instroot + "/tmp")
680 os.write(fd, s.script)
681 os.close(fd)
682 os.chmod(path, 0700)
684 env = self._get_post_scripts_env(s.inChroot)
686 if not s.inChroot:
687 env["INSTALL_ROOT"] = self._instroot
688 preexec = None
689 script = path
690 else:
691 preexec = self._chroot
692 script = "/tmp/" + os.path.basename(path)
694 try:
695 subprocess.check_call([s.interp, script],
696 preexec_fn = preexec, env = env)
697 except OSError, e:
698 raise CreatorError("Failed to execute %%post script "
699 "with '%s' : %s" % (s.interp, e.strerror))
700 except subprocess.CalledProcessError, err:
701 if s.errorOnFail:
702 raise CreatorError("%%post script failed with code %d "
703 % err.returncode)
704 logging.warning("ignoring %%post failure (code %d)"
705 % err.returncode)
706 finally:
707 os.unlink(path)
709 def configure(self):
710 """Configure the system image according to the kickstart.
712 This method applies the (e.g. keyboard or network) configuration
713 specified in the kickstart and executes the kickstart %post scripts.
715 If neccessary, it also prepares the image to be bootable by e.g.
716 creating an initrd and bootloader configuration.
719 ksh = self.ks.handler
721 kickstart.LanguageConfig(self._instroot).apply(ksh.lang)
722 kickstart.KeyboardConfig(self._instroot).apply(ksh.keyboard)
723 kickstart.TimezoneConfig(self._instroot).apply(ksh.timezone)
724 kickstart.AuthConfig(self._instroot).apply(ksh.authconfig)
725 kickstart.FirewallConfig(self._instroot).apply(ksh.firewall)
726 kickstart.RootPasswordConfig(self._instroot).apply(ksh.rootpw)
727 kickstart.ServicesConfig(self._instroot).apply(ksh.services)
728 kickstart.XConfig(self._instroot).apply(ksh.xconfig)
729 kickstart.NetworkConfig(self._instroot).apply(ksh.network)
730 kickstart.RPMMacroConfig(self._instroot).apply(self.ks)
732 self._create_bootconfig()
734 self._run_post_scripts()
735 kickstart.SelinuxConfig(self._instroot).apply(ksh.selinux)
737 def launch_shell(self):
738 """Launch a shell in the install root.
740 This method is launches a bash shell chroot()ed in the install root;
741 this can be useful for debugging.
744 subprocess.call(["/bin/bash"], preexec_fn = self._chroot)
746 def package(self, destdir = "."):
747 """Prepares the created image for final delivery.
749 In its simplest form, this method merely copies the install root to the
750 supplied destination directory; other subclasses may choose to package
751 the image by e.g. creating a bootable ISO containing the image and
752 bootloader configuration.
754 destdir -- the directory into which the final image should be moved;
755 this defaults to the current directory.
758 self._stage_final_image()
760 for f in os.listdir(self._outdir):
761 shutil.move(os.path.join(self._outdir, f),
762 os.path.join(destdir, f))
764 def create(self):
765 """Install, configure and package an image.
767 This method is a utility method which creates and image by calling some
768 of the other methods in the following order - mount(), install(),
769 configure(), unmount and package().
772 self.mount()
773 self.install()
774 self.configure()
775 self.unmount()
776 self.package()
778 class LoopImageCreator(ImageCreator):
779 """Installs a system into a loopback-mountable filesystem image.
781 LoopImageCreator is a straightforward ImageCreator subclass; the system
782 is installed into an ext3 filesystem on a sparse file which can be
783 subsequently loopback-mounted.
787 def __init__(self, ks, name, fslabel=None, releasever=None, tmpdir="/tmp",
788 useplugins=False, cacheonly=False, docleanup=True):
789 """Initialize a LoopImageCreator instance.
791 This method takes the same arguments as ImageCreator.__init__() with
792 the addition of:
794 fslabel -- A string used as a label for any filesystems created.
797 ImageCreator.__init__(self, ks, name, releasever=releasever, tmpdir=tmpdir,
798 useplugins=useplugins, cacheonly=cacheonly, docleanup=docleanup)
800 self.__fslabel = None
801 self.fslabel = fslabel
803 self.__minsize_KB = 0
804 self.__blocksize = 4096
805 self.__fstype = kickstart.get_image_fstype(self.ks, "ext3")
807 self.__instloop = None
808 self.__imgdir = None
810 self.__image_size = kickstart.get_image_size(self.ks,
811 4096L * 1024 * 1024)
814 # Properties
816 def __get_fslabel(self):
817 if self.__fslabel is None:
818 return self.name
819 else:
820 return self.__fslabel
821 def __set_fslabel(self, val):
822 if val is None:
823 self.__fslabel = None
824 else:
825 self.__fslabel = val[:FSLABEL_MAXLEN]
826 fslabel = property(__get_fslabel, __set_fslabel)
827 """A string used to label any filesystems created.
829 Some filesystems impose a constraint on the maximum allowed size of the
830 filesystem label. In the case of ext3 it's 16 characters, but in the case
831 of ISO9660 it's 32 characters.
833 mke2fs silently truncates the label, but mkisofs aborts if the label is too
834 long. So, for convenience sake, any string assigned to this attribute is
835 silently truncated to FSLABEL_MAXLEN (32) characters.
839 def __get_image(self):
840 if self.__imgdir is None:
841 raise CreatorError("_image is not valid before calling mount()")
842 return self.__imgdir + "/ext3fs.img"
843 _image = property(__get_image)
844 """The location of the image file.
846 This is the path to the filesystem image. Subclasses may use this path
847 in order to package the image in _stage_final_image().
849 Note, this directory does not exist before ImageCreator.mount() is called.
851 Note also, this is a read-only attribute.
855 def __get_blocksize(self):
856 return self.__blocksize
857 def __set_blocksize(self, val):
858 if self.__instloop:
859 raise CreatorError("_blocksize must be set before calling mount()")
860 try:
861 self.__blocksize = int(val)
862 except ValueError:
863 raise CreatorError("'%s' is not a valid integer value "
864 "for _blocksize" % val)
865 _blocksize = property(__get_blocksize, __set_blocksize)
866 """The block size used by the image's filesystem.
868 This is the block size used when creating the filesystem image. Subclasses
869 may change this if they wish to use something other than a 4k block size.
871 Note, this attribute may only be set before calling mount().
875 def __get_fstype(self):
876 return self.__fstype
877 def __set_fstype(self, val):
878 if val not in ("ext2", "ext3", "ext4"):
879 raise CreatorError("Unknown _fstype '%s' supplied" % val)
880 self.__fstype = val
881 _fstype = property(__get_fstype, __set_fstype)
882 """The type of filesystem used for the image.
884 This is the filesystem type used when creating the filesystem image.
885 Subclasses may change this if they wish to use something other ext3.
887 Note, only ext2, ext3, ext4 are currently supported.
889 Note also, this attribute may only be set before calling mount().
894 # Helpers for subclasses
896 def _resparse(self, size = None):
897 """Rebuild the filesystem image to be as sparse as possible.
899 This method should be used by subclasses when staging the final image
900 in order to reduce the actual space taken up by the sparse image file
901 to be as little as possible.
903 This is done by resizing the filesystem to the minimal size (thereby
904 eliminating any space taken up by deleted files) and then resizing it
905 back to the supplied size.
907 size -- the size in, in bytes, which the filesystem image should be
908 resized to after it has been minimized; this defaults to None,
909 causing the original size specified by the kickstart file to
910 be used (or 4GiB if not specified in the kickstart).
913 return self.__instloop.resparse(size)
915 def _base_on(self, base_on):
916 shutil.copyfile(base_on, self._image)
919 # Actual implementation
921 def _mount_instroot(self, base_on = None):
922 self.__imgdir = self._mkdtemp()
924 if not base_on is None:
925 self._base_on(base_on)
927 self.__instloop = ExtDiskMount(SparseLoopbackDisk(self._image,
928 self.__image_size),
929 self._instroot,
930 self.__fstype,
931 self.__blocksize,
932 self.fslabel,
933 self.tmpdir)
935 try:
936 self.__instloop.mount()
937 except MountError, e:
938 raise CreatorError("Failed to loopback mount '%s' : %s" %
939 (self._image, e))
941 def _unmount_instroot(self):
942 if not self.__instloop is None:
943 self.__instloop.cleanup()
945 def _stage_final_image(self):
946 self._resparse()
947 shutil.move(self._image, self._outdir + "/" + self.name + ".img")