change naming for release option
[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, grabber, runner, 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         self.pack_to = None
78         self.repourl = {}
79
80         # If the kernel is save to the destdir when copy_kernel cmd is called.
81         self._need_copy_kernel = False
82
83         if createopts:
84             # Mapping table for variables that have different names.
85             optmap = {"pkgmgr" : "pkgmgr_name",
86                       "outdir" : "destdir",
87                       "arch" : "target_arch",
88                       "local_pkgs_path" : "_local_pkgs_path",
89                       "copy_kernel" : "_need_copy_kernel",
90                      }
91
92             # update setting from createopts
93             for key in createopts.keys():
94                 if key in optmap:
95                     option = optmap[key]
96                 else:
97                     option = key
98                 setattr(self, option, createopts[key])
99
100             self.destdir = os.path.abspath(os.path.expanduser(self.destdir))
101
102             if 'release' in createopts and createopts['release']:
103                 self.name = createopts['release'] + '_' + self.name
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"]
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 necessary tools
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
278         content = None
279         if 'vcs' in self._recording_pkgs:
280             vcslst = ["%s %s" % (k, v) for (k, v) in self._pkgs_vcsinfo.items()]
281             content = '\n'.join(sorted(vcslst))
282         elif 'name' in self._recording_pkgs:
283             content = '\n'.join(pkgs)
284         if content:
285             namefile = os.path.join(destdir, self.name + '.packages')
286             f = open(namefile, "w")
287             f.write(content)
288             f.close()
289             self.outimage.append(namefile);
290
291         # if 'content', save more details
292         if 'content' in self._recording_pkgs:
293             contfile = os.path.join(destdir, self.name + '.files')
294             f = open(contfile, "w")
295
296             for pkg in pkgs:
297                 content = pkg + '\n'
298
299                 pkgcont = self._pkgs_content[pkg]
300                 content += '    '
301                 content += '\n    '.join(pkgcont)
302                 content += '\n'
303
304                 content += '\n'
305                 f.write(content)
306             f.close()
307             self.outimage.append(contfile)
308
309         if 'license' in self._recording_pkgs:
310             licensefile = os.path.join(destdir, self.name + '.license')
311             f = open(licensefile, "w")
312
313             f.write('Summary:\n')
314             for license in reversed(sorted(self._pkgs_license, key=\
315                             lambda license: len(self._pkgs_license[license]))):
316                 f.write("    - %s: %s\n" \
317                         % (license, len(self._pkgs_license[license])))
318
319             f.write('\nDetails:\n')
320             for license in reversed(sorted(self._pkgs_license, key=\
321                             lambda license: len(self._pkgs_license[license]))):
322                 f.write("    - %s:\n" % (license))
323                 for pkg in sorted(self._pkgs_license[license]):
324                     f.write("        - %s\n" % (pkg))
325                 f.write('\n')
326
327             f.close()
328             self.outimage.append(licensefile)
329
330     def _get_required_packages(self):
331         """Return a list of required packages.
332
333         This is the hook where subclasses may specify a set of packages which
334         it requires to be installed.
335
336         This returns an empty list by default.
337
338         Note, subclasses should usually chain up to the base class
339         implementation of this hook.
340
341         """
342         return []
343
344     def _get_excluded_packages(self):
345         """Return a list of excluded packages.
346
347         This is the hook where subclasses may specify a set of packages which
348         it requires _not_ to be installed.
349
350         This returns an empty list by default.
351
352         Note, subclasses should usually chain up to the base class
353         implementation of this hook.
354
355         """
356         return []
357
358     def _get_local_packages(self):
359         """Return a list of rpm path to be local installed.
360
361         This is the hook where subclasses may specify a set of rpms which
362         it requires to be installed locally.
363
364         This returns an empty list by default.
365
366         Note, subclasses should usually chain up to the base class
367         implementation of this hook.
368
369         """
370         if self._local_pkgs_path:
371             if os.path.isdir(self._local_pkgs_path):
372                 return glob.glob(
373                         os.path.join(self._local_pkgs_path, '*.rpm'))
374             elif os.path.splitext(self._local_pkgs_path)[-1] == '.rpm':
375                 return [self._local_pkgs_path]
376
377         return []
378
379     def _get_fstab(self):
380         """Return the desired contents of /etc/fstab.
381
382         This is the hook where subclasses may specify the contents of
383         /etc/fstab by returning a string containing the desired contents.
384
385         A sensible default implementation is provided.
386
387         """
388         s =  "/dev/root  /         %s    %s 0 0\n" \
389              % (self._fstype,
390                 "defaults,noatime" if not self._fsopts else self._fsopts)
391         s += self._get_fstab_special()
392         return s
393
394     def _get_fstab_special(self):
395         s = "devpts     /dev/pts  devpts  gid=5,mode=620   0 0\n"
396         s += "tmpfs      /dev/shm  tmpfs   defaults         0 0\n"
397         s += "proc       /proc     proc    defaults         0 0\n"
398         s += "sysfs      /sys      sysfs   defaults         0 0\n"
399         return s
400
401     def _get_post_scripts_env(self, in_chroot):
402         """Return an environment dict for %post scripts.
403
404         This is the hook where subclasses may specify some environment
405         variables for %post scripts by return a dict containing the desired
406         environment.
407
408         By default, this returns an empty dict.
409
410         in_chroot -- whether this %post script is to be executed chroot()ed
411                      into _instroot.
412
413         """
414         return {}
415
416     def __get_imgname(self):
417         return self.name
418     _name = property(__get_imgname)
419     """The name of the image file.
420
421     """
422
423     def _get_kernel_versions(self):
424         """Return a dict detailing the available kernel types/versions.
425
426         This is the hook where subclasses may override what kernel types and
427         versions should be available for e.g. creating the booloader
428         configuration.
429
430         A dict should be returned mapping the available kernel types to a list
431         of the available versions for those kernels.
432
433         The default implementation uses rpm to iterate over everything
434         providing 'kernel', finds /boot/vmlinuz-* and returns the version
435         obtained from the vmlinuz filename. (This can differ from the kernel
436         RPM's n-v-r in the case of e.g. xen)
437
438         """
439         def get_kernel_versions(instroot):
440             ret = {}
441             versions = set()
442             files = glob.glob(instroot + "/boot/vmlinuz-*")
443             for file in files:
444                 version = os.path.basename(file)[8:]
445                 if version is None:
446                     continue
447                 versions.add(version)
448             ret["kernel"] = list(versions)
449             return ret
450
451         def get_version(header):
452             version = None
453             for f in header['filenames']:
454                 if f.startswith('/boot/vmlinuz-'):
455                     version = f[14:]
456             return version
457
458         if self.ks is None:
459             return get_kernel_versions(self._instroot)
460
461         ts = rpm.TransactionSet(self._instroot)
462
463         ret = {}
464         for header in ts.dbMatch('provides', 'kernel'):
465             version = get_version(header)
466             if version is None:
467                 continue
468
469             name = header['name']
470             if not name in ret:
471                 ret[name] = [version]
472             elif not version in ret[name]:
473                 ret[name].append(version)
474
475         return ret
476
477
478     #
479     # Helpers for subclasses
480     #
481     def _do_bindmounts(self):
482         """Mount various system directories onto _instroot.
483
484         This method is called by mount(), but may also be used by subclasses
485         in order to re-mount the bindmounts after modifying the underlying
486         filesystem.
487
488         """
489         for b in self.__bindmounts:
490             b.mount()
491
492     def _undo_bindmounts(self):
493         """Unmount the bind-mounted system directories from _instroot.
494
495         This method is usually only called by unmount(), but may also be used
496         by subclasses in order to gain access to the filesystem obscured by
497         the bindmounts - e.g. in order to create device nodes on the image
498         filesystem.
499
500         """
501         self.__bindmounts.reverse()
502         for b in self.__bindmounts:
503             b.unmount()
504
505     def _chroot(self):
506         """Chroot into the install root.
507
508         This method may be used by subclasses when executing programs inside
509         the install root e.g.
510
511           subprocess.call(["/bin/ls"], preexec_fn = self.chroot)
512
513         """
514         os.chroot(self._instroot)
515         os.chdir("/")
516
517     def _mkdtemp(self, prefix = "tmp-"):
518         """Create a temporary directory.
519
520         This method may be used by subclasses to create a temporary directory
521         for use in building the final image - e.g. a subclass might create
522         a temporary directory in order to bundle a set of files into a package.
523
524         The subclass may delete this directory if it wishes, but it will be
525         automatically deleted by cleanup().
526
527         The absolute path to the temporary directory is returned.
528
529         Note, this method should only be called after mount() has been called.
530
531         prefix -- a prefix which should be used when creating the directory;
532                   defaults to "tmp-".
533
534         """
535         self.__ensure_builddir()
536         return tempfile.mkdtemp(dir = self.__builddir, prefix = prefix)
537
538     def _mkstemp(self, prefix = "tmp-"):
539         """Create a temporary file.
540
541         This method may be used by subclasses to create a temporary file
542         for use in building the final image - e.g. a subclass might need
543         a temporary location to unpack a compressed file.
544
545         The subclass may delete this file if it wishes, but it will be
546         automatically deleted by cleanup().
547
548         A tuple containing a file descriptor (returned from os.open() and the
549         absolute path to the temporary directory is returned.
550
551         Note, this method should only be called after mount() has been called.
552
553         prefix -- a prefix which should be used when creating the file;
554                   defaults to "tmp-".
555
556         """
557         self.__ensure_builddir()
558         return tempfile.mkstemp(dir = self.__builddir, prefix = prefix)
559
560     def _mktemp(self, prefix = "tmp-"):
561         """Create a temporary file.
562
563         This method simply calls _mkstemp() and closes the returned file
564         descriptor.
565
566         The absolute path to the temporary file is returned.
567
568         Note, this method should only be called after mount() has been called.
569
570         prefix -- a prefix which should be used when creating the file;
571                   defaults to "tmp-".
572
573         """
574
575         (f, path) = self._mkstemp(prefix)
576         os.close(f)
577         return path
578
579
580     #
581     # Actual implementation
582     #
583     def __ensure_builddir(self):
584         if not self.__builddir is None:
585             return
586
587         try:
588             self.__builddir = tempfile.mkdtemp(dir = self.tmpdir,
589                                                prefix = "imgcreate-")
590         except OSError, (err, msg):
591             raise CreatorError("Failed create build directory in %s: %s" %
592                                (self.tmpdir, msg))
593
594     def get_cachedir(self, cachedir = None):
595         if self.cachedir:
596             return self.cachedir
597
598         self.__ensure_builddir()
599         if cachedir:
600             self.cachedir = cachedir
601         else:
602             self.cachedir = self.__builddir + "/mic-cache"
603         fs.makedirs(self.cachedir)
604         return self.cachedir
605
606     def __sanity_check(self):
607         """Ensure that the config we've been given is sane."""
608         if not (kickstart.get_packages(self.ks) or
609                 kickstart.get_groups(self.ks)):
610             raise CreatorError("No packages or groups specified")
611
612         kickstart.convert_method_to_repo(self.ks)
613
614         if not kickstart.get_repos(self.ks):
615             raise CreatorError("No repositories specified")
616
617     def __write_fstab(self):
618         fstab = open(self._instroot + "/etc/fstab", "w")
619         fstab.write(self._get_fstab())
620         fstab.close()
621
622     def __create_minimal_dev(self):
623         """Create a minimal /dev so that we don't corrupt the host /dev"""
624         origumask = os.umask(0000)
625         devices = (('null',   1, 3, 0666),
626                    ('urandom',1, 9, 0666),
627                    ('random', 1, 8, 0666),
628                    ('full',   1, 7, 0666),
629                    ('ptmx',   5, 2, 0666),
630                    ('tty',    5, 0, 0666),
631                    ('zero',   1, 5, 0666))
632
633         links = (("/proc/self/fd", "/dev/fd"),
634                  ("/proc/self/fd/0", "/dev/stdin"),
635                  ("/proc/self/fd/1", "/dev/stdout"),
636                  ("/proc/self/fd/2", "/dev/stderr"))
637
638         for (node, major, minor, perm) in devices:
639             if not os.path.exists(self._instroot + "/dev/" + node):
640                 os.mknod(self._instroot + "/dev/" + node,
641                          perm | stat.S_IFCHR,
642                          os.makedev(major,minor))
643
644         for (src, dest) in links:
645             if not os.path.exists(self._instroot + dest):
646                 os.symlink(src, self._instroot + dest)
647
648         os.umask(origumask)
649
650     def mount(self, base_on = None, cachedir = None):
651         """Setup the target filesystem in preparation for an install.
652
653         This function sets up the filesystem which the ImageCreator will
654         install into and configure. The ImageCreator class merely creates an
655         install root directory, bind mounts some system directories (e.g. /dev)
656         and writes out /etc/fstab. Other subclasses may also e.g. create a
657         sparse file, format it and loopback mount it to the install root.
658
659         base_on -- a previous install on which to base this install; defaults
660                    to None, causing a new image to be created
661
662         cachedir -- a directory in which to store the Yum cache; defaults to
663                     None, causing a new cache to be created; by setting this
664                     to another directory, the same cache can be reused across
665                     multiple installs.
666
667         """
668         self.__ensure_builddir()
669
670         # prevent popup dialog in Ubuntu(s)
671         misc.hide_loopdev_presentation()
672
673         fs.makedirs(self._instroot)
674         fs.makedirs(self._outdir)
675
676         self._mount_instroot(base_on)
677
678         for d in ("/dev/pts",
679                   "/etc",
680                   "/boot",
681                   "/var/log",
682                   "/sys",
683                   "/proc",
684                   "/usr/bin"):
685             fs.makedirs(self._instroot + d)
686
687         if self.target_arch and self.target_arch.startswith("arm"):
688             self.qemu_emulator = misc.setup_qemu_emulator(self._instroot,
689                                                           self.target_arch)
690
691         self.get_cachedir(cachedir)
692
693         # bind mount system directories into _instroot
694         for (f, dest) in [("/sys", None),
695                           ("/proc", None),
696                           ("/proc/sys/fs/binfmt_misc", None),
697                           ("/dev/pts", None)]:
698             self.__bindmounts.append(
699                     fs.BindChrootMount(
700                         f, self._instroot, dest))
701
702         self._do_bindmounts()
703
704         self.__create_minimal_dev()
705
706         if os.path.exists(self._instroot + "/etc/mtab"):
707             os.unlink(self._instroot + "/etc/mtab")
708         os.symlink("../proc/mounts", self._instroot + "/etc/mtab")
709
710         self.__write_fstab()
711
712         # get size of available space in 'instroot' fs
713         self._root_fs_avail = misc.get_filesystem_avail(self._instroot)
714
715     def unmount(self):
716         """Unmounts the target filesystem.
717
718         The ImageCreator class detaches the system from the install root, but
719         other subclasses may also detach the loopback mounted filesystem image
720         from the install root.
721
722         """
723         try:
724             mtab = self._instroot + "/etc/mtab"
725             if not os.path.islink(mtab):
726                 os.unlink(self._instroot + "/etc/mtab")
727
728             if self.qemu_emulator:
729                 os.unlink(self._instroot + self.qemu_emulator)
730         except OSError:
731             pass
732
733         self._undo_bindmounts()
734
735         """ Clean up yum garbage """
736         try:
737             instroot_pdir = os.path.dirname(self._instroot + self._instroot)
738             if os.path.exists(instroot_pdir):
739                 shutil.rmtree(instroot_pdir, ignore_errors = True)
740             yumlibdir = self._instroot + "/var/lib/yum"
741             if os.path.exists(yumlibdir):
742                 shutil.rmtree(yumlibdir, ignore_errors = True)
743         except OSError:
744             pass
745
746         self._unmount_instroot()
747
748         # reset settings of popup dialog in Ubuntu(s)
749         misc.unhide_loopdev_presentation()
750
751     def cleanup(self):
752         """Unmounts the target filesystem and deletes temporary files.
753
754         This method calls unmount() and then deletes any temporary files and
755         directories that were created on the host system while building the
756         image.
757
758         Note, make sure to call this method once finished with the creator
759         instance in order to ensure no stale files are left on the host e.g.:
760
761           creator = ImageCreator(ks, name)
762           try:
763               creator.create()
764           finally:
765               creator.cleanup()
766
767         """
768         if not self.__builddir:
769             return
770
771         self.unmount()
772
773         shutil.rmtree(self.__builddir, ignore_errors = True)
774         self.__builddir = None
775
776     def __is_excluded_pkg(self, pkg):
777         if pkg in self._excluded_pkgs:
778             self._excluded_pkgs.remove(pkg)
779             return True
780
781         for xpkg in self._excluded_pkgs:
782             if xpkg.endswith('*'):
783                 if pkg.startswith(xpkg[:-1]):
784                     return True
785             elif xpkg.startswith('*'):
786                 if pkg.endswith(xpkg[1:]):
787                     return True
788
789         return None
790
791     def __select_packages(self, pkg_manager):
792         skipped_pkgs = []
793         for pkg in self._required_pkgs:
794             e = pkg_manager.selectPackage(pkg)
795             if e:
796                 if kickstart.ignore_missing(self.ks):
797                     skipped_pkgs.append(pkg)
798                 elif self.__is_excluded_pkg(pkg):
799                     skipped_pkgs.append(pkg)
800                 else:
801                     raise CreatorError("Failed to find package '%s' : %s" %
802                                        (pkg, e))
803
804         for pkg in skipped_pkgs:
805             msger.warning("Skipping missing package '%s'" % (pkg,))
806
807     def __select_groups(self, pkg_manager):
808         skipped_groups = []
809         for group in self._required_groups:
810             e = pkg_manager.selectGroup(group.name, group.include)
811             if e:
812                 if kickstart.ignore_missing(self.ks):
813                     skipped_groups.append(group)
814                 else:
815                     raise CreatorError("Failed to find group '%s' : %s" %
816                                        (group.name, e))
817
818         for group in skipped_groups:
819             msger.warning("Skipping missing group '%s'" % (group.name,))
820
821     def __deselect_packages(self, pkg_manager):
822         for pkg in self._excluded_pkgs:
823             pkg_manager.deselectPackage(pkg)
824
825     def __localinst_packages(self, pkg_manager):
826         for rpm_path in self._get_local_packages():
827             pkg_manager.installLocal(rpm_path)
828
829     def __preinstall_packages(self, pkg_manager):
830         if not self.ks:
831             return
832
833         self._preinstall_pkgs = kickstart.get_pre_packages(self.ks)
834         for pkg in self._preinstall_pkgs:
835             pkg_manager.preInstall(pkg)
836
837     def __attachment_packages(self, pkg_manager):
838         if not self.ks:
839             return
840
841         self._attachment = []
842         for item in kickstart.get_attachment(self.ks):
843             if item.startswith('/'):
844                 fpaths = os.path.join(self._instroot, item.lstrip('/'))
845                 for fpath in glob.glob(fpaths):
846                     self._attachment.append(fpath)
847                 continue
848
849             filelist = pkg_manager.getFilelist(item)
850             if filelist:
851                 # found rpm in rootfs
852                 for pfile in pkg_manager.getFilelist(item):
853                     fpath = os.path.join(self._instroot, pfile.lstrip('/'))
854                     self._attachment.append(fpath)
855                 continue
856
857             # try to retrieve rpm file
858             (url, proxies) = pkg_manager.package_url(item)
859             if not url:
860                 msger.warning("Can't get url from repo for %s" % item)
861                 continue
862             fpath = os.path.join(self.cachedir, os.path.basename(url))
863             if not os.path.exists(fpath):
864                 # download pkgs
865                 try:
866                     fpath = grabber.myurlgrab(url, fpath, proxies, None)
867                 except CreatorError:
868                     raise
869
870             tmpdir = self._mkdtemp()
871             misc.extract_rpm(fpath, tmpdir)
872             for (root, dirs, files) in os.walk(tmpdir):
873                 for fname in files:
874                     fpath = os.path.join(root, fname)
875                     self._attachment.append(fpath)
876
877     def install(self, repo_urls=None):
878         """Install packages into the install root.
879
880         This function installs the packages listed in the supplied kickstart
881         into the install root. By default, the packages are installed from the
882         repository URLs specified in the kickstart.
883
884         repo_urls -- a dict which maps a repository name to a repository URL;
885                      if supplied, this causes any repository URLs specified in
886                      the kickstart to be overridden.
887
888         """
889
890         # initialize pkg list to install
891         if self.ks:
892             self.__sanity_check()
893
894             self._required_pkgs = \
895                 kickstart.get_packages(self.ks, self._get_required_packages())
896             self._excluded_pkgs = \
897                 kickstart.get_excluded(self.ks, self._get_excluded_packages())
898             self._required_groups = kickstart.get_groups(self.ks)
899         else:
900             self._required_pkgs = None
901             self._excluded_pkgs = None
902             self._required_groups = None
903
904         pkg_manager = self.get_pkg_manager()
905         pkg_manager.setup()
906
907         if hasattr(self, 'install_pkgs') and self.install_pkgs:
908             if 'debuginfo' in self.install_pkgs:
909                 pkg_manager.install_debuginfo = True
910
911         for repo in kickstart.get_repos(self.ks, repo_urls):
912             (name, baseurl, mirrorlist, inc, exc,
913              proxy, proxy_username, proxy_password, debuginfo,
914              source, gpgkey, disable, ssl_verify, nocache,
915              cost, priority) = repo
916
917             yr = pkg_manager.addRepository(name, baseurl, mirrorlist, proxy,
918                         proxy_username, proxy_password, inc, exc, ssl_verify,
919                         nocache, cost, priority)
920
921         if kickstart.exclude_docs(self.ks):
922             rpm.addMacro("_excludedocs", "1")
923         rpm.addMacro("_dbpath", "/var/lib/rpm")
924         rpm.addMacro("__file_context_path", "%{nil}")
925         if kickstart.inst_langs(self.ks) != None:
926             rpm.addMacro("_install_langs", kickstart.inst_langs(self.ks))
927
928         try:
929             self.__preinstall_packages(pkg_manager)
930             self.__select_packages(pkg_manager)
931             self.__select_groups(pkg_manager)
932             self.__deselect_packages(pkg_manager)
933             self.__localinst_packages(pkg_manager)
934
935             BOOT_SAFEGUARD = 256L * 1024 * 1024 # 256M
936             checksize = self._root_fs_avail
937             if checksize:
938                 checksize -= BOOT_SAFEGUARD
939             if self.target_arch:
940                 pkg_manager._add_prob_flags(rpm.RPMPROB_FILTER_IGNOREARCH)
941             pkg_manager.runInstall(checksize)
942         except CreatorError, e:
943             raise
944         except  KeyboardInterrupt:
945             raise
946         else:
947             self._pkgs_content = pkg_manager.getAllContent()
948             self._pkgs_license = pkg_manager.getPkgsLicense()
949             self._pkgs_vcsinfo = pkg_manager.getVcsInfo()
950             self.__attachment_packages(pkg_manager)
951         finally:
952             pkg_manager.close()
953
954         # hook post install
955         self.postinstall()
956
957         # do some clean up to avoid lvm info leakage.  this sucks.
958         for subdir in ("cache", "backup", "archive"):
959             lvmdir = self._instroot + "/etc/lvm/" + subdir
960             try:
961                 for f in os.listdir(lvmdir):
962                     os.unlink(lvmdir + "/" + f)
963             except:
964                 pass
965
966     def postinstall(self):
967         self.copy_attachment()
968
969     def __run_post_scripts(self):
970         msger.info("Running scripts ...")
971         if os.path.exists(self._instroot + "/tmp"):
972             shutil.rmtree(self._instroot + "/tmp")
973         os.mkdir (self._instroot + "/tmp", 0755)
974         for s in kickstart.get_post_scripts(self.ks):
975             (fd, path) = tempfile.mkstemp(prefix = "ks-script-",
976                                           dir = self._instroot + "/tmp")
977
978             s.script = s.script.replace("\r", "")
979             os.write(fd, s.script)
980             os.close(fd)
981             os.chmod(path, 0700)
982
983             env = self._get_post_scripts_env(s.inChroot)
984
985             if not s.inChroot:
986                 env["INSTALL_ROOT"] = self._instroot
987                 env["IMG_NAME"] = self._name
988                 preexec = None
989                 script = path
990             else:
991                 preexec = self._chroot
992                 script = "/tmp/" + os.path.basename(path)
993
994             try:
995                 try:
996                     subprocess.call([s.interp, script],
997                                     preexec_fn = preexec,
998                                     env = env,
999                                     stdout = sys.stdout,
1000                                     stderr = sys.stderr)
1001                 except OSError, (err, msg):
1002                     raise CreatorError("Failed to execute %%post script "
1003                                        "with '%s' : %s" % (s.interp, msg))
1004             finally:
1005                 os.unlink(path)
1006
1007     def __save_repo_keys(self, repodata):
1008         if not repodata:
1009             return None
1010
1011         gpgkeydir = "/etc/pki/rpm-gpg"
1012         fs.makedirs(self._instroot + gpgkeydir)
1013         for repo in repodata:
1014             if repo["repokey"]:
1015                 repokey = gpgkeydir + "/RPM-GPG-KEY-%s" %  repo["name"]
1016                 shutil.copy(repo["repokey"], self._instroot + repokey)
1017
1018     def configure(self, repodata = None):
1019         """Configure the system image according to the kickstart.
1020
1021         This method applies the (e.g. keyboard or network) configuration
1022         specified in the kickstart and executes the kickstart %post scripts.
1023
1024         If necessary, it also prepares the image to be bootable by e.g.
1025         creating an initrd and bootloader configuration.
1026
1027         """
1028         ksh = self.ks.handler
1029
1030         msger.info('Applying configurations ...')
1031         try:
1032             kickstart.LanguageConfig(self._instroot).apply(ksh.lang)
1033             kickstart.KeyboardConfig(self._instroot).apply(ksh.keyboard)
1034             kickstart.TimezoneConfig(self._instroot).apply(ksh.timezone)
1035             #kickstart.AuthConfig(self._instroot).apply(ksh.authconfig)
1036             kickstart.FirewallConfig(self._instroot).apply(ksh.firewall)
1037             kickstart.RootPasswordConfig(self._instroot).apply(ksh.rootpw)
1038             kickstart.UserConfig(self._instroot).apply(ksh.user)
1039             kickstart.ServicesConfig(self._instroot).apply(ksh.services)
1040             kickstart.XConfig(self._instroot).apply(ksh.xconfig)
1041             kickstart.NetworkConfig(self._instroot).apply(ksh.network)
1042             kickstart.RPMMacroConfig(self._instroot).apply(self.ks)
1043             kickstart.DesktopConfig(self._instroot).apply(ksh.desktop)
1044             self.__save_repo_keys(repodata)
1045             kickstart.MoblinRepoConfig(self._instroot).apply(ksh.repo, repodata, self.repourl)
1046         except:
1047             msger.warning("Failed to apply configuration to image")
1048             raise
1049
1050         self._create_bootconfig()
1051         self.__run_post_scripts()
1052
1053     def launch_shell(self, launch):
1054         """Launch a shell in the install root.
1055
1056         This method is launches a bash shell chroot()ed in the install root;
1057         this can be useful for debugging.
1058
1059         """
1060         if launch:
1061             msger.info("Launching shell. Exit to continue.")
1062             subprocess.call(["/bin/bash"], preexec_fn = self._chroot)
1063
1064     def do_genchecksum(self, image_name):
1065         if not self._genchecksum:
1066             return
1067
1068         md5sum = misc.get_md5sum(image_name)
1069         with open(image_name + ".md5sum", "w") as f:
1070             f.write("%s %s" % (md5sum, os.path.basename(image_name)))
1071         self.outimage.append(image_name+".md5sum")
1072
1073     def package(self, destdir = "."):
1074         """Prepares the created image for final delivery.
1075
1076         In its simplest form, this method merely copies the install root to the
1077         supplied destination directory; other subclasses may choose to package
1078         the image by e.g. creating a bootable ISO containing the image and
1079         bootloader configuration.
1080
1081         destdir -- the directory into which the final image should be moved;
1082                    this defaults to the current directory.
1083
1084         """
1085         self._stage_final_image()
1086
1087         if not os.path.exists(destdir):
1088             fs.makedirs(destdir)
1089
1090         if self._recording_pkgs:
1091             self._save_recording_pkgs(destdir)
1092
1093         # For image formats with two or multiple image files, it will be
1094         # better to put them under a directory
1095         if self.image_format in ("raw", "vmdk", "vdi", "nand", "mrstnand"):
1096             destdir = os.path.join(destdir, "%s-%s" \
1097                                             % (self.name, self.image_format))
1098             msger.debug("creating destination dir: %s" % destdir)
1099             fs.makedirs(destdir)
1100
1101         # Ensure all data is flushed to _outdir
1102         runner.quiet('sync')
1103
1104         misc.check_space_pre_cp(self._outdir, destdir)
1105         for f in os.listdir(self._outdir):
1106             shutil.move(os.path.join(self._outdir, f),
1107                         os.path.join(destdir, f))
1108             self.outimage.append(os.path.join(destdir, f))
1109             self.do_genchecksum(os.path.join(destdir, f))
1110
1111     def print_outimage_info(self):
1112         msg = "The new image can be found here:\n"
1113         self.outimage.sort()
1114         for file in self.outimage:
1115             msg += '  %s\n' % os.path.abspath(file)
1116
1117         msger.info(msg)
1118
1119     def check_depend_tools(self):
1120         for tool in self._dep_checks:
1121             fs.find_binary_path(tool)
1122
1123     def package_output(self, image_format, destdir = ".", package="none"):
1124         if not package or package == "none":
1125             return
1126
1127         destdir = os.path.abspath(os.path.expanduser(destdir))
1128         (pkg, comp) = os.path.splitext(package)
1129         if comp:
1130             comp=comp.lstrip(".")
1131
1132         if pkg == "tar":
1133             if comp:
1134                 dst = "%s/%s-%s.tar.%s" %\
1135                       (destdir, self.name, image_format, comp)
1136             else:
1137                 dst = "%s/%s-%s.tar" %\
1138                       (destdir, self.name, image_format)
1139
1140             msger.info("creating %s" % dst)
1141             tar = tarfile.open(dst, "w:" + comp)
1142
1143             for file in self.outimage:
1144                 msger.info("adding %s to %s" % (file, dst))
1145                 tar.add(file,
1146                         arcname=os.path.join("%s-%s" \
1147                                            % (self.name, image_format),
1148                                               os.path.basename(file)))
1149                 if os.path.isdir(file):
1150                     shutil.rmtree(file, ignore_errors = True)
1151                 else:
1152                     os.remove(file)
1153
1154             tar.close()
1155
1156             '''All the file in outimage has been packaged into tar.* file'''
1157             self.outimage = [dst]
1158
1159     def release_output(self, config, destdir, release):
1160         """ Create release directory and files
1161         """
1162
1163         def _rpath(fn):
1164             """ release path """
1165             return os.path.join(destdir, fn)
1166
1167         outimages = self.outimage
1168
1169         # new ks
1170         new_kspath = _rpath(self.name+'.ks')
1171         with open(config) as fr:
1172             with open(new_kspath, "w") as wf:
1173                 # When building a release we want to make sure the .ks
1174                 # file generates the same build even when --release not used.
1175                 wf.write(fr.read().replace("@BUILD_ID@", release))
1176         outimages.append(new_kspath)
1177
1178         # save log file, logfile is only available in creator attrs
1179         if hasattr(self, 'logfile') and not self.logfile:
1180             log_path = _rpath(self.name + ".log")
1181             # touch the log file, else outimages will filter it out
1182             with open(log_path, 'w') as wf:
1183                 wf.write('')
1184             msger.set_logfile(log_path)
1185             outimages.append(_rpath(self.name + ".log"))
1186
1187         # rename iso and usbimg
1188         for f in os.listdir(destdir):
1189             if f.endswith(".iso"):
1190                 newf = f[:-4] + '.img'
1191             elif f.endswith(".usbimg"):
1192                 newf = f[:-7] + '.img'
1193             else:
1194                 continue
1195             os.rename(_rpath(f), _rpath(newf))
1196             outimages.append(_rpath(newf))
1197
1198         # generate MD5SUMS
1199         with open(_rpath("MD5SUMS"), "w") as wf:
1200             for f in os.listdir(destdir):
1201                 if f == "MD5SUMS":
1202                     continue
1203
1204                 if os.path.isdir(os.path.join(destdir, f)):
1205                     continue
1206
1207                 md5sum = misc.get_md5sum(_rpath(f))
1208                 # There needs to be two spaces between the sum and
1209                 # filepath to match the syntax with md5sum.
1210                 # This way also md5sum -c MD5SUMS can be used by users
1211                 wf.write("%s *%s\n" % (md5sum, f))
1212
1213         outimages.append("%s/MD5SUMS" % destdir)
1214
1215         # Filter out the nonexist file
1216         for fp in outimages[:]:
1217             if not os.path.exists("%s" % fp):
1218                 outimages.remove(fp)
1219
1220     def copy_kernel(self):
1221         """ Copy kernel files to the outimage directory.
1222         NOTE: This needs to be called before unmounting the instroot.
1223         """
1224
1225         if not self._need_copy_kernel:
1226             return
1227
1228         if not os.path.exists(self.destdir):
1229             os.makedirs(self.destdir)
1230
1231         for kernel in glob.glob("%s/boot/vmlinuz-*" % self._instroot):
1232             kernelfilename = "%s/%s-%s" % (self.destdir,
1233                                            self.name,
1234                                            os.path.basename(kernel))
1235             msger.info('copy kernel file %s as %s' % (os.path.basename(kernel),
1236                                                       kernelfilename))
1237             shutil.copy(kernel, kernelfilename)
1238             self.outimage.append(kernelfilename)
1239
1240     def copy_attachment(self):
1241         """ Subclass implement it to handle attachment files
1242         NOTE: This needs to be called before unmounting the instroot.
1243         """
1244         pass
1245
1246     def get_pkg_manager(self):
1247         return self.pkgmgr(target_arch = self.target_arch,
1248                            instroot = self._instroot,
1249                            cachedir = self.cachedir)