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