f3da977945df5f291271f80177dcc27992798057
[tools/mic.git] / micng / imager / baseimager.py
1 #
2 # creator.py : ImageCreator and LoopImageCreator base classes
3 #
4 # Copyright 2007, Red Hat  Inc.
5 #
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.
9 #
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.
14 #
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.
18
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
27 import re
28 import tarfile
29 import glob
30
31 import rpm
32
33 from micng.utils.errors import *
34 from micng.utils.fs_related import *
35 from micng.utils.rpmmisc import *
36 from micng.utils.misc import *
37 from micng.utils import kickstart
38
39 class BaseImageCreator(object):
40     """Installs a system to a chroot directory.
41
42     ImageCreator is the simplest creator class available; it will install and
43     configure a system image according to the supplied kickstart file.
44
45     e.g.
46
47       import micng.imgcreate as imgcreate
48       ks = imgcreate.read_kickstart("foo.ks")
49       imgcreate.ImageCreator(ks, "foo").create()
50
51     """
52
53     def __init__(self, createopts = None, pkgmgr = None):
54         """Initialize an ImageCreator instance.
55
56         ks -- a pykickstart.KickstartParser instance; this instance will be
57               used to drive the install by e.g. providing the list of packages
58               to be installed, the system configuration and %post scripts
59
60         name -- a name for the image; used for e.g. image filenames or
61                 filesystem labels
62
63         """
64         self.pkgmgr = pkgmgr 
65
66         if createopts:
67             # A pykickstart.KickstartParser instance."""
68             self.ks = createopts['ks']
69             self.repometadata = createopts['repomd']
70
71             # A name for the image."""
72             self.name = 'output'
73
74             # The directory in which all temporary files will be created."""
75             self.tmpdir = createopts['tmpdir']
76
77             self.cachedir = createopts['cachedir']
78
79             self.destdir = createopts['outdir']
80         else:
81             self.ks = None
82             self.repometadata = None
83             self.name = "MeeGo"
84             self.tmpdir = "/var/tmp"
85             self.cachedir = "/var/cache"
86             self.destdir = "."
87  
88         self.__builddir = None
89         self.__bindmounts = []
90
91         self._dep_checks = ["ls", "bash", "cp", "echo", "modprobe", "passwd"]
92
93         ### to be obsolete
94         self.distro_name = "MeeGo"
95         # Output image file names"""
96         self.outimage = []
97         # A flag to generate checksum"""
98         self._genchecksum = False
99         self._alt_initrd_name = None
100         # the disk image after creation, e.g., bz2.
101         # This value is set with compression_method function. """
102         self.__img_compression_method = None
103         # dependent commands to check
104         self._recording_pkgs = None
105         self._include_src = None
106         self._local_pkgs_path = None
107         # available size in root fs, init to 0
108         self._root_fs_avail = 0
109         # target arch for non-x86 image
110         self.target_arch = None
111         # Name of the disk image file that is created. """
112         self._img_name = None
113         # Image format """
114         self.image_format = None
115         # Save qemu emulator file name in order to clean up it finally """
116         self.qemu_emulator = None
117         # No ks provided when called by convertor, so skip the dependency check """
118         if self.ks:
119             # If we have btrfs partition we need to check that we have toosl for those """
120             for part in self.ks.handler.partition.partitions:
121                 if part.fstype and part.fstype == "btrfs":
122                     self._dep_checks.append("mkfs.btrfs")
123                     break
124
125     def set_target_arch(self, arch):
126         if arch not in arches.keys():
127             return False
128         self.target_arch = arch
129         if self.target_arch.startswith("arm"):
130             for dep in self._dep_checks:
131                 if dep == "extlinux":
132                     self._dep_checks.remove(dep)
133
134             if not os.path.exists("/usr/bin/qemu-arm") or not is_statically_linked("/usr/bin/qemu-arm"):
135                 self._dep_checks.append("qemu-arm-static")
136                 
137             if os.path.exists("/proc/sys/vm/vdso_enabled"):
138                 vdso_fh = open("/proc/sys/vm/vdso_enabled","r")
139                 vdso_value = vdso_fh.read().strip()
140                 vdso_fh.close()
141                 if (int)(vdso_value) == 1:
142                     print "\n= WARNING ="
143                     print "vdso is enabled on your host, which might cause problems with arm emulations."
144                     print "You can disable vdso with following command before starting image build:"
145                     print "echo 0 | sudo tee /proc/sys/vm/vdso_enabled"
146                     print "= WARNING =\n"
147
148         return True
149
150
151     def __del__(self):
152         self.cleanup()
153
154     #
155     # Properties
156     #
157     def __get_instroot(self):
158         if self.__builddir is None:
159             raise CreatorError("_instroot is not valid before calling mount()")
160         return self.__builddir + "/install_root"
161     _instroot = property(__get_instroot)
162     """The location of the install root directory.
163
164     This is the directory into which the system is installed. Subclasses may
165     mount a filesystem image here or copy files to/from here.
166
167     Note, this directory does not exist before ImageCreator.mount() is called.
168
169     Note also, this is a read-only attribute.
170
171     """
172
173     def __get_outdir(self):
174         if self.__builddir is None:
175             raise CreatorError("_outdir is not valid before calling mount()")
176         return self.__builddir + "/out"
177     _outdir = property(__get_outdir)
178     """The staging location for the final image.
179
180     This is where subclasses should stage any files that are part of the final
181     image. ImageCreator.package() will copy any files found here into the
182     requested destination directory.
183
184     Note, this directory does not exist before ImageCreator.mount() is called.
185
186     Note also, this is a read-only attribute.
187
188     """
189
190     #
191     # Hooks for subclasses
192     #
193     def _mount_instroot(self, base_on = None):
194         """Mount or prepare the install root directory.
195
196         This is the hook where subclasses may prepare the install root by e.g.
197         mounting creating and loopback mounting a filesystem image to
198         _instroot.
199
200         There is no default implementation.
201
202         base_on -- this is the value passed to mount() and can be interpreted
203                    as the subclass wishes; it might e.g. be the location of
204                    a previously created ISO containing a system image.
205
206         """
207         pass
208
209     def _unmount_instroot(self):
210         """Undo anything performed in _mount_instroot().
211
212         This is the hook where subclasses must undo anything which was done
213         in _mount_instroot(). For example, if a filesystem image was mounted
214         onto _instroot, it should be unmounted here.
215
216         There is no default implementation.
217
218         """
219         pass
220
221     def _create_bootconfig(self):
222         """Configure the image so that it's bootable.
223
224         This is the hook where subclasses may prepare the image for booting by
225         e.g. creating an initramfs and bootloader configuration.
226
227         This hook is called while the install root is still mounted, after the
228         packages have been installed and the kickstart configuration has been
229         applied, but before the %post scripts have been executed.
230
231         There is no default implementation.
232
233         """
234         pass
235
236     def _stage_final_image(self):
237         """Stage the final system image in _outdir.
238
239         This is the hook where subclasses should place the image in _outdir
240         so that package() can copy it to the requested destination directory.
241
242         By default, this moves the install root into _outdir.
243
244         """
245         shutil.move(self._instroot, self._outdir + "/" + self.name)
246
247     def get_installed_packages(self):
248         return self._pkgs_content.keys()
249
250     def _save_recording_pkgs(self, destdir):
251         """Save the list or content of installed packages to file.
252         """
253         if self._recording_pkgs not in ('content', 'name'):
254             return
255
256         pkgs = self._pkgs_content.keys()
257         pkgs.sort() # inplace op
258
259         # save package name list anyhow
260         if not os.path.exists(destdir):
261             makedirs(destdir)
262
263         namefile = os.path.join(destdir, self.name + '-pkgs.txt')
264         f = open(namefile, "w")
265         content = '\n'.join(pkgs)
266         f.write(content)
267         f.close()
268         self.outimage.append(namefile);
269
270         # if 'content', save more details
271         if self._recording_pkgs == 'content':
272             contfile = os.path.join(destdir, self.name + '-pkgs-content.txt')
273             f = open(contfile, "w")
274
275             for pkg in pkgs:
276                 content = pkg + '\n'
277
278                 pkgcont = self._pkgs_content[pkg]
279                 items = []
280                 if pkgcont.has_key('dir'):
281                     items = map(lambda x:x+'/', pkgcont['dir'])
282                 if pkgcont.has_key('file'):
283                     items.extend(pkgcont['file'])
284
285                 if items:
286                     content += '    '
287                     content += '\n    '.join(items)
288                     content += '\n'
289
290                 content += '\n'
291                 f.write(content)
292             f.close()
293             self.outimage.append(contfile)
294
295     def _get_required_packages(self):
296         """Return a list of required packages.
297
298         This is the hook where subclasses may specify a set of packages which
299         it requires to be installed.
300
301         This returns an empty list by default.
302
303         Note, subclasses should usually chain up to the base class
304         implementation of this hook.
305
306         """
307         return []
308
309     def _get_excluded_packages(self):
310         """Return a list of excluded packages.
311
312         This is the hook where subclasses may specify a set of packages which
313         it requires _not_ to be installed.
314
315         This returns an empty list by default.
316
317         Note, subclasses should usually chain up to the base class
318         implementation of this hook.
319
320         """
321         excluded_packages = []
322         for rpm_path in self._get_local_packages():
323             rpm_name = os.path.basename(rpm_path)
324             package_name = splitFilename(rpm_name)[0]
325             excluded_packages += [package_name]
326         return excluded_packages
327
328     def _get_local_packages(self):
329         """Return a list of rpm path to be local installed.
330
331         This is the hook where subclasses may specify a set of rpms which
332         it requires to be installed locally.
333
334         This returns an empty list by default.
335
336         Note, subclasses should usually chain up to the base class
337         implementation of this hook.
338
339         """
340         if self._local_pkgs_path:
341             if os.path.isdir(self._local_pkgs_path):
342                 return glob.glob(
343                         os.path.join(self._local_pkgs_path, '*.rpm'))
344             elif os.path.splitext(self._local_pkgs_path)[-1] == '.rpm':
345                 return [self._local_pkgs_path]
346
347         return []
348
349     def _get_fstab(self):
350         """Return the desired contents of /etc/fstab.
351
352         This is the hook where subclasses may specify the contents of
353         /etc/fstab by returning a string containing the desired contents.
354
355         A sensible default implementation is provided.
356
357         """
358         s =  "/dev/root  /         %s    %s 0 0\n" % (self._fstype, "defaults,noatime" if not self._fsopts else self._fsopts)
359         s += self._get_fstab_special()
360         return s
361
362     def _get_fstab_special(self):
363         s = "devpts     /dev/pts  devpts  gid=5,mode=620   0 0\n"
364         s += "tmpfs      /dev/shm  tmpfs   defaults         0 0\n"
365         s += "proc       /proc     proc    defaults         0 0\n"
366         s += "sysfs      /sys      sysfs   defaults         0 0\n"
367         return s
368
369     def _get_post_scripts_env(self, in_chroot):
370         """Return an environment dict for %post scripts.
371
372         This is the hook where subclasses may specify some environment
373         variables for %post scripts by return a dict containing the desired
374         environment.
375
376         By default, this returns an empty dict.
377
378         in_chroot -- whether this %post script is to be executed chroot()ed
379                      into _instroot.
380
381         """
382         return {}
383
384     def __get_imgname(self):
385         return self.name
386     _name = property(__get_imgname)
387     """The name of the image file.
388
389     """
390
391     def _get_kernel_versions(self):
392         """Return a dict detailing the available kernel types/versions.
393
394         This is the hook where subclasses may override what kernel types and
395         versions should be available for e.g. creating the booloader
396         configuration.
397
398         A dict should be returned mapping the available kernel types to a list
399         of the available versions for those kernels.
400
401         The default implementation uses rpm to iterate over everything
402         providing 'kernel', finds /boot/vmlinuz-* and returns the version
403         obtained from the vmlinuz filename. (This can differ from the kernel
404         RPM's n-v-r in the case of e.g. xen)
405
406         """
407         def get_version(header):
408             version = None
409             for f in header['filenames']:
410                 if f.startswith('/boot/vmlinuz-'):
411                     version = f[14:]
412             return version
413
414         ts = rpm.TransactionSet(self._instroot)
415
416         ret = {}
417         for header in ts.dbMatch('provides', 'kernel'):
418             version = get_version(header)
419             if version is None:
420                 continue
421
422             name = header['name']
423             if not name in ret:
424                 ret[name] = [version]
425             elif not version in ret[name]:
426                 ret[name].append(version)
427
428         return ret
429
430     #
431     # Helpers for subclasses
432     #
433     def _do_bindmounts(self):
434         """Mount various system directories onto _instroot.
435
436         This method is called by mount(), but may also be used by subclasses
437         in order to re-mount the bindmounts after modifying the underlying
438         filesystem.
439
440         """
441         for b in self.__bindmounts:
442             b.mount()
443
444     def _undo_bindmounts(self):
445         """Unmount the bind-mounted system directories from _instroot.
446
447         This method is usually only called by unmount(), but may also be used
448         by subclasses in order to gain access to the filesystem obscured by
449         the bindmounts - e.g. in order to create device nodes on the image
450         filesystem.
451
452         """
453         self.__bindmounts.reverse()
454         for b in self.__bindmounts:
455             b.unmount()
456
457     def _chroot(self):
458         """Chroot into the install root.
459
460         This method may be used by subclasses when executing programs inside
461         the install root e.g.
462
463           subprocess.call(["/bin/ls"], preexec_fn = self.chroot)
464
465         """
466         os.chroot(self._instroot)
467         os.chdir("/")
468
469     def _mkdtemp(self, prefix = "tmp-"):
470         """Create a temporary directory.
471
472         This method may be used by subclasses to create a temporary directory
473         for use in building the final image - e.g. a subclass might create
474         a temporary directory in order to bundle a set of files into a package.
475
476         The subclass may delete this directory if it wishes, but it will be
477         automatically deleted by cleanup().
478
479         The absolute path to the temporary directory is returned.
480
481         Note, this method should only be called after mount() has been called.
482
483         prefix -- a prefix which should be used when creating the directory;
484                   defaults to "tmp-".
485
486         """
487         self.__ensure_builddir()
488         return tempfile.mkdtemp(dir = self.__builddir, prefix = prefix)
489
490     def _mkstemp(self, prefix = "tmp-"):
491         """Create a temporary file.
492
493         This method may be used by subclasses to create a temporary file
494         for use in building the final image - e.g. a subclass might need
495         a temporary location to unpack a compressed file.
496
497         The subclass may delete this file if it wishes, but it will be
498         automatically deleted by cleanup().
499
500         A tuple containing a file descriptor (returned from os.open() and the
501         absolute path to the temporary directory is returned.
502
503         Note, this method should only be called after mount() has been called.
504
505         prefix -- a prefix which should be used when creating the file;
506                   defaults to "tmp-".
507
508         """
509         self.__ensure_builddir()
510         return tempfile.mkstemp(dir = self.__builddir, prefix = prefix)
511
512     def _mktemp(self, prefix = "tmp-"):
513         """Create a temporary file.
514
515         This method simply calls _mkstemp() and closes the returned file
516         descriptor.
517
518         The absolute path to the temporary file is returned.
519
520         Note, this method should only be called after mount() has been called.
521
522         prefix -- a prefix which should be used when creating the file;
523                   defaults to "tmp-".
524
525         """
526
527         (f, path) = self._mkstemp(prefix)
528         os.close(f)
529         return path
530
531     #
532     # Actual implementation
533     #
534     def __ensure_builddir(self):
535         if not self.__builddir is None:
536             return
537
538         try:
539             self.__builddir = tempfile.mkdtemp(dir = self.tmpdir,
540                                                prefix = "imgcreate-")
541         except OSError, (err, msg):
542             raise CreatorError("Failed create build directory in %s: %s" %
543                                (self.tmpdir, msg))
544
545     def get_cachedir(self, cachedir = None):
546         if self.cachedir:
547             return self.cachedir
548
549         self.__ensure_builddir()
550         if cachedir:
551             self.cachedir = cachedir
552         else:
553             self.cachedir = self.__builddir + "/yum-cache"
554         makedirs(self.cachedir)
555         return self.cachedir
556
557     def __sanity_check(self):
558         """Ensure that the config we've been given is sane."""
559         if not (kickstart.get_packages(self.ks) or
560                 kickstart.get_groups(self.ks)):
561             raise CreatorError("No packages or groups specified")
562
563         kickstart.convert_method_to_repo(self.ks)
564
565         if not kickstart.get_repos(self.ks):
566             raise CreatorError("No repositories specified")
567
568     def __write_fstab(self):
569         fstab = open(self._instroot + "/etc/fstab", "w")
570         fstab.write(self._get_fstab())
571         fstab.close()
572
573     def __create_minimal_dev(self):
574         """Create a minimal /dev so that we don't corrupt the host /dev"""
575         origumask = os.umask(0000)
576         devices = (('null',   1, 3, 0666),
577                    ('urandom',1, 9, 0666),
578                    ('random', 1, 8, 0666),
579                    ('full',   1, 7, 0666),
580                    ('ptmx',   5, 2, 0666),
581                    ('tty',    5, 0, 0666),
582                    ('zero',   1, 5, 0666))
583         links = (("/proc/self/fd", "/dev/fd"),
584                  ("/proc/self/fd/0", "/dev/stdin"),
585                  ("/proc/self/fd/1", "/dev/stdout"),
586                  ("/proc/self/fd/2", "/dev/stderr"))
587
588         for (node, major, minor, perm) in devices:
589             if not os.path.exists(self._instroot + "/dev/" + node):
590                 os.mknod(self._instroot + "/dev/" + node, perm | stat.S_IFCHR, os.makedev(major,minor))
591         for (src, dest) in links:
592             if not os.path.exists(self._instroot + dest):
593                 os.symlink(src, self._instroot + dest)
594         os.umask(origumask)
595
596
597     def mount(self, base_on = None, cachedir = None):
598         """Setup the target filesystem in preparation for an install.
599
600         This function sets up the filesystem which the ImageCreator will
601         install into and configure. The ImageCreator class merely creates an
602         install root directory, bind mounts some system directories (e.g. /dev)
603         and writes out /etc/fstab. Other subclasses may also e.g. create a
604         sparse file, format it and loopback mount it to the install root.
605
606         base_on -- a previous install on which to base this install; defaults
607                    to None, causing a new image to be created
608
609         cachedir -- a directory in which to store the Yum cache; defaults to
610                     None, causing a new cache to be created; by setting this
611                     to another directory, the same cache can be reused across
612                     multiple installs.
613
614         """
615         self.__ensure_builddir()
616
617         makedirs(self._instroot)
618         makedirs(self._outdir)
619
620         self._mount_instroot(base_on)
621
622         for d in ("/dev/pts", "/etc", "/boot", "/var/log", "/var/cache/yum", "/sys", "/proc", "/usr/bin"):
623             makedirs(self._instroot + d)
624
625         if self.target_arch and self.target_arch.startswith("arm"):
626             self.qemu_emulator = setup_qemu_emulator(self._instroot, self.target_arch)
627
628         self.get_cachedir(cachedir)
629
630         # bind mount system directories into _instroot
631         for (f, dest) in [("/sys", None), ("/proc", None), ("/proc/sys/fs/binfmt_misc", None),
632                           ("/dev/pts", None),
633                           (self.get_cachedir(), "/var/cache/yum")]:
634             self.__bindmounts.append(BindChrootMount(f, self._instroot, dest))
635
636
637         self._do_bindmounts()
638
639         self.__create_minimal_dev()
640
641         if os.path.exists(self._instroot + "/etc/mtab"):
642             os.unlink(self._instroot + "/etc/mtab")
643         os.symlink("../proc/mounts", self._instroot + "/etc/mtab")
644
645         self.__write_fstab()
646
647         # get size of available space in 'instroot' fs
648         self._root_fs_avail = get_filesystem_avail(self._instroot)
649
650     def unmount(self):
651         """Unmounts the target filesystem.
652
653         The ImageCreator class detaches the system from the install root, but
654         other subclasses may also detach the loopback mounted filesystem image
655         from the install root.
656
657         """
658         try:
659             os.unlink(self._instroot + "/etc/mtab")
660             if self.qemu_emulator:
661                 os.unlink(self._instroot + self.qemu_emulator)
662             """ Clean up yum garbage """
663             instroot_pdir = os.path.dirname(self._instroot + self._instroot)
664             if os.path.exists(instroot_pdir):
665                 shutil.rmtree(instroot_pdir, ignore_errors = True)
666         except OSError:
667             pass
668
669
670         self._undo_bindmounts()
671
672         self._unmount_instroot()
673
674     def cleanup(self):
675         """Unmounts the target filesystem and deletes temporary files.
676
677         This method calls unmount() and then deletes any temporary files and
678         directories that were created on the host system while building the
679         image.
680
681         Note, make sure to call this method once finished with the creator
682         instance in order to ensure no stale files are left on the host e.g.:
683
684           creator = ImageCreator(ks, name)
685           try:
686               creator.create()
687           finally:
688               creator.cleanup()
689
690         """
691         if not self.__builddir:
692             return
693
694         self.unmount()
695
696         shutil.rmtree(self.__builddir, ignore_errors = True)
697         self.__builddir = None
698
699     def __is_excluded_pkg(self, pkg):
700         if pkg in self._excluded_pkgs:
701             self._excluded_pkgs.remove(pkg)
702             return True
703
704         for xpkg in self._excluded_pkgs:
705             if xpkg.endswith('*'):
706                 if pkg.startswith(xpkg[:-1]):
707                     return True
708             elif xpkg.startswith('*'):
709                 if pkg.endswith(xpkg[1:]):
710                     return True
711
712         return None
713
714     def __select_packages(self, pkg_manager):
715         skipped_pkgs = []
716         for pkg in self._required_pkgs:
717             e = pkg_manager.selectPackage(pkg)
718             if e:
719                 if kickstart.ignore_missing(self.ks):
720                     skipped_pkgs.append(pkg)
721                 elif self.__is_excluded_pkg(pkg):
722                     skipped_pkgs.append(pkg)
723                 else:
724                     raise CreatorError("Failed to find package '%s' : %s" %
725                                        (pkg, e))
726
727         for pkg in skipped_pkgs:
728             logging.warn("Skipping missing package '%s'" % (pkg,))
729
730     def __select_groups(self, pkg_manager):
731         skipped_groups = []
732         for group in self._required_groups:
733             e = pkg_manager.selectGroup(group.name, group.include)
734             if e:
735                 if kickstart.ignore_missing(self.ks):
736                     skipped_groups.append(group)
737                 else:
738                     raise CreatorError("Failed to find group '%s' : %s" %
739                                        (group.name, e))
740
741         for group in skipped_groups:
742             logging.warn("Skipping missing group '%s'" % (group.name,))
743
744     def __deselect_packages(self, pkg_manager):
745         for pkg in self._excluded_pkgs:
746             pkg_manager.deselectPackage(pkg)
747
748     def __localinst_packages(self, pkg_manager):
749         for rpm_path in self._get_local_packages():
750             pkg_manager.installLocal(rpm_path)
751
752     def install(self, repo_urls = {}):
753         """Install packages into the install root.
754
755         This function installs the packages listed in the supplied kickstart
756         into the install root. By default, the packages are installed from the
757         repository URLs specified in the kickstart.
758
759         repo_urls -- a dict which maps a repository name to a repository URL;
760                      if supplied, this causes any repository URLs specified in
761                      the kickstart to be overridden.
762
763         """
764
765
766         # initialize pkg list to install
767         if self.ks:
768             self.__sanity_check()
769
770             self._required_pkgs = \
771                 kickstart.get_packages(self.ks, self._get_required_packages())
772             self._excluded_pkgs = \
773                 kickstart.get_excluded(self.ks, self._get_excluded_packages())
774             self._required_groups = kickstart.get_groups(self.ks)
775         else:
776             self._required_pkgs = None
777             self._excluded_pkgs = None
778             self._required_groups = None
779
780         yum_conf = self._mktemp(prefix = "yum.conf-")
781
782         keep_record = None
783         if self._include_src:
784             keep_record = 'include_src'
785         if self._recording_pkgs in ('name', 'content'):
786             keep_record = self._recording_pkgs
787
788         pkg_manager = self.get_pkg_manager(keep_record)
789         pkg_manager.setup(yum_conf, self._instroot)
790
791         for repo in kickstart.get_repos(self.ks, repo_urls):
792             (name, baseurl, mirrorlist, inc, exc, proxy, proxy_username, proxy_password, debuginfo, source, gpgkey, disable) = repo
793
794             yr = pkg_manager.addRepository(name, baseurl, mirrorlist, proxy, proxy_username, proxy_password, inc, exc)
795         
796         if kickstart.exclude_docs(self.ks):
797             rpm.addMacro("_excludedocs", "1")
798         rpm.addMacro("__file_context_path", "%{nil}")
799         if kickstart.inst_langs(self.ks) != None:
800             rpm.addMacro("_install_langs", kickstart.inst_langs(self.ks))
801
802         try:
803             try:
804                 self.__select_packages(pkg_manager)
805                 self.__select_groups(pkg_manager)
806                 self.__deselect_packages(pkg_manager)
807                 self.__localinst_packages(pkg_manager)
808
809                 BOOT_SAFEGUARD = 256L * 1024 * 1024 # 256M
810                 checksize = self._root_fs_avail
811                 if checksize:
812                     checksize -= BOOT_SAFEGUARD
813                 if self.target_arch:
814                     pkg_manager._add_prob_flags(rpm.RPMPROB_FILTER_IGNOREARCH)
815                 pkg_manager.runInstall(checksize)
816             except CreatorError, e:
817                 raise CreatorError("%s" % (e,))
818         finally:
819             if keep_record:
820                 self._pkgs_content = pkg_manager.getAllContent()
821
822             pkg_manager.closeRpmDB()
823             pkg_manager.close()
824             os.unlink(yum_conf)
825
826         # do some clean up to avoid lvm info leakage.  this sucks.
827         for subdir in ("cache", "backup", "archive"):
828             lvmdir = self._instroot + "/etc/lvm/" + subdir
829             try:
830                 for f in os.listdir(lvmdir):
831                     os.unlink(lvmdir + "/" + f)
832             except:
833                 pass
834
835     def __run_post_scripts(self):
836         print "Running scripts"
837         for s in kickstart.get_post_scripts(self.ks):
838             (fd, path) = tempfile.mkstemp(prefix = "ks-script-",
839                                           dir = self._instroot + "/tmp")
840
841             s.script = s.script.replace("\r", "")
842             os.write(fd, s.script)
843             os.close(fd)
844             os.chmod(path, 0700)
845
846             env = self._get_post_scripts_env(s.inChroot)
847
848             if not s.inChroot:
849                 env["INSTALL_ROOT"] = self._instroot
850                 env["IMG_NAME"] = self._name
851                 preexec = None
852                 script = path
853             else:
854                 preexec = self._chroot
855                 script = "/tmp/" + os.path.basename(path)
856
857             try:
858                 try:
859                     subprocess.call([s.interp, script],
860                                     preexec_fn = preexec, env = env, stdout = sys.stdout, stderr = sys.stderr)
861                 except OSError, (err, msg):
862                     raise CreatorError("Failed to execute %%post script "
863                                        "with '%s' : %s" % (s.interp, msg))
864             finally:
865                 os.unlink(path)
866
867     def __save_repo_keys(self, repodata):
868         if not repodata:
869             return None
870         gpgkeydir = "/etc/pki/rpm-gpg"
871         makedirs(self._instroot + gpgkeydir)
872         for repo in repodata:
873             if repo["repokey"]:
874                 repokey = gpgkeydir + "/RPM-GPG-KEY-%s" %  repo["name"]
875                 shutil.copy(repo["repokey"], self._instroot + repokey)
876
877     def configure(self, repodata = None):
878         """Configure the system image according to the kickstart.
879
880         This method applies the (e.g. keyboard or network) configuration
881         specified in the kickstart and executes the kickstart %post scripts.
882
883         If neccessary, it also prepares the image to be bootable by e.g.
884         creating an initrd and bootloader configuration.
885
886         """
887         ksh = self.ks.handler
888
889         try:
890             kickstart.LanguageConfig(self._instroot).apply(ksh.lang)
891             kickstart.KeyboardConfig(self._instroot).apply(ksh.keyboard)
892             kickstart.TimezoneConfig(self._instroot).apply(ksh.timezone)
893             #kickstart.AuthConfig(self._instroot).apply(ksh.authconfig)
894             kickstart.FirewallConfig(self._instroot).apply(ksh.firewall)
895             kickstart.RootPasswordConfig(self._instroot).apply(ksh.rootpw)
896             kickstart.UserConfig(self._instroot).apply(ksh.user)
897             kickstart.ServicesConfig(self._instroot).apply(ksh.services)
898             kickstart.XConfig(self._instroot).apply(ksh.xconfig)
899             kickstart.NetworkConfig(self._instroot).apply(ksh.network)
900             kickstart.RPMMacroConfig(self._instroot).apply(self.ks)
901             kickstart.DesktopConfig(self._instroot).apply(ksh.desktop)
902             self.__save_repo_keys(repodata)
903             kickstart.MoblinRepoConfig(self._instroot).apply(ksh.repo, repodata)
904         except:
905             print "Failed to apply configuration to image"
906             raise
907
908         self._create_bootconfig()
909         self.__run_post_scripts()
910
911     def launch_shell(self, launch):
912         """Launch a shell in the install root.
913
914         This method is launches a bash shell chroot()ed in the install root;
915         this can be useful for debugging.
916
917         """
918         if launch:
919             print "Launching shell. Exit to continue."
920             print "----------------------------------"
921             subprocess.call(["/bin/bash"], preexec_fn = self._chroot)
922
923     def do_genchecksum(self, image_name):
924         if not self._genchecksum:
925             return
926
927         """ Generate md5sum if /usr/bin/md5sum is available """
928         if os.path.exists("/usr/bin/md5sum"):
929             p = subprocess.Popen(["/usr/bin/md5sum", "-b", image_name],
930                                  stdout=subprocess.PIPE)
931             (md5sum, errorstr) = p.communicate()
932             if p.returncode != 0:
933                 logging.warning("Can't generate md5sum for image %s" % image_name)
934             else:
935                 pattern = re.compile("\*.*$")
936                 md5sum = pattern.sub("*" + os.path.basename(image_name), md5sum)
937                 fd = open(image_name + ".md5sum", "w")
938                 fd.write(md5sum)
939                 fd.close()
940                 self.outimage.append(image_name+".md5sum")
941
942     def package(self, destdir = "."):
943         """Prepares the created image for final delivery.
944
945         In its simplest form, this method merely copies the install root to the
946         supplied destination directory; other subclasses may choose to package
947         the image by e.g. creating a bootable ISO containing the image and
948         bootloader configuration.
949
950         destdir -- the directory into which the final image should be moved;
951                    this defaults to the current directory.
952
953         """
954         self._stage_final_image()
955
956         if self.__img_compression_method:
957             if not self._img_name:
958                 raise CreatorError("Image name not set.")
959             rc = None
960             img_location = os.path.join(self._outdir,self._img_name)
961             if self.__img_compression_method == "bz2":
962                 bzip2 = find_binary_path('bzip2')
963                 print "Compressing %s with bzip2. Please wait..." % img_location
964                 rc = subprocess.call([bzip2, "-f", img_location])
965                 if rc:
966                     raise CreatorError("Failed to compress image %s with %s." % (img_location, self.__img_compression_method))
967                 for bootimg in glob.glob(os.path.dirname(img_location) + "/*-boot.bin"):
968                     print "Compressing %s with bzip2. Please wait..." % bootimg
969                     rc = subprocess.call([bzip2, "-f", bootimg])
970                     if rc:
971                         raise CreatorError("Failed to compress image %s with %s." % (bootimg, self.__img_compression_method))
972
973         if self._recording_pkgs:
974             self._save_recording_pkgs(destdir)
975
976         """ For image formats with two or multiple image files, it will be better to put them under a directory """
977         if self.image_format in ("raw", "vmdk", "vdi", "nand", "mrstnand"):
978             destdir = os.path.join(destdir, "%s-%s" % (self.name, self.image_format))
979             logging.debug("creating destination dir: %s" % destdir)
980             makedirs(destdir)
981
982         # Ensure all data is flushed to _outdir
983         synccmd = find_binary_path("sync")
984         subprocess.call([synccmd])
985
986         for f in os.listdir(self._outdir):
987             shutil.move(os.path.join(self._outdir, f),
988                         os.path.join(destdir, f))
989             self.outimage.append(os.path.join(destdir, f))
990             self.do_genchecksum(os.path.join(destdir, f))
991
992     def create(self):
993         """Install, configure and package an image.
994
995         This method is a utility method which creates and image by calling some
996         of the other methods in the following order - mount(), install(),
997         configure(), unmount and package().
998
999         """
1000         self.mount(None, self.cachedir)
1001         self.install()
1002         self.configure(self.repometadata)
1003         self.unmount()
1004         self.package(self.destdir)
1005
1006     def print_outimage_info(self):
1007         print "Your new image can be found here:"
1008         self.outimage.sort()
1009         for file in self.outimage:
1010             print os.path.abspath(file)
1011
1012     def check_depend_tools(self):
1013         for tool in self._dep_checks:
1014             find_binary_path(tool)
1015
1016     def package_output(self, image_format, destdir = ".", package="none"):
1017         if not package or package == "none":
1018             return
1019
1020         destdir = os.path.abspath(os.path.expanduser(destdir))
1021         (pkg, comp) = os.path.splitext(package)
1022         if comp:
1023             comp=comp.lstrip(".")
1024
1025         if pkg == "tar":
1026             if comp:
1027                 dst = "%s/%s-%s.tar.%s" % (destdir, self.name, image_format, comp)
1028             else:
1029                 dst = "%s/%s-%s.tar" % (destdir, self.name, image_format)
1030             print "creating %s" % dst
1031             tar = tarfile.open(dst, "w:" + comp)
1032
1033             for file in self.outimage:
1034                 print "adding %s to %s" % (file, dst)
1035                 tar.add(file, arcname=os.path.join("%s-%s" % (self.name, image_format), os.path.basename(file)))
1036                 if os.path.isdir(file):
1037                     shutil.rmtree(file, ignore_errors = True)
1038                 else:
1039                     os.remove(file)
1040
1041
1042             tar.close()
1043
1044             '''All the file in outimage has been packaged into tar.* file'''
1045             self.outimage = [dst]
1046
1047     def release_output(self, config, destdir, name, release):
1048         self.outimage = create_release(config, destdir, name, self.outimage, release)
1049
1050     def save_kernel(self, destdir):
1051         if not os.path.exists(destdir):
1052             makedirs(destdir)
1053         for kernel in glob.glob("%s/boot/vmlinuz-*" % self._instroot):
1054             kernelfilename = "%s/%s-%s" % (destdir, self.name, os.path.basename(kernel))
1055             shutil.copy(kernel, kernelfilename)
1056             self.outimage.append(kernelfilename)
1057
1058     def compress_disk_image(self, compression_method):
1059         """
1060         With this you can set the method that is used to compress the disk
1061         image after it is created.
1062         """
1063
1064         if compression_method not in ('bz2'):
1065             raise CreatorError("Given disk image compression method ('%s') is not valid." % (compression_method))
1066
1067         self.__img_compression_method = compression_method
1068
1069     def get_pkg_manager(self, recording_pkgs=None):
1070         return self.pkgmgr(creator = self, recording_pkgs = recording_pkgs)
1071
1072