Implement cacheonly (offline) support in ImageCreator and LoopCreator
[livecd.git] / imgcreate / creator.py
blob77b6e56e5adb2c1a7d766a3880b328ceac0e9c32
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, cacheonly=False):
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
68 cacheonly -- Only read from cache, work offline
69 """
70 self.ks = ks
71 """A pykickstart.KickstartParser instance."""
73 self.name = name
74 """A name for the image."""
76 self.releasever = releasever
77 self.useplugins = useplugins
79 self.tmpdir = tmpdir
80 """The directory in which all temporary files will be created."""
81 if not os.path.exists(self.tmpdir):
82 makedirs(self.tmpdir)
84 self.cacheonly = cacheonly
86 self.__builddir = None
87 self.__bindmounts = []
89 self.__sanity_check()
91 # get selinuxfs mountpoint
92 self.__selinux_mountpoint = "/sys/fs/selinux"
93 with open("/proc/self/mountinfo", "r") as f:
94 for line in f.readlines():
95 fields = line.split()
96 if fields[-2] == "selinuxfs":
97 self.__selinux_mountpoint = fields[4]
98 break
100 def __del__(self):
101 self.cleanup()
104 # Properties
106 def __get_instroot(self):
107 if self.__builddir is None:
108 raise CreatorError("_instroot is not valid before calling mount()")
109 return self.__builddir + "/install_root"
110 _instroot = property(__get_instroot)
111 """The location of the install root directory.
113 This is the directory into which the system is installed. Subclasses may
114 mount a filesystem image here or copy files to/from here.
116 Note, this directory does not exist before ImageCreator.mount() is called.
118 Note also, this is a read-only attribute.
122 def __get_outdir(self):
123 if self.__builddir is None:
124 raise CreatorError("_outdir is not valid before calling mount()")
125 return self.__builddir + "/out"
126 _outdir = property(__get_outdir)
127 """The staging location for the final image.
129 This is where subclasses should stage any files that are part of the final
130 image. ImageCreator.package() will copy any files found here into the
131 requested destination directory.
133 Note, this directory does not exist before ImageCreator.mount() is called.
135 Note also, this is a read-only attribute.
140 # Hooks for subclasses
142 def _mount_instroot(self, base_on = None):
143 """Mount or prepare the install root directory.
145 This is the hook where subclasses may prepare the install root by e.g.
146 mounting creating and loopback mounting a filesystem image to
147 _instroot.
149 There is no default implementation.
151 base_on -- this is the value passed to mount() and can be interpreted
152 as the subclass wishes; it might e.g. be the location of
153 a previously created ISO containing a system image.
156 pass
158 def _unmount_instroot(self):
159 """Undo anything performed in _mount_instroot().
161 This is the hook where subclasses must undo anything which was done
162 in _mount_instroot(). For example, if a filesystem image was mounted
163 onto _instroot, it should be unmounted here.
165 There is no default implementation.
168 pass
170 def _create_bootconfig(self):
171 """Configure the image so that it's bootable.
173 This is the hook where subclasses may prepare the image for booting by
174 e.g. creating an initramfs and bootloader configuration.
176 This hook is called while the install root is still mounted, after the
177 packages have been installed and the kickstart configuration has been
178 applied, but before the %post scripts have been executed.
180 There is no default implementation.
183 pass
185 def _stage_final_image(self):
186 """Stage the final system image in _outdir.
188 This is the hook where subclasses should place the image in _outdir
189 so that package() can copy it to the requested destination directory.
191 By default, this moves the install root into _outdir.
194 shutil.move(self._instroot, self._outdir + "/" + self.name)
196 def _get_required_packages(self):
197 """Return a list of required packages.
199 This is the hook where subclasses may specify a set of packages which
200 it requires to be installed.
202 This returns an empty list by default.
204 Note, subclasses should usually chain up to the base class
205 implementation of this hook.
208 return []
210 def _get_excluded_packages(self):
211 """Return a list of excluded packages.
213 This is the hook where subclasses may specify a set of packages which
214 it requires _not_ to be installed.
216 This returns an empty list by default.
218 Note, subclasses should usually chain up to the base class
219 implementation of this hook.
222 return []
224 def _get_fstab(self):
225 """Return the desired contents of /etc/fstab.
227 This is the hook where subclasses may specify the contents of
228 /etc/fstab by returning a string containing the desired contents.
230 A sensible default implementation is provided.
233 s = "/dev/root / %s defaults,noatime 0 0\n" %(self._fstype)
234 s += self._get_fstab_special()
235 return s
237 def _get_fstab_special(self):
238 s = "devpts /dev/pts devpts gid=5,mode=620 0 0\n"
239 s += "tmpfs /dev/shm tmpfs defaults 0 0\n"
240 s += "proc /proc proc defaults 0 0\n"
241 s += "sysfs /sys sysfs defaults 0 0\n"
242 return s
244 def _get_post_scripts_env(self, in_chroot):
245 """Return an environment dict for %post scripts.
247 This is the hook where subclasses may specify some environment
248 variables for %post scripts by return a dict containing the desired
249 environment.
251 By default, this returns an empty dict.
253 in_chroot -- whether this %post script is to be executed chroot()ed
254 into _instroot.
257 return {}
259 def _get_kernel_versions(self):
260 """Return a dict detailing the available kernel types/versions.
262 This is the hook where subclasses may override what kernel types and
263 versions should be available for e.g. creating the booloader
264 configuration.
266 A dict should be returned mapping the available kernel types to a list
267 of the available versions for those kernels.
269 The default implementation uses rpm to iterate over everything
270 providing 'kernel', finds /boot/vmlinuz-* and returns the version
271 obtained from the vmlinuz filename. (This can differ from the kernel
272 RPM's n-v-r in the case of e.g. xen)
275 def get_version(header):
276 version = None
277 for f in header['filenames']:
278 if f.startswith('/boot/vmlinuz-'):
279 version = f[14:]
280 return version
282 ts = rpm.TransactionSet(self._instroot)
284 ret = {}
285 for header in ts.dbMatch('provides', 'kernel'):
286 version = get_version(header)
287 if version is None:
288 continue
290 name = header['name']
291 if not name in ret:
292 ret[name] = [version]
293 elif not version in ret[name]:
294 ret[name].append(version)
296 return ret
299 # Helpers for subclasses
301 def _do_bindmounts(self):
302 """Mount various system directories onto _instroot.
304 This method is called by mount(), but may also be used by subclasses
305 in order to re-mount the bindmounts after modifying the underlying
306 filesystem.
309 for b in self.__bindmounts:
310 b.mount()
312 def _undo_bindmounts(self):
313 """Unmount the bind-mounted system directories from _instroot.
315 This method is usually only called by unmount(), but may also be used
316 by subclasses in order to gain access to the filesystem obscured by
317 the bindmounts - e.g. in order to create device nodes on the image
318 filesystem.
321 self.__bindmounts.reverse()
322 for b in self.__bindmounts:
323 b.unmount()
325 def _chroot(self):
326 """Chroot into the install root.
328 This method may be used by subclasses when executing programs inside
329 the install root e.g.
331 subprocess.call(["/bin/ls"], preexec_fn = self.chroot)
334 os.chroot(self._instroot)
335 os.chdir("/")
337 def _mkdtemp(self, prefix = "tmp-"):
338 """Create a temporary directory.
340 This method may be used by subclasses to create a temporary directory
341 for use in building the final image - e.g. a subclass might create
342 a temporary directory in order to bundle a set of files into a package.
344 The subclass may delete this directory if it wishes, but it will be
345 automatically deleted by cleanup().
347 The absolute path to the temporary directory is returned.
349 Note, this method should only be called after mount() has been called.
351 prefix -- a prefix which should be used when creating the directory;
352 defaults to "tmp-".
355 self.__ensure_builddir()
356 return tempfile.mkdtemp(dir = self.__builddir, prefix = prefix)
358 def _mkstemp(self, prefix = "tmp-"):
359 """Create a temporary file.
361 This method may be used by subclasses to create a temporary file
362 for use in building the final image - e.g. a subclass might need
363 a temporary location to unpack a compressed file.
365 The subclass may delete this file if it wishes, but it will be
366 automatically deleted by cleanup().
368 A tuple containing a file descriptor (returned from os.open() and the
369 absolute path to the temporary directory is returned.
371 Note, this method should only be called after mount() has been called.
373 prefix -- a prefix which should be used when creating the file;
374 defaults to "tmp-".
377 self.__ensure_builddir()
378 return tempfile.mkstemp(dir = self.__builddir, prefix = prefix)
380 def _mktemp(self, prefix = "tmp-"):
381 """Create a temporary file.
383 This method simply calls _mkstemp() and closes the returned file
384 descriptor.
386 The absolute path to the temporary file is returned.
388 Note, this method should only be called after mount() has been called.
390 prefix -- a prefix which should be used when creating the file;
391 defaults to "tmp-".
395 (f, path) = self._mkstemp(prefix)
396 os.close(f)
397 return path
400 # Actual implementation
402 def __ensure_builddir(self):
403 if not self.__builddir is None:
404 return
406 try:
407 self.__builddir = tempfile.mkdtemp(dir = os.path.abspath(self.tmpdir),
408 prefix = "imgcreate-")
409 except OSError, e:
410 raise CreatorError("Failed create build directory in %s: %s" %
411 (self.tmpdir, e.strerror))
413 def __sanity_check(self):
414 """Ensure that the config we've been given is sane."""
415 if not (kickstart.get_packages(self.ks) or
416 kickstart.get_groups(self.ks)):
417 raise CreatorError("No packages or groups specified")
419 kickstart.convert_method_to_repo(self.ks)
421 if not kickstart.get_repos(self.ks):
422 raise CreatorError("No repositories specified")
424 def __write_fstab(self):
425 fstab = open(self._instroot + "/etc/fstab", "w")
426 fstab.write(self._get_fstab())
427 fstab.close()
429 def __create_minimal_dev(self):
430 """Create a minimal /dev so that we don't corrupt the host /dev"""
431 origumask = os.umask(0000)
432 devices = (('null', 1, 3, 0666),
433 ('urandom',1, 9, 0666),
434 ('random', 1, 8, 0666),
435 ('full', 1, 7, 0666),
436 ('ptmx', 5, 2, 0666),
437 ('tty', 5, 0, 0666),
438 ('zero', 1, 5, 0666))
439 links = (("/proc/self/fd", "/dev/fd"),
440 ("/proc/self/fd/0", "/dev/stdin"),
441 ("/proc/self/fd/1", "/dev/stdout"),
442 ("/proc/self/fd/2", "/dev/stderr"))
444 for (node, major, minor, perm) in devices:
445 if not os.path.exists(self._instroot + "/dev/" + node):
446 os.mknod(self._instroot + "/dev/" + node, perm | stat.S_IFCHR, os.makedev(major,minor))
447 for (src, dest) in links:
448 if not os.path.exists(self._instroot + dest):
449 os.symlink(src, self._instroot + dest)
450 os.umask(origumask)
452 def __create_selinuxfs(self):
453 if not os.path.exists(self.__selinux_mountpoint):
454 return
456 arglist = ["/bin/mount", "--bind", "/dev/null", self._instroot + self.__selinux_mountpoint + "/load"]
457 subprocess.call(arglist, close_fds = True)
459 if kickstart.selinux_enabled(self.ks):
460 # label the fs like it is a root before the bind mounting
461 arglist = ["/sbin/setfiles", "-F", "-r", self._instroot, selinux.selinux_file_context_path(), self._instroot]
462 subprocess.call(arglist, close_fds = True)
463 # these dumb things don't get magically fixed, so make the user generic
464 # if selinux exists on the host we need to lie to the chroot
465 if selinux.is_selinux_enabled():
466 for f in ("/proc", "/sys"):
467 arglist = ["/usr/bin/chcon", "-u", "system_u", self._instroot + f]
468 subprocess.call(arglist, close_fds = True)
470 def __destroy_selinuxfs(self):
471 if not os.path.exists(self.__selinux_mountpoint):
472 return
474 # if the system was running selinux clean up our lies
475 arglist = ["/bin/umount", self._instroot + self.__selinux_mountpoint + "/load"]
476 subprocess.call(arglist, close_fds = True)
478 def mount(self, base_on = None, cachedir = None):
479 """Setup the target filesystem in preparation for an install.
481 This function sets up the filesystem which the ImageCreator will
482 install into and configure. The ImageCreator class merely creates an
483 install root directory, bind mounts some system directories (e.g. /dev)
484 and writes out /etc/fstab. Other subclasses may also e.g. create a
485 sparse file, format it and loopback mount it to the install root.
487 base_on -- a previous install on which to base this install; defaults
488 to None, causing a new image to be created
490 cachedir -- a directory in which to store the Yum cache; defaults to
491 None, causing a new cache to be created; by setting this
492 to another directory, the same cache can be reused across
493 multiple installs.
496 self.__ensure_builddir()
498 makedirs(self._instroot)
499 makedirs(self._outdir)
501 self._mount_instroot(base_on)
503 for d in ("/dev/pts", "/etc", "/boot", "/var/log", "/var/cache/yum", "/sys", "/proc"):
504 makedirs(self._instroot + d)
506 cachesrc = cachedir or (self.__builddir + "/yum-cache")
507 makedirs(cachesrc)
509 # bind mount system directories into _instroot
510 for (f, dest) in [("/sys", None), ("/proc", None),
511 ("/dev/pts", None), ("/dev/shm", None),
512 (self.__selinux_mountpoint, self.__selinux_mountpoint),
513 (cachesrc, "/var/cache/yum")]:
514 if os.path.exists(f):
515 self.__bindmounts.append(BindChrootMount(f, self._instroot, dest))
516 else:
517 logging.warn("Skipping (%s,%s) because source doesn't exist." % (f, dest))
519 self._do_bindmounts()
521 self.__create_selinuxfs()
523 self.__create_minimal_dev()
525 os.symlink("/proc/self/mounts", self._instroot + "/etc/mtab")
527 self.__write_fstab()
529 def unmount(self):
530 """Unmounts the target filesystem.
532 The ImageCreator class detaches the system from the install root, but
533 other subclasses may also detach the loopback mounted filesystem image
534 from the install root.
537 self.__destroy_selinuxfs()
539 self._undo_bindmounts()
541 self._unmount_instroot()
543 def cleanup(self):
544 """Unmounts the target filesystem and deletes temporary files.
546 This method calls unmount() and then deletes any temporary files and
547 directories that were created on the host system while building the
548 image.
550 Note, make sure to call this method once finished with the creator
551 instance in order to ensure no stale files are left on the host e.g.:
553 creator = ImageCreator(ks, name)
554 try:
555 creator.create()
556 finally:
557 creator.cleanup()
560 if not self.__builddir:
561 return
563 self.unmount()
565 shutil.rmtree(self.__builddir, ignore_errors = True)
566 self.__builddir = None
568 def __select_packages(self, ayum):
569 skipped_pkgs = []
570 for pkg in kickstart.get_packages(self.ks,
571 self._get_required_packages()):
572 try:
573 ayum.selectPackage(pkg)
574 except yum.Errors.InstallError, e:
575 if kickstart.ignore_missing(self.ks):
576 skipped_pkgs.append(pkg)
577 else:
578 raise CreatorError("Failed to find package '%s' : %s" %
579 (pkg, e))
581 for pkg in skipped_pkgs:
582 logging.warn("Skipping missing package '%s'" % (pkg,))
584 def __select_groups(self, ayum):
585 skipped_groups = []
586 for group in kickstart.get_groups(self.ks):
587 try:
588 ayum.selectGroup(group.name, group.include)
589 except (yum.Errors.InstallError, yum.Errors.GroupsError), e:
590 if kickstart.ignore_missing(self.ks):
591 raise CreatorError("Failed to find group '%s' : %s" %
592 (group.name, e))
593 else:
594 skipped_groups.append(group)
596 for group in skipped_groups:
597 logging.warn("Skipping missing group '%s'" % (group.name,))
599 def __deselect_packages(self, ayum):
600 for pkg in kickstart.get_excluded(self.ks,
601 self._get_excluded_packages()):
602 ayum.deselectPackage(pkg)
604 # if the system is running selinux and the kickstart wants it disabled
605 # we need /usr/sbin/lokkit
606 def __can_handle_selinux(self, ayum):
607 file = "/usr/sbin/lokkit"
608 if not kickstart.selinux_enabled(self.ks) and selinux.is_selinux_enabled() and not ayum.installHasFile(file):
609 raise CreatorError("Unable to disable SELinux because the installed package set did not include the file %s" % (file))
611 def install(self, repo_urls = {}):
612 """Install packages into the install root.
614 This function installs the packages listed in the supplied kickstart
615 into the install root. By default, the packages are installed from the
616 repository URLs specified in the kickstart.
618 repo_urls -- a dict which maps a repository name to a repository URL;
619 if supplied, this causes any repository URLs specified in
620 the kickstart to be overridden.
623 yum_conf = self._mktemp(prefix = "yum.conf-")
625 ayum = LiveCDYum(releasever=self.releasever, useplugins=self.useplugins)
626 ayum.setup(yum_conf, self._instroot, cacheonly=self.cacheonly)
628 for repo in kickstart.get_repos(self.ks, repo_urls):
629 (name, baseurl, mirrorlist, proxy, inc, exc, cost) = repo
631 yr = ayum.addRepository(name, baseurl, mirrorlist)
632 if inc:
633 yr.includepkgs = inc
634 if exc:
635 yr.exclude = exc
636 if proxy:
637 yr.proxy = proxy
638 if cost is not None:
639 yr.cost = cost
640 ayum.setup(yum_conf, self._instroot)
642 if kickstart.exclude_docs(self.ks):
643 rpm.addMacro("_excludedocs", "1")
644 if not kickstart.selinux_enabled(self.ks):
645 rpm.addMacro("__file_context_path", "%{nil}")
646 if kickstart.inst_langs(self.ks) != None:
647 rpm.addMacro("_install_langs", kickstart.inst_langs(self.ks))
649 try:
650 self.__select_packages(ayum)
651 self.__select_groups(ayum)
652 self.__deselect_packages(ayum)
654 self.__can_handle_selinux(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", useplugins=False, cacheonly=False):
788 """Initialize a LoopImageCreator instance.
790 This method takes the same arguments as ImageCreator.__init__() with
791 the addition of:
793 fslabel -- A string used as a label for any filesystems created.
796 ImageCreator.__init__(self, ks, name, releasever=releasever, tmpdir=tmpdir, useplugins=useplugins, cacheonly=cacheonly)
798 self.__fslabel = None
799 self.fslabel = fslabel
801 self.__minsize_KB = 0
802 self.__blocksize = 4096
803 self.__fstype = kickstart.get_image_fstype(self.ks, "ext3")
805 self.__instloop = None
806 self.__imgdir = None
808 self.__image_size = kickstart.get_image_size(self.ks,
809 4096L * 1024 * 1024)
812 # Properties
814 def __get_fslabel(self):
815 if self.__fslabel is None:
816 return self.name
817 else:
818 return self.__fslabel
819 def __set_fslabel(self, val):
820 if val is None:
821 self.__fslabel = None
822 else:
823 self.__fslabel = val[:FSLABEL_MAXLEN]
824 fslabel = property(__get_fslabel, __set_fslabel)
825 """A string used to label any filesystems created.
827 Some filesystems impose a constraint on the maximum allowed size of the
828 filesystem label. In the case of ext3 it's 16 characters, but in the case
829 of ISO9660 it's 32 characters.
831 mke2fs silently truncates the label, but mkisofs aborts if the label is too
832 long. So, for convenience sake, any string assigned to this attribute is
833 silently truncated to FSLABEL_MAXLEN (32) characters.
837 def __get_image(self):
838 if self.__imgdir is None:
839 raise CreatorError("_image is not valid before calling mount()")
840 return self.__imgdir + "/ext3fs.img"
841 _image = property(__get_image)
842 """The location of the image file.
844 This is the path to the filesystem image. Subclasses may use this path
845 in order to package the image in _stage_final_image().
847 Note, this directory does not exist before ImageCreator.mount() is called.
849 Note also, this is a read-only attribute.
853 def __get_blocksize(self):
854 return self.__blocksize
855 def __set_blocksize(self, val):
856 if self.__instloop:
857 raise CreatorError("_blocksize must be set before calling mount()")
858 try:
859 self.__blocksize = int(val)
860 except ValueError:
861 raise CreatorError("'%s' is not a valid integer value "
862 "for _blocksize" % val)
863 _blocksize = property(__get_blocksize, __set_blocksize)
864 """The block size used by the image's filesystem.
866 This is the block size used when creating the filesystem image. Subclasses
867 may change this if they wish to use something other than a 4k block size.
869 Note, this attribute may only be set before calling mount().
873 def __get_fstype(self):
874 return self.__fstype
875 def __set_fstype(self, val):
876 if val not in ("ext2", "ext3", "ext4"):
877 raise CreatorError("Unknown _fstype '%s' supplied" % val)
878 self.__fstype = val
879 _fstype = property(__get_fstype, __set_fstype)
880 """The type of filesystem used for the image.
882 This is the filesystem type used when creating the filesystem image.
883 Subclasses may change this if they wish to use something other ext3.
885 Note, only ext2, ext3, ext4 are currently supported.
887 Note also, this attribute may only be set before calling mount().
892 # Helpers for subclasses
894 def _resparse(self, size = None):
895 """Rebuild the filesystem image to be as sparse as possible.
897 This method should be used by subclasses when staging the final image
898 in order to reduce the actual space taken up by the sparse image file
899 to be as little as possible.
901 This is done by resizing the filesystem to the minimal size (thereby
902 eliminating any space taken up by deleted files) and then resizing it
903 back to the supplied size.
905 size -- the size in, in bytes, which the filesystem image should be
906 resized to after it has been minimized; this defaults to None,
907 causing the original size specified by the kickstart file to
908 be used (or 4GiB if not specified in the kickstart).
911 return self.__instloop.resparse(size)
913 def _base_on(self, base_on):
914 shutil.copyfile(base_on, self._image)
917 # Actual implementation
919 def _mount_instroot(self, base_on = None):
920 self.__imgdir = self._mkdtemp()
922 if not base_on is None:
923 self._base_on(base_on)
925 self.__instloop = ExtDiskMount(SparseLoopbackDisk(self._image,
926 self.__image_size),
927 self._instroot,
928 self.__fstype,
929 self.__blocksize,
930 self.fslabel,
931 self.tmpdir)
933 try:
934 self.__instloop.mount()
935 except MountError, e:
936 raise CreatorError("Failed to loopback mount '%s' : %s" %
937 (self._image, e))
939 def _unmount_instroot(self):
940 if not self.__instloop is None:
941 self.__instloop.cleanup()
943 def _stage_final_image(self):
944 self._resparse()
945 shutil.move(self._image, self._outdir + "/" + self.name + ".img")