Version 18.3
[livecd.git] / imgcreate / creator.py
blob35fe77767911dee618c99d7a37d9a18fc190699e
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"):
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 releasever -- Value to substitute for $releasever in repo urls
66 tmpdir -- Top level directory to use for temporary files and dirs
67 """
68 self.ks = ks
69 """A pykickstart.KickstartParser instance."""
71 self.name = name
72 """A name for the image."""
74 self.releasever = releasever
76 self.tmpdir = tmpdir
77 """The directory in which all temporary files will be created."""
78 if not os.path.exists(self.tmpdir):
79 makedirs(self.tmpdir)
81 self.__builddir = None
82 self.__bindmounts = []
84 self.__sanity_check()
86 # get selinuxfs mountpoint
87 self.__selinux_mountpoint = "/sys/fs/selinux"
88 with open("/proc/self/mountinfo", "r") as f:
89 for line in f.readlines():
90 fields = line.split()
91 if fields[-2] == "selinuxfs":
92 self.__selinux_mountpoint = fields[4]
93 break
95 def __del__(self):
96 self.cleanup()
99 # Properties
101 def __get_instroot(self):
102 if self.__builddir is None:
103 raise CreatorError("_instroot is not valid before calling mount()")
104 return self.__builddir + "/install_root"
105 _instroot = property(__get_instroot)
106 """The location of the install root directory.
108 This is the directory into which the system is installed. Subclasses may
109 mount a filesystem image here or copy files to/from here.
111 Note, this directory does not exist before ImageCreator.mount() is called.
113 Note also, this is a read-only attribute.
117 def __get_outdir(self):
118 if self.__builddir is None:
119 raise CreatorError("_outdir is not valid before calling mount()")
120 return self.__builddir + "/out"
121 _outdir = property(__get_outdir)
122 """The staging location for the final image.
124 This is where subclasses should stage any files that are part of the final
125 image. ImageCreator.package() will copy any files found here into the
126 requested destination directory.
128 Note, this directory does not exist before ImageCreator.mount() is called.
130 Note also, this is a read-only attribute.
135 # Hooks for subclasses
137 def _mount_instroot(self, base_on = None):
138 """Mount or prepare the install root directory.
140 This is the hook where subclasses may prepare the install root by e.g.
141 mounting creating and loopback mounting a filesystem image to
142 _instroot.
144 There is no default implementation.
146 base_on -- this is the value passed to mount() and can be interpreted
147 as the subclass wishes; it might e.g. be the location of
148 a previously created ISO containing a system image.
151 pass
153 def _unmount_instroot(self):
154 """Undo anything performed in _mount_instroot().
156 This is the hook where subclasses must undo anything which was done
157 in _mount_instroot(). For example, if a filesystem image was mounted
158 onto _instroot, it should be unmounted here.
160 There is no default implementation.
163 pass
165 def _create_bootconfig(self):
166 """Configure the image so that it's bootable.
168 This is the hook where subclasses may prepare the image for booting by
169 e.g. creating an initramfs and bootloader configuration.
171 This hook is called while the install root is still mounted, after the
172 packages have been installed and the kickstart configuration has been
173 applied, but before the %post scripts have been executed.
175 There is no default implementation.
178 pass
180 def _stage_final_image(self):
181 """Stage the final system image in _outdir.
183 This is the hook where subclasses should place the image in _outdir
184 so that package() can copy it to the requested destination directory.
186 By default, this moves the install root into _outdir.
189 shutil.move(self._instroot, self._outdir + "/" + self.name)
191 def _get_required_packages(self):
192 """Return a list of required packages.
194 This is the hook where subclasses may specify a set of packages which
195 it requires to be installed.
197 This returns an empty list by default.
199 Note, subclasses should usually chain up to the base class
200 implementation of this hook.
203 return []
205 def _get_excluded_packages(self):
206 """Return a list of excluded packages.
208 This is the hook where subclasses may specify a set of packages which
209 it requires _not_ to be installed.
211 This returns an empty list by default.
213 Note, subclasses should usually chain up to the base class
214 implementation of this hook.
217 return []
219 def _get_fstab(self):
220 """Return the desired contents of /etc/fstab.
222 This is the hook where subclasses may specify the contents of
223 /etc/fstab by returning a string containing the desired contents.
225 A sensible default implementation is provided.
228 s = "/dev/root / %s defaults,noatime 0 0\n" %(self._fstype)
229 s += self._get_fstab_special()
230 return s
232 def _get_fstab_special(self):
233 s = "devpts /dev/pts devpts gid=5,mode=620 0 0\n"
234 s += "tmpfs /dev/shm tmpfs defaults 0 0\n"
235 s += "proc /proc proc defaults 0 0\n"
236 s += "sysfs /sys sysfs defaults 0 0\n"
237 return s
239 def _get_post_scripts_env(self, in_chroot):
240 """Return an environment dict for %post scripts.
242 This is the hook where subclasses may specify some environment
243 variables for %post scripts by return a dict containing the desired
244 environment.
246 By default, this returns an empty dict.
248 in_chroot -- whether this %post script is to be executed chroot()ed
249 into _instroot.
252 return {}
254 def _get_kernel_versions(self):
255 """Return a dict detailing the available kernel types/versions.
257 This is the hook where subclasses may override what kernel types and
258 versions should be available for e.g. creating the booloader
259 configuration.
261 A dict should be returned mapping the available kernel types to a list
262 of the available versions for those kernels.
264 The default implementation uses rpm to iterate over everything
265 providing 'kernel', finds /boot/vmlinuz-* and returns the version
266 obtained from the vmlinuz filename. (This can differ from the kernel
267 RPM's n-v-r in the case of e.g. xen)
270 def get_version(header):
271 version = None
272 for f in header['filenames']:
273 if f.startswith('/boot/vmlinuz-'):
274 version = f[14:]
275 return version
277 ts = rpm.TransactionSet(self._instroot)
279 ret = {}
280 for header in ts.dbMatch('provides', 'kernel'):
281 version = get_version(header)
282 if version is None:
283 continue
285 name = header['name']
286 if not name in ret:
287 ret[name] = [version]
288 elif not version in ret[name]:
289 ret[name].append(version)
291 return ret
294 # Helpers for subclasses
296 def _do_bindmounts(self):
297 """Mount various system directories onto _instroot.
299 This method is called by mount(), but may also be used by subclasses
300 in order to re-mount the bindmounts after modifying the underlying
301 filesystem.
304 for b in self.__bindmounts:
305 b.mount()
307 def _undo_bindmounts(self):
308 """Unmount the bind-mounted system directories from _instroot.
310 This method is usually only called by unmount(), but may also be used
311 by subclasses in order to gain access to the filesystem obscured by
312 the bindmounts - e.g. in order to create device nodes on the image
313 filesystem.
316 self.__bindmounts.reverse()
317 for b in self.__bindmounts:
318 b.unmount()
320 def _chroot(self):
321 """Chroot into the install root.
323 This method may be used by subclasses when executing programs inside
324 the install root e.g.
326 subprocess.call(["/bin/ls"], preexec_fn = self.chroot)
329 os.chroot(self._instroot)
330 os.chdir("/")
332 def _mkdtemp(self, prefix = "tmp-"):
333 """Create a temporary directory.
335 This method may be used by subclasses to create a temporary directory
336 for use in building the final image - e.g. a subclass might create
337 a temporary directory in order to bundle a set of files into a package.
339 The subclass may delete this directory if it wishes, but it will be
340 automatically deleted by cleanup().
342 The absolute path to the temporary directory is returned.
344 Note, this method should only be called after mount() has been called.
346 prefix -- a prefix which should be used when creating the directory;
347 defaults to "tmp-".
350 self.__ensure_builddir()
351 return tempfile.mkdtemp(dir = self.__builddir, prefix = prefix)
353 def _mkstemp(self, prefix = "tmp-"):
354 """Create a temporary file.
356 This method may be used by subclasses to create a temporary file
357 for use in building the final image - e.g. a subclass might need
358 a temporary location to unpack a compressed file.
360 The subclass may delete this file if it wishes, but it will be
361 automatically deleted by cleanup().
363 A tuple containing a file descriptor (returned from os.open() and the
364 absolute path to the temporary directory is returned.
366 Note, this method should only be called after mount() has been called.
368 prefix -- a prefix which should be used when creating the file;
369 defaults to "tmp-".
372 self.__ensure_builddir()
373 return tempfile.mkstemp(dir = self.__builddir, prefix = prefix)
375 def _mktemp(self, prefix = "tmp-"):
376 """Create a temporary file.
378 This method simply calls _mkstemp() and closes the returned file
379 descriptor.
381 The absolute path to the temporary file is returned.
383 Note, this method should only be called after mount() has been called.
385 prefix -- a prefix which should be used when creating the file;
386 defaults to "tmp-".
390 (f, path) = self._mkstemp(prefix)
391 os.close(f)
392 return path
395 # Actual implementation
397 def __ensure_builddir(self):
398 if not self.__builddir is None:
399 return
401 try:
402 self.__builddir = tempfile.mkdtemp(dir = os.path.abspath(self.tmpdir),
403 prefix = "imgcreate-")
404 except OSError, e:
405 raise CreatorError("Failed create build directory in %s: %s" %
406 (self.tmpdir, e.strerror))
408 def __sanity_check(self):
409 """Ensure that the config we've been given is sane."""
410 if not (kickstart.get_packages(self.ks) or
411 kickstart.get_groups(self.ks)):
412 raise CreatorError("No packages or groups specified")
414 kickstart.convert_method_to_repo(self.ks)
416 if not kickstart.get_repos(self.ks):
417 raise CreatorError("No repositories specified")
419 def __write_fstab(self):
420 fstab = open(self._instroot + "/etc/fstab", "w")
421 fstab.write(self._get_fstab())
422 fstab.close()
424 def __create_minimal_dev(self):
425 """Create a minimal /dev so that we don't corrupt the host /dev"""
426 origumask = os.umask(0000)
427 devices = (('null', 1, 3, 0666),
428 ('urandom',1, 9, 0666),
429 ('random', 1, 8, 0666),
430 ('full', 1, 7, 0666),
431 ('ptmx', 5, 2, 0666),
432 ('tty', 5, 0, 0666),
433 ('zero', 1, 5, 0666))
434 links = (("/proc/self/fd", "/dev/fd"),
435 ("/proc/self/fd/0", "/dev/stdin"),
436 ("/proc/self/fd/1", "/dev/stdout"),
437 ("/proc/self/fd/2", "/dev/stderr"))
439 for (node, major, minor, perm) in devices:
440 if not os.path.exists(self._instroot + "/dev/" + node):
441 os.mknod(self._instroot + "/dev/" + node, perm | stat.S_IFCHR, os.makedev(major,minor))
442 for (src, dest) in links:
443 if not os.path.exists(self._instroot + dest):
444 os.symlink(src, self._instroot + dest)
445 os.umask(origumask)
447 def __create_selinuxfs(self):
448 if not os.path.exists(self.__selinux_mountpoint):
449 return
451 arglist = ["/bin/mount", "--bind", "/dev/null", self._instroot + self.__selinux_mountpoint + "/load"]
452 subprocess.call(arglist, close_fds = True)
454 if kickstart.selinux_enabled(self.ks):
455 # label the fs like it is a root before the bind mounting
456 arglist = ["/sbin/setfiles", "-F", "-r", self._instroot, selinux.selinux_file_context_path(), self._instroot]
457 subprocess.call(arglist, close_fds = True)
458 # these dumb things don't get magically fixed, so make the user generic
459 # if selinux exists on the host we need to lie to the chroot
460 if selinux.is_selinux_enabled():
461 for f in ("/proc", "/sys"):
462 arglist = ["/usr/bin/chcon", "-u", "system_u", self._instroot + f]
463 subprocess.call(arglist, close_fds = True)
465 def __destroy_selinuxfs(self):
466 if not os.path.exists(self.__selinux_mountpoint):
467 return
469 # if the system was running selinux clean up our lies
470 arglist = ["/bin/umount", self._instroot + self.__selinux_mountpoint + "/load"]
471 subprocess.call(arglist, close_fds = True)
473 def mount(self, base_on = None, cachedir = None):
474 """Setup the target filesystem in preparation for an install.
476 This function sets up the filesystem which the ImageCreator will
477 install into and configure. The ImageCreator class merely creates an
478 install root directory, bind mounts some system directories (e.g. /dev)
479 and writes out /etc/fstab. Other subclasses may also e.g. create a
480 sparse file, format it and loopback mount it to the install root.
482 base_on -- a previous install on which to base this install; defaults
483 to None, causing a new image to be created
485 cachedir -- a directory in which to store the Yum cache; defaults to
486 None, causing a new cache to be created; by setting this
487 to another directory, the same cache can be reused across
488 multiple installs.
491 self.__ensure_builddir()
493 makedirs(self._instroot)
494 makedirs(self._outdir)
496 self._mount_instroot(base_on)
498 for d in ("/dev/pts", "/etc", "/boot", "/var/log", "/var/cache/yum", "/sys", "/proc"):
499 makedirs(self._instroot + d)
501 cachesrc = cachedir or (self.__builddir + "/yum-cache")
502 makedirs(cachesrc)
504 # bind mount system directories into _instroot
505 for (f, dest) in [("/sys", None), ("/proc", None),
506 ("/dev/pts", None), ("/dev/shm", None),
507 (self.__selinux_mountpoint, self.__selinux_mountpoint),
508 (cachesrc, "/var/cache/yum")]:
509 if os.path.exists(f):
510 self.__bindmounts.append(BindChrootMount(f, self._instroot, dest))
511 else:
512 logging.warn("Skipping (%s,%s) because source doesn't exist." % (f, dest))
514 self._do_bindmounts()
516 self.__create_selinuxfs()
518 self.__create_minimal_dev()
520 os.symlink("/proc/self/mounts", self._instroot + "/etc/mtab")
522 self.__write_fstab()
524 def unmount(self):
525 """Unmounts the target filesystem.
527 The ImageCreator class detaches the system from the install root, but
528 other subclasses may also detach the loopback mounted filesystem image
529 from the install root.
532 self.__destroy_selinuxfs()
534 self._undo_bindmounts()
536 self._unmount_instroot()
538 def cleanup(self):
539 """Unmounts the target filesystem and deletes temporary files.
541 This method calls unmount() and then deletes any temporary files and
542 directories that were created on the host system while building the
543 image.
545 Note, make sure to call this method once finished with the creator
546 instance in order to ensure no stale files are left on the host e.g.:
548 creator = ImageCreator(ks, name)
549 try:
550 creator.create()
551 finally:
552 creator.cleanup()
555 if not self.__builddir:
556 return
558 self.unmount()
560 shutil.rmtree(self.__builddir, ignore_errors = True)
561 self.__builddir = None
563 def __select_packages(self, ayum):
564 skipped_pkgs = []
565 for pkg in kickstart.get_packages(self.ks,
566 self._get_required_packages()):
567 try:
568 ayum.selectPackage(pkg)
569 except yum.Errors.InstallError, e:
570 if kickstart.ignore_missing(self.ks):
571 skipped_pkgs.append(pkg)
572 else:
573 raise CreatorError("Failed to find package '%s' : %s" %
574 (pkg, e))
576 for pkg in skipped_pkgs:
577 logging.warn("Skipping missing package '%s'" % (pkg,))
579 def __select_groups(self, ayum):
580 skipped_groups = []
581 for group in kickstart.get_groups(self.ks):
582 try:
583 ayum.selectGroup(group.name, group.include)
584 except (yum.Errors.InstallError, yum.Errors.GroupsError), e:
585 if kickstart.ignore_missing(self.ks):
586 raise CreatorError("Failed to find group '%s' : %s" %
587 (group.name, e))
588 else:
589 skipped_groups.append(group)
591 for group in skipped_groups:
592 logging.warn("Skipping missing group '%s'" % (group.name,))
594 def __deselect_packages(self, ayum):
595 for pkg in kickstart.get_excluded(self.ks,
596 self._get_excluded_packages()):
597 ayum.deselectPackage(pkg)
599 # if the system is running selinux and the kickstart wants it disabled
600 # we need /usr/sbin/lokkit
601 def __can_handle_selinux(self, ayum):
602 file = "/usr/sbin/lokkit"
603 if not kickstart.selinux_enabled(self.ks) and selinux.is_selinux_enabled() and not ayum.installHasFile(file):
604 raise CreatorError("Unable to disable SELinux because the installed package set did not include the file %s" % (file))
606 def install(self, repo_urls = {}):
607 """Install packages into the install root.
609 This function installs the packages listed in the supplied kickstart
610 into the install root. By default, the packages are installed from the
611 repository URLs specified in the kickstart.
613 repo_urls -- a dict which maps a repository name to a repository URL;
614 if supplied, this causes any repository URLs specified in
615 the kickstart to be overridden.
618 yum_conf = self._mktemp(prefix = "yum.conf-")
620 ayum = LiveCDYum(releasever=self.releasever)
621 ayum.setup(yum_conf, self._instroot)
623 for repo in kickstart.get_repos(self.ks, repo_urls):
624 (name, baseurl, mirrorlist, proxy, inc, exc, cost) = repo
626 yr = ayum.addRepository(name, baseurl, mirrorlist)
627 if inc:
628 yr.includepkgs = inc
629 if exc:
630 yr.exclude = exc
631 if proxy:
632 yr.proxy = proxy
633 if cost is not None:
634 yr.cost = cost
636 if kickstart.exclude_docs(self.ks):
637 rpm.addMacro("_excludedocs", "1")
638 if not kickstart.selinux_enabled(self.ks):
639 rpm.addMacro("__file_context_path", "%{nil}")
640 if kickstart.inst_langs(self.ks) != None:
641 rpm.addMacro("_install_langs", kickstart.inst_langs(self.ks))
643 try:
644 self.__select_packages(ayum)
645 self.__select_groups(ayum)
646 self.__deselect_packages(ayum)
648 self.__can_handle_selinux(ayum)
650 ayum.runInstall()
651 except yum.Errors.RepoError, e:
652 raise CreatorError("Unable to download from repo : %s" % (e,))
653 except yum.Errors.YumBaseError, e:
654 raise CreatorError("Unable to install: %s" % (e,))
655 finally:
656 ayum.closeRpmDB()
657 ayum.close()
658 os.unlink(yum_conf)
660 # do some clean up to avoid lvm info leakage. this sucks.
661 for subdir in ("cache", "backup", "archive"):
662 lvmdir = self._instroot + "/etc/lvm/" + subdir
663 try:
664 for f in os.listdir(lvmdir):
665 os.unlink(lvmdir + "/" + f)
666 except:
667 pass
669 def _run_post_scripts(self):
670 for s in kickstart.get_post_scripts(self.ks):
671 (fd, path) = tempfile.mkstemp(prefix = "ks-script-",
672 dir = self._instroot + "/tmp")
674 os.write(fd, s.script)
675 os.close(fd)
676 os.chmod(path, 0700)
678 env = self._get_post_scripts_env(s.inChroot)
680 if not s.inChroot:
681 env["INSTALL_ROOT"] = self._instroot
682 preexec = None
683 script = path
684 else:
685 preexec = self._chroot
686 script = "/tmp/" + os.path.basename(path)
688 try:
689 subprocess.check_call([s.interp, script],
690 preexec_fn = preexec, env = env)
691 except OSError, e:
692 raise CreatorError("Failed to execute %%post script "
693 "with '%s' : %s" % (s.interp, e.strerror))
694 except subprocess.CalledProcessError, err:
695 if s.errorOnFail:
696 raise CreatorError("%%post script failed with code %d "
697 % err.returncode)
698 logging.warning("ignoring %%post failure (code %d)"
699 % err.returncode)
700 finally:
701 os.unlink(path)
703 def configure(self):
704 """Configure the system image according to the kickstart.
706 This method applies the (e.g. keyboard or network) configuration
707 specified in the kickstart and executes the kickstart %post scripts.
709 If neccessary, it also prepares the image to be bootable by e.g.
710 creating an initrd and bootloader configuration.
713 ksh = self.ks.handler
715 kickstart.LanguageConfig(self._instroot).apply(ksh.lang)
716 kickstart.KeyboardConfig(self._instroot).apply(ksh.keyboard)
717 kickstart.TimezoneConfig(self._instroot).apply(ksh.timezone)
718 kickstart.AuthConfig(self._instroot).apply(ksh.authconfig)
719 kickstart.FirewallConfig(self._instroot).apply(ksh.firewall)
720 kickstart.RootPasswordConfig(self._instroot).apply(ksh.rootpw)
721 kickstart.ServicesConfig(self._instroot).apply(ksh.services)
722 kickstart.XConfig(self._instroot).apply(ksh.xconfig)
723 kickstart.NetworkConfig(self._instroot).apply(ksh.network)
724 kickstart.RPMMacroConfig(self._instroot).apply(self.ks)
726 self._create_bootconfig()
728 self._run_post_scripts()
729 kickstart.SelinuxConfig(self._instroot).apply(ksh.selinux)
731 def launch_shell(self):
732 """Launch a shell in the install root.
734 This method is launches a bash shell chroot()ed in the install root;
735 this can be useful for debugging.
738 subprocess.call(["/bin/bash"], preexec_fn = self._chroot)
740 def package(self, destdir = "."):
741 """Prepares the created image for final delivery.
743 In its simplest form, this method merely copies the install root to the
744 supplied destination directory; other subclasses may choose to package
745 the image by e.g. creating a bootable ISO containing the image and
746 bootloader configuration.
748 destdir -- the directory into which the final image should be moved;
749 this defaults to the current directory.
752 self._stage_final_image()
754 for f in os.listdir(self._outdir):
755 shutil.move(os.path.join(self._outdir, f),
756 os.path.join(destdir, f))
758 def create(self):
759 """Install, configure and package an image.
761 This method is a utility method which creates and image by calling some
762 of the other methods in the following order - mount(), install(),
763 configure(), unmount and package().
766 self.mount()
767 self.install()
768 self.configure()
769 self.unmount()
770 self.package()
772 class LoopImageCreator(ImageCreator):
773 """Installs a system into a loopback-mountable filesystem image.
775 LoopImageCreator is a straightforward ImageCreator subclass; the system
776 is installed into an ext3 filesystem on a sparse file which can be
777 subsequently loopback-mounted.
781 def __init__(self, ks, name, fslabel=None, releasever=None, tmpdir="/tmp"):
782 """Initialize a LoopImageCreator instance.
784 This method takes the same arguments as ImageCreator.__init__() with
785 the addition of:
787 fslabel -- A string used as a label for any filesystems created.
790 ImageCreator.__init__(self, ks, name, releasever=releasever, tmpdir=tmpdir)
792 self.__fslabel = None
793 self.fslabel = fslabel
795 self.__minsize_KB = 0
796 self.__blocksize = 4096
797 self.__fstype = kickstart.get_image_fstype(self.ks, "ext3")
799 self.__instloop = None
800 self.__imgdir = None
802 self.__image_size = kickstart.get_image_size(self.ks,
803 4096L * 1024 * 1024)
806 # Properties
808 def __get_fslabel(self):
809 if self.__fslabel is None:
810 return self.name
811 else:
812 return self.__fslabel
813 def __set_fslabel(self, val):
814 if val is None:
815 self.__fslabel = None
816 else:
817 self.__fslabel = val[:FSLABEL_MAXLEN]
818 fslabel = property(__get_fslabel, __set_fslabel)
819 """A string used to label any filesystems created.
821 Some filesystems impose a constraint on the maximum allowed size of the
822 filesystem label. In the case of ext3 it's 16 characters, but in the case
823 of ISO9660 it's 32 characters.
825 mke2fs silently truncates the label, but mkisofs aborts if the label is too
826 long. So, for convenience sake, any string assigned to this attribute is
827 silently truncated to FSLABEL_MAXLEN (32) characters.
831 def __get_image(self):
832 if self.__imgdir is None:
833 raise CreatorError("_image is not valid before calling mount()")
834 return self.__imgdir + "/ext3fs.img"
835 _image = property(__get_image)
836 """The location of the image file.
838 This is the path to the filesystem image. Subclasses may use this path
839 in order to package the image in _stage_final_image().
841 Note, this directory does not exist before ImageCreator.mount() is called.
843 Note also, this is a read-only attribute.
847 def __get_blocksize(self):
848 return self.__blocksize
849 def __set_blocksize(self, val):
850 if self.__instloop:
851 raise CreatorError("_blocksize must be set before calling mount()")
852 try:
853 self.__blocksize = int(val)
854 except ValueError:
855 raise CreatorError("'%s' is not a valid integer value "
856 "for _blocksize" % val)
857 _blocksize = property(__get_blocksize, __set_blocksize)
858 """The block size used by the image's filesystem.
860 This is the block size used when creating the filesystem image. Subclasses
861 may change this if they wish to use something other than a 4k block size.
863 Note, this attribute may only be set before calling mount().
867 def __get_fstype(self):
868 return self.__fstype
869 def __set_fstype(self, val):
870 if val not in ("ext2", "ext3", "ext4"):
871 raise CreatorError("Unknown _fstype '%s' supplied" % val)
872 self.__fstype = val
873 _fstype = property(__get_fstype, __set_fstype)
874 """The type of filesystem used for the image.
876 This is the filesystem type used when creating the filesystem image.
877 Subclasses may change this if they wish to use something other ext3.
879 Note, only ext2, ext3, ext4 are currently supported.
881 Note also, this attribute may only be set before calling mount().
886 # Helpers for subclasses
888 def _resparse(self, size = None):
889 """Rebuild the filesystem image to be as sparse as possible.
891 This method should be used by subclasses when staging the final image
892 in order to reduce the actual space taken up by the sparse image file
893 to be as little as possible.
895 This is done by resizing the filesystem to the minimal size (thereby
896 eliminating any space taken up by deleted files) and then resizing it
897 back to the supplied size.
899 size -- the size in, in bytes, which the filesystem image should be
900 resized to after it has been minimized; this defaults to None,
901 causing the original size specified by the kickstart file to
902 be used (or 4GiB if not specified in the kickstart).
905 return self.__instloop.resparse(size)
907 def _base_on(self, base_on):
908 shutil.copyfile(base_on, self._image)
911 # Actual implementation
913 def _mount_instroot(self, base_on = None):
914 self.__imgdir = self._mkdtemp()
916 if not base_on is None:
917 self._base_on(base_on)
919 self.__instloop = ExtDiskMount(SparseLoopbackDisk(self._image,
920 self.__image_size),
921 self._instroot,
922 self.__fstype,
923 self.__blocksize,
924 self.fslabel,
925 self.tmpdir)
927 try:
928 self.__instloop.mount()
929 except MountError, e:
930 raise CreatorError("Failed to loopback mount '%s' : %s" %
931 (self._image, e))
933 def _unmount_instroot(self):
934 if not self.__instloop is None:
935 self.__instloop.cleanup()
937 def _stage_final_image(self):
938 self._resparse()
939 shutil.move(self._image, self._outdir + "/" + self.name + ".img")