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