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