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