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