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