- Added priority option for repos to support package installations of older packages...
[meego-developer-tools:image-creator.git] / mic / imgcreate / creator.py
1 #
2 # creator.py : ImageCreator and LoopImageCreator base classes
3 #
4 # Copyright 2007, Red Hat  Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; version 2 of the License.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU Library General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
18
19 import os
20 import os.path
21 import stat
22 import sys
23 import tempfile
24 import shutil
25 import logging
26 import subprocess
27 import re
28 import tarfile
29 import glob
30
31 import rpm
32
33 from mic.imgcreate.errors import *
34 from mic.imgcreate.fs import *
35 from mic.imgcreate import kickstart
36 from mic.imgcreate import pkgmanagers
37 from mic.imgcreate.rpmmisc import *
38 from mic.utils.misc import *
39
40 FSLABEL_MAXLEN = 32
41 """The maximum string length supported for LoopImageCreator.fslabel."""
42
43 class ImageCreator(object):
44     """Installs a system to a chroot directory.
45
46     ImageCreator is the simplest creator class available; it will install and
47     configure a system image according to the supplied kickstart file.
48
49     e.g.
50
51       import mic.imgcreate as imgcreate
52       ks = imgcreate.read_kickstart("foo.ks")
53       imgcreate.ImageCreator(ks, "foo").create()
54
55     """
56
57     def __init__(self, ks, name):
58         """Initialize an ImageCreator instance.
59
60         ks -- a pykickstart.KickstartParser instance; this instance will be
61               used to drive the install by e.g. providing the list of packages
62               to be installed, the system configuration and %post scripts
63
64         name -- a name for the image; used for e.g. image filenames or
65                 filesystem labels
66
67         """
68
69         """ Initialize package managers """
70         self.pkgmgr = pkgmanagers.pkgManager()
71         self.pkgmgr.load_pkg_managers()
72
73         self.ks = ks
74         """A pykickstart.KickstartParser instance."""
75
76         self.name = name
77         """A name for the image."""
78
79         self.distro_name = "MeeGo"
80
81         """Output image file names"""
82         self.outimage = []
83
84         """A flag to generate checksum"""
85         self._genchecksum = False
86
87         self.tmpdir = "/var/tmp"
88         """The directory in which all temporary files will be created."""
89
90         self.cachedir = None
91
92         self._alt_initrd_name = None
93
94         self.__builddir = None
95         self.__bindmounts = []
96
97         """ Contains the compression method that is used to compress
98         the disk image after creation, e.g., bz2.
99         This value is set with compression_method function. """
100         self._img_compression_method = None
101
102         # dependent commands to check
103         self._dep_checks = ["ls", "bash", "cp", "echo", "modprobe", "passwd"]
104
105         self._recording_pkgs = None
106
107         self._include_src = None
108
109         self._local_pkgs_path = None
110
111         # available size in root fs, init to 0
112         self._root_fs_avail = 0
113
114         # target arch for non-x86 image
115         self.target_arch = None
116
117         """ Name of the disk image file that is created. """
118         self._img_name = None
119
120         """ Image format """
121         self.image_format = None
122
123         """ Save qemu emulator file name in order to clean up it finally """
124         self.qemu_emulator = None
125
126         """ No ks provided when called by convertor, so skip the dependency check """
127         if self.ks:
128             """ If we have btrfs partition we need to check that we have toosl for those """
129             for part in self.ks.handler.partition.partitions:
130                 if part.fstype and part.fstype == "btrfs":
131                     self._dep_checks.append("mkfs.btrfs")
132                     break
133
134     def set_target_arch(self, arch):
135         if arch not in arches.keys():
136             return False
137         self.target_arch = arch
138         if self.target_arch.startswith("arm"):
139             for dep in self._dep_checks:
140                 if dep == "extlinux":
141                     self._dep_checks.remove(dep)
142
143             if not os.path.exists("/usr/bin/qemu-arm") or not is_statically_linked("/usr/bin/qemu-arm"):
144                 self._dep_checks.append("qemu-arm-static")
145                 
146             if os.path.exists("/proc/sys/vm/vdso_enabled"):
147                 vdso_fh = open("/proc/sys/vm/vdso_enabled","r")
148                 vdso_value = vdso_fh.read().strip()
149                 vdso_fh.close()
150                 if (int)(vdso_value) == 1:
151                     print "\n= WARNING ="
152                     print "vdso is enabled on your host, which might cause problems with arm emulations."
153                     print "You can disable vdso with following command before starting image build:"
154                     print "echo 0 | sudo tee /proc/sys/vm/vdso_enabled"
155                     print "= WARNING =\n"
156
157         return True
158
159
160     def __del__(self):
161         self.cleanup()
162
163     #
164     # Properties
165     #
166     def __get_instroot(self):
167         if self.__builddir is None:
168             raise CreatorError("_instroot is not valid before calling mount()")
169         return self.__builddir + "/install_root"
170     _instroot = property(__get_instroot)
171     """The location of the install root directory.
172
173     This is the directory into which the system is installed. Subclasses may
174     mount a filesystem image here or copy files to/from here.
175
176     Note, this directory does not exist before ImageCreator.mount() is called.
177
178     Note also, this is a read-only attribute.
179
180     """
181
182     def __get_outdir(self):
183         if self.__builddir is None:
184             raise CreatorError("_outdir is not valid before calling mount()")
185         return self.__builddir + "/out"
186     _outdir = property(__get_outdir)
187     """The staging location for the final image.
188
189     This is where subclasses should stage any files that are part of the final
190     image. ImageCreator.package() will copy any files found here into the
191     requested destination directory.
192
193     Note, this directory does not exist before ImageCreator.mount() is called.
194
195     Note also, this is a read-only attribute.
196
197     """
198
199     #
200     # Hooks for subclasses
201     #
202     def _mount_instroot(self, base_on = None):
203         """Mount or prepare the install root directory.
204
205         This is the hook where subclasses may prepare the install root by e.g.
206         mounting creating and loopback mounting a filesystem image to
207         _instroot.
208
209         There is no default implementation.
210
211         base_on -- this is the value passed to mount() and can be interpreted
212                    as the subclass wishes; it might e.g. be the location of
213                    a previously created ISO containing a system image.
214
215         """
216         pass
217
218     def _unmount_instroot(self):
219         """Undo anything performed in _mount_instroot().
220
221         This is the hook where subclasses must undo anything which was done
222         in _mount_instroot(). For example, if a filesystem image was mounted
223         onto _instroot, it should be unmounted here.
224
225         There is no default implementation.
226
227         """
228         pass
229
230     def _create_bootconfig(self):
231         """Configure the image so that it's bootable.
232
233         This is the hook where subclasses may prepare the image for booting by
234         e.g. creating an initramfs and bootloader configuration.
235
236         This hook is called while the install root is still mounted, after the
237         packages have been installed and the kickstart configuration has been
238         applied, but before the %post scripts have been executed.
239
240         There is no default implementation.
241
242         """
243         pass
244
245     def _stage_final_image(self):
246         """Stage the final system image in _outdir.
247
248         This is the hook where subclasses should place the image in _outdir
249         so that package() can copy it to the requested destination directory.
250
251         By default, this moves the install root into _outdir.
252
253         """
254         shutil.move(self._instroot, self._outdir + "/" + self.name)
255
256     def get_installed_packages(self):
257         return self._pkgs_content.keys()
258
259     def _save_recording_pkgs(self, destdir):
260         """Save the list or content of installed packages to file.
261         """
262         if self._recording_pkgs not in ('content', 'name'):
263             return
264
265         pkgs = self._pkgs_content.keys()
266         pkgs.sort() # inplace op
267
268         # save package name list anyhow
269         if not os.path.exists(destdir):
270             makedirs(destdir)
271
272         namefile = os.path.join(destdir, self.name + '-pkgs.txt')
273         f = open(namefile, "w")
274         content = '\n'.join(pkgs)
275         f.write(content)
276         f.close()
277         self.outimage.append(namefile);
278
279         # if 'content', save more details
280         if self._recording_pkgs == 'content':
281             contfile = os.path.join(destdir, self.name + '-pkgs-content.txt')
282             f = open(contfile, "w")
283
284             for pkg in pkgs:
285                 content = pkg + '\n'
286
287                 pkgcont = self._pkgs_content[pkg]
288                 items = []
289                 if pkgcont.has_key('dir'):
290                     items = map(lambda x:x+'/', pkgcont['dir'])
291                 if pkgcont.has_key('file'):
292                     items.extend(pkgcont['file'])
293
294                 if items:
295                     content += '    '
296                     content += '\n    '.join(items)
297                     content += '\n'
298
299                 content += '\n'
300                 f.write(content)
301             f.close()
302             self.outimage.append(contfile)
303
304     def _get_required_packages(self):
305         """Return a list of required packages.
306
307         This is the hook where subclasses may specify a set of packages which
308         it requires to be installed.
309
310         This returns an empty list by default.
311
312         Note, subclasses should usually chain up to the base class
313         implementation of this hook.
314
315         """
316         return []
317
318     def _get_excluded_packages(self):
319         """Return a list of excluded packages.
320
321         This is the hook where subclasses may specify a set of packages which
322         it requires _not_ to be installed.
323
324         This returns an empty list by default.
325
326         Note, subclasses should usually chain up to the base class
327         implementation of this hook.
328
329         """
330         excluded_packages = []
331         for rpm_path in self._get_local_packages():
332             rpm_name = os.path.basename(rpm_path)
333             package_name = splitFilename(rpm_name)[0]
334             excluded_packages += [package_name]
335         return excluded_packages
336
337     def _get_local_packages(self):
338         """Return a list of rpm path to be local installed.
339
340         This is the hook where subclasses may specify a set of rpms which
341         it requires to be installed locally.
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         if self._local_pkgs_path:
350             if os.path.isdir(self._local_pkgs_path):
351                 return glob.glob(
352                         os.path.join(self._local_pkgs_path, '*.rpm'))
353             elif os.path.splitext(self._local_pkgs_path)[-1] == '.rpm':
354                 return [self._local_pkgs_path]
355
356         return []
357
358     def _get_fstab(self):
359         """Return the desired contents of /etc/fstab.
360
361         This is the hook where subclasses may specify the contents of
362         /etc/fstab by returning a string containing the desired contents.
363
364         A sensible default implementation is provided.
365
366         """
367         s =  "/dev/root  /         %s    %s 0 0\n" % (self._fstype, "defaults,noatime" if not self._fsopts else self._fsopts)
368         s += self._get_fstab_special()
369         return s
370
371     def _get_fstab_special(self):
372         s = "devpts     /dev/pts  devpts  gid=5,mode=620   0 0\n"
373         s += "tmpfs      /dev/shm  tmpfs   defaults         0 0\n"
374         s += "proc       /proc     proc    defaults         0 0\n"
375         s += "sysfs      /sys      sysfs   defaults         0 0\n"
376         return s
377
378     def _get_post_scripts_env(self, in_chroot):
379         """Return an environment dict for %post scripts.
380
381         This is the hook where subclasses may specify some environment
382         variables for %post scripts by return a dict containing the desired
383         environment.
384
385         By default, this returns an empty dict.
386
387         in_chroot -- whether this %post script is to be executed chroot()ed
388                      into _instroot.
389
390         """
391         return {}
392
393     def __get_imgname(self):
394         return self.name
395     _name = property(__get_imgname)
396     """The name of the image file.
397
398     """
399
400     def _get_kernel_versions(self):
401         """Return a dict detailing the available kernel types/versions.
402
403         This is the hook where subclasses may override what kernel types and
404         versions should be available for e.g. creating the booloader
405         configuration.
406
407         A dict should be returned mapping the available kernel types to a list
408         of the available versions for those kernels.
409
410         The default implementation uses rpm to iterate over everything
411         providing 'kernel', finds /boot/vmlinuz-* and returns the version
412         obtained from the vmlinuz filename. (This can differ from the kernel
413         RPM's n-v-r in the case of e.g. xen)
414
415         """
416         def get_version(header):
417             version = None
418             for f in header['filenames']:
419                 if f.startswith('/boot/vmlinuz-'):
420                     version = f[14:]
421             return version
422
423         ts = rpm.TransactionSet(self._instroot)
424
425         ret = {}
426         for header in ts.dbMatch('provides', 'kernel'):
427             version = get_version(header)
428             if version is None:
429                 continue
430
431             name = header['name']
432             if not name in ret:
433                 ret[name] = [version]
434             elif not version in ret[name]:
435                 ret[name].append(version)
436
437         return ret
438
439     #
440     # Helpers for subclasses
441     #
442     def _do_bindmounts(self):
443         """Mount various system directories onto _instroot.
444
445         This method is called by mount(), but may also be used by subclasses
446         in order to re-mount the bindmounts after modifying the underlying
447         filesystem.
448
449         """
450         for b in self.__bindmounts:
451             b.mount()
452
453     def _undo_bindmounts(self):
454         """Unmount the bind-mounted system directories from _instroot.
455
456         This method is usually only called by unmount(), but may also be used
457         by subclasses in order to gain access to the filesystem obscured by
458         the bindmounts - e.g. in order to create device nodes on the image
459         filesystem.
460
461         """
462         self.__bindmounts.reverse()
463         for b in self.__bindmounts:
464             b.unmount()
465
466     def _chroot(self):
467         """Chroot into the install root.
468
469         This method may be used by subclasses when executing programs inside
470         the install root e.g.
471
472           subprocess.call(["/bin/ls"], preexec_fn = self.chroot)
473
474         """
475         os.chroot(self._instroot)
476         os.chdir("/")
477
478     def _mkdtemp(self, prefix = "tmp-"):
479         """Create a temporary directory.
480
481         This method may be used by subclasses to create a temporary directory
482         for use in building the final image - e.g. a subclass might create
483         a temporary directory in order to bundle a set of files into a package.
484
485         The subclass may delete this directory if it wishes, but it will be
486         automatically deleted by cleanup().
487
488         The absolute path to the temporary directory is returned.
489
490         Note, this method should only be called after mount() has been called.
491
492         prefix -- a prefix which should be used when creating the directory;
493                   defaults to "tmp-".
494
495         """
496         self.__ensure_builddir()
497         return tempfile.mkdtemp(dir = self.__builddir, prefix = prefix)
498
499     def _mkstemp(self, prefix = "tmp-"):
500         """Create a temporary file.
501
502         This method may be used by subclasses to create a temporary file
503         for use in building the final image - e.g. a subclass might need
504         a temporary location to unpack a compressed file.
505
506         The subclass may delete this file if it wishes, but it will be
507         automatically deleted by cleanup().
508
509         A tuple containing a file descriptor (returned from os.open() and the
510         absolute path to the temporary directory is returned.
511
512         Note, this method should only be called after mount() has been called.
513
514         prefix -- a prefix which should be used when creating the file;
515                   defaults to "tmp-".
516
517         """
518         self.__ensure_builddir()
519         return tempfile.mkstemp(dir = self.__builddir, prefix = prefix)
520
521     def _mktemp(self, prefix = "tmp-"):
522         """Create a temporary file.
523
524         This method simply calls _mkstemp() and closes the returned file
525         descriptor.
526
527         The absolute path to the temporary file is returned.
528
529         Note, this method should only be called after mount() has been called.
530
531         prefix -- a prefix which should be used when creating the file;
532                   defaults to "tmp-".
533
534         """
535
536         (f, path) = self._mkstemp(prefix)
537         os.close(f)
538         return path
539
540     #
541     # Actual implementation
542     #
543     def __ensure_builddir(self):
544         if not self.__builddir is None:
545             return
546
547         try:
548             self.__builddir = tempfile.mkdtemp(dir = self.tmpdir,
549                                                prefix = "imgcreate-")
550         except OSError, (err, msg):
551             raise CreatorError("Failed create build directory in %s: %s" %
552                                (self.tmpdir, msg))
553
554     def get_cachedir(self, cachedir = None):
555         if self.cachedir:
556             return self.cachedir
557
558         self.__ensure_builddir()
559         if cachedir:
560             self.cachedir = cachedir
561         else:
562             self.cachedir = self.__builddir + "/yum-cache"
563         makedirs(self.cachedir)
564         return self.cachedir
565
566     def __sanity_check(self):
567         """Ensure that the config we've been given is sane."""
568         if not (kickstart.get_packages(self.ks) or
569                 kickstart.get_groups(self.ks)):
570             raise CreatorError("No packages or groups specified")
571
572         kickstart.convert_method_to_repo(self.ks)
573
574         if not kickstart.get_repos(self.ks):
575             raise CreatorError("No repositories specified")
576
577     def __write_fstab(self):
578         fstab = open(self._instroot + "/etc/fstab", "w")
579         fstab.write(self._get_fstab())
580         fstab.close()
581
582     def __create_minimal_dev(self):
583         """Create a minimal /dev so that we don't corrupt the host /dev"""
584         origumask = os.umask(0000)
585         devices = (('null',   1, 3, 0666),
586                    ('urandom',1, 9, 0666),
587                    ('random', 1, 8, 0666),
588                    ('full',   1, 7, 0666),
589                    ('ptmx',   5, 2, 0666),
590                    ('tty',    5, 0, 0666),
591                    ('zero',   1, 5, 0666))
592         links = (("/proc/self/fd", "/dev/fd"),
593                  ("/proc/self/fd/0", "/dev/stdin"),
594                  ("/proc/self/fd/1", "/dev/stdout"),
595                  ("/proc/self/fd/2", "/dev/stderr"))
596
597         for (node, major, minor, perm) in devices:
598             if not os.path.exists(self._instroot + "/dev/" + node):
599                 os.mknod(self._instroot + "/dev/" + node, perm | stat.S_IFCHR, os.makedev(major,minor))
600         for (src, dest) in links:
601             if not os.path.exists(self._instroot + dest):
602                 os.symlink(src, self._instroot + dest)
603         os.umask(origumask)
604
605
606     def mount(self, base_on = None, cachedir = None):
607         """Setup the target filesystem in preparation for an install.
608
609         This function sets up the filesystem which the ImageCreator will
610         install into and configure. The ImageCreator class merely creates an
611         install root directory, bind mounts some system directories (e.g. /dev)
612         and writes out /etc/fstab. Other subclasses may also e.g. create a
613         sparse file, format it and loopback mount it to the install root.
614
615         base_on -- a previous install on which to base this install; defaults
616                    to None, causing a new image to be created
617
618         cachedir -- a directory in which to store the Yum cache; defaults to
619                     None, causing a new cache to be created; by setting this
620                     to another directory, the same cache can be reused across
621                     multiple installs.
622
623         """
624         self.__ensure_builddir()
625
626         makedirs(self._instroot)
627         makedirs(self._outdir)
628
629         self._mount_instroot(base_on)
630
631         for d in ("/dev/pts", "/etc", "/boot", "/var/log", "/var/cache/yum", "/sys", "/proc", "/usr/bin"):
632             makedirs(self._instroot + d)
633
634         if self.target_arch and self.target_arch.startswith("arm"):
635             self.qemu_emulator = setup_qemu_emulator(self._instroot, self.target_arch)
636
637         self.get_cachedir(cachedir)
638
639         # bind mount system directories into _instroot
640         for (f, dest) in [("/sys", None), ("/proc", None), ("/proc/sys/fs/binfmt_misc", None),
641                           ("/dev/pts", None),
642                           (self.get_cachedir(), "/var/cache/yum")]:
643             self.__bindmounts.append(BindChrootMount(f, self._instroot, dest))
644
645
646         self._do_bindmounts()
647
648         self.__create_minimal_dev()
649
650         if os.path.exists(self._instroot + "/etc/mtab"):
651             os.unlink(self._instroot + "/etc/mtab")
652         os.symlink("../proc/mounts", self._instroot + "/etc/mtab")
653
654         self.__write_fstab()
655
656         # get size of available space in 'instroot' fs
657         self._root_fs_avail = get_filesystem_avail(self._instroot)
658
659     def unmount(self):
660         """Unmounts the target filesystem.
661
662         The ImageCreator class detaches the system from the install root, but
663         other subclasses may also detach the loopback mounted filesystem image
664         from the install root.
665
666         """
667         try:
668             mtab = self._instroot + "/etc/mtab"
669             if not os.path.islink(mtab):
670                 os.unlink(self._instroot + "/etc/mtab")
671             if self.qemu_emulator:
672                 os.unlink(self._instroot + self.qemu_emulator)
673         except OSError:
674             pass
675
676
677         self._undo_bindmounts()
678
679         """ Clean up yum garbage """
680         try:
681             instroot_pdir = os.path.dirname(self._instroot + self._instroot)
682             if os.path.exists(instroot_pdir):
683                 shutil.rmtree(instroot_pdir, ignore_errors = True)
684             yumcachedir = self._instroot + "/var/cache/yum"
685             if os.path.exists(yumcachedir):
686                 shutil.rmtree(yumcachedir, ignore_errors = True)
687             yumlibdir = self._instroot + "/var/lib/yum"
688             if os.path.exists(yumlibdir):
689                 shutil.rmtree(yumlibdir, ignore_errors = True)
690         except OSError:
691             pass
692
693         self._unmount_instroot()
694
695     def cleanup(self):
696         """Unmounts the target filesystem and deletes temporary files.
697
698         This method calls unmount() and then deletes any temporary files and
699         directories that were created on the host system while building the
700         image.
701
702         Note, make sure to call this method once finished with the creator
703         instance in order to ensure no stale files are left on the host e.g.:
704
705           creator = ImageCreator(ks, name)
706           try:
707               creator.create()
708           finally:
709               creator.cleanup()
710
711         """
712         if not self.__builddir:
713             return
714
715         self.unmount()
716
717         shutil.rmtree(self.__builddir, ignore_errors = True)
718         self.__builddir = None
719
720     def __is_excluded_pkg(self, pkg):
721         if pkg in self._excluded_pkgs:
722             self._excluded_pkgs.remove(pkg)
723             return True
724
725         for xpkg in self._excluded_pkgs:
726             if xpkg.endswith('*'):
727                 if pkg.startswith(xpkg[:-1]):
728                     return True
729             elif xpkg.startswith('*'):
730                 if pkg.endswith(xpkg[1:]):
731                     return True
732
733         return None
734
735     def __select_packages(self, pkg_manager):
736         skipped_pkgs = []
737         for pkg in self._required_pkgs:
738             e = pkg_manager.selectPackage(pkg)
739             if e:
740                 if kickstart.ignore_missing(self.ks):
741                     skipped_pkgs.append(pkg)
742                 elif self.__is_excluded_pkg(pkg):
743                     skipped_pkgs.append(pkg)
744                 else:
745                     raise CreatorError("Failed to find package '%s' : %s" %
746                                        (pkg, e))
747
748         for pkg in skipped_pkgs:
749             logging.warn("Skipping missing package '%s'" % (pkg,))
750
751     def __select_groups(self, pkg_manager):
752         skipped_groups = []
753         for group in self._required_groups:
754             e = pkg_manager.selectGroup(group.name, group.include)
755             if e:
756                 if kickstart.ignore_missing(self.ks):
757                     skipped_groups.append(group)
758                 else:
759                     raise CreatorError("Failed to find group '%s' : %s" %
760                                        (group.name, e))
761
762         for group in skipped_groups:
763             logging.warn("Skipping missing group '%s'" % (group.name,))
764
765     def __deselect_packages(self, pkg_manager):
766         for pkg in self._excluded_pkgs:
767             pkg_manager.deselectPackage(pkg)
768
769     def __localinst_packages(self, pkg_manager):
770         for rpm_path in self._get_local_packages():
771             pkg_manager.installLocal(rpm_path)
772
773     def install(self, repo_urls = {}):
774         """Install packages into the install root.
775
776         This function installs the packages listed in the supplied kickstart
777         into the install root. By default, the packages are installed from the
778         repository URLs specified in the kickstart.
779
780         repo_urls -- a dict which maps a repository name to a repository URL;
781                      if supplied, this causes any repository URLs specified in
782                      the kickstart to be overridden.
783
784         """
785
786
787         # initialize pkg list to install
788         if self.ks:
789             self.__sanity_check()
790
791             self._required_pkgs = \
792                 kickstart.get_packages(self.ks, self._get_required_packages())
793             self._excluded_pkgs = \
794                 kickstart.get_excluded(self.ks, self._get_excluded_packages())
795             self._required_groups = kickstart.get_groups(self.ks)
796         else:
797             self._required_pkgs = None
798             self._excluded_pkgs = None
799             self._required_groups = None
800
801         yum_conf = self._mktemp(prefix = "yum.conf-")
802
803         keep_record = None
804         if self._include_src:
805             keep_record = 'include_src'
806         if self._recording_pkgs in ('name', 'content'):
807             keep_record = self._recording_pkgs
808
809         pkg_manager = self.get_pkg_manager(keep_record)
810         pkg_manager.setup(yum_conf, self._instroot)
811
812         for repo in kickstart.get_repos(self.ks, repo_urls):
813             (name, baseurl, mirrorlist, inc, exc, proxy, proxy_username, proxy_password, debuginfo, source, gpgkey, disable, cost, priority) = repo
814             try:
815                 yr = pkg_manager.addRepository(name, baseurl, mirrorlist, proxy, proxy_username, proxy_password, inc, exc, cost, priority)
816                 if inc:
817                     yr.includepkgs = inc
818                 if exc:
819                     yr.exclude = exc
820             except CreatorError, e:
821                 raise CreatorError("%s" % (e,))
822
823         if kickstart.exclude_docs(self.ks):
824             rpm.addMacro("_excludedocs", "1")
825         rpm.addMacro("__file_context_path", "%{nil}")
826         if kickstart.inst_langs(self.ks) != None:
827             rpm.addMacro("_install_langs", kickstart.inst_langs(self.ks))
828
829         try:
830             self.__select_packages(pkg_manager)
831             self.__select_groups(pkg_manager)
832             self.__deselect_packages(pkg_manager)
833             self.__localinst_packages(pkg_manager)
834
835             BOOT_SAFEGUARD = 256L * 1024 * 1024 # 256M
836             checksize = self._root_fs_avail
837             if checksize:
838                 checksize -= BOOT_SAFEGUARD
839             if self.target_arch:
840                 pkg_manager._add_prob_flags(rpm.RPMPROB_FILTER_IGNOREARCH)
841
842             try:
843                 save_env = os.environ["LC_ALL"]
844             except KeyError:
845                 save_env = None
846             os.environ["LC_ALL"] = 'C'
847             pkg_manager.runInstall(checksize)
848             if save_env:
849                 os.environ["LC_ALL"] = save_env
850             else:
851                 os.unsetenv("LC_ALL")
852         finally:
853             if keep_record:
854                 self._pkgs_content = pkg_manager.getAllContent()
855
856             pkg_manager.closeRpmDB()
857             pkg_manager.close()
858             os.unlink(yum_conf)
859
860         # do some clean up to avoid lvm info leakage.  this sucks.
861         for subdir in ("cache", "backup", "archive"):
862             lvmdir = self._instroot + "/etc/lvm/" + subdir
863             try:
864                 for f in os.listdir(lvmdir):
865                     os.unlink(lvmdir + "/" + f)
866             except:
867                 pass
868
869     def __run_post_scripts(self):
870         print "Running scripts"
871         for s in kickstart.get_post_scripts(self.ks):
872             (fd, path) = tempfile.mkstemp(prefix = "ks-script-",
873                                           dir = self._instroot + "/tmp")
874
875             s.script = s.script.replace("\r", "")
876             os.write(fd, s.script)
877             os.close(fd)
878             os.chmod(path, 0700)
879
880             env = self._get_post_scripts_env(s.inChroot)
881
882             if not s.inChroot:
883                 env["INSTALL_ROOT"] = self._instroot
884                 env["IMG_NAME"] = self._name
885                 preexec = None
886                 script = path
887             else:
888                 preexec = self._chroot
889                 script = "/tmp/" + os.path.basename(path)
890
891             try:
892                 try:
893                     subprocess.call([s.interp, script],
894                                     preexec_fn = preexec, env = env, stdout = sys.stdout, stderr = sys.stderr)
895                 except OSError, (err, msg):
896                     raise CreatorError("Failed to execute %%post script "
897                                        "with '%s' : %s" % (s.interp, msg))
898             finally:
899                 os.unlink(path)
900
901     def __save_repo_keys(self, repodata):
902         if not repodata:
903             return None
904         gpgkeydir = "/etc/pki/rpm-gpg"
905         makedirs(self._instroot + gpgkeydir)
906         for repo in repodata:
907             if repo["repokey"]:
908                 repokey = gpgkeydir + "/RPM-GPG-KEY-%s" %  repo["name"]
909                 shutil.copy(repo["repokey"], self._instroot + repokey)
910
911     def configure(self, repodata = None):
912         """Configure the system image according to the kickstart.
913
914         This method applies the (e.g. keyboard or network) configuration
915         specified in the kickstart and executes the kickstart %post scripts.
916
917         If neccessary, it also prepares the image to be bootable by e.g.
918         creating an initrd and bootloader configuration.
919
920         """
921         ksh = self.ks.handler
922
923         try:
924             kickstart.LanguageConfig(self._instroot).apply(ksh.lang)
925             kickstart.KeyboardConfig(self._instroot).apply(ksh.keyboard)
926             kickstart.TimezoneConfig(self._instroot).apply(ksh.timezone)
927             #kickstart.AuthConfig(self._instroot).apply(ksh.authconfig)
928             kickstart.FirewallConfig(self._instroot).apply(ksh.firewall)
929             kickstart.RootPasswordConfig(self._instroot).apply(ksh.rootpw)
930             kickstart.UserConfig(self._instroot).apply(ksh.user)
931             kickstart.ServicesConfig(self._instroot).apply(ksh.services)
932             kickstart.XConfig(self._instroot).apply(ksh.xconfig)
933             kickstart.NetworkConfig(self._instroot).apply(ksh.network)
934             kickstart.RPMMacroConfig(self._instroot).apply(self.ks)
935             kickstart.DesktopConfig(self._instroot).apply(ksh.desktop)
936             self.__save_repo_keys(repodata)
937             kickstart.MoblinRepoConfig(self._instroot).apply(ksh.repo, repodata)
938         except:
939             print "Failed to apply configuration to image"
940             raise
941
942         self._create_bootconfig()
943         self.__run_post_scripts()
944
945     def launch_shell(self, launch):
946         """Launch a shell in the install root.
947
948         This method is launches a bash shell chroot()ed in the install root;
949         this can be useful for debugging.
950
951         """
952         if launch:
953             print "Launching shell. Exit to continue."
954             print "----------------------------------"
955             subprocess.call(["/bin/bash"], preexec_fn = self._chroot)
956
957     def do_genchecksum(self, image_name):
958         if not self._genchecksum:
959             return
960
961         """ Generate md5sum if /usr/bin/md5sum is available """
962         if os.path.exists("/usr/bin/md5sum"):
963             p = subprocess.Popen(["/usr/bin/md5sum", "-b", image_name],
964                                  stdout=subprocess.PIPE)
965             (md5sum, errorstr) = p.communicate()
966             if p.returncode != 0:
967                 logging.warning("Can't generate md5sum for image %s" % image_name)
968             else:
969                 pattern = re.compile("\*.*$")
970                 md5sum = pattern.sub("*" + os.path.basename(image_name), md5sum)
971                 fd = open(image_name + ".md5sum", "w")
972                 fd.write(md5sum)
973                 fd.close()
974                 self.outimage.append(image_name+".md5sum")
975
976     def package(self, destdir = "."):
977         """Prepares the created image for final delivery.
978
979         In its simplest form, this method merely copies the install root to the
980         supplied destination directory; other subclasses may choose to package
981         the image by e.g. creating a bootable ISO containing the image and
982         bootloader configuration.
983
984         destdir -- the directory into which the final image should be moved;
985                    this defaults to the current directory.
986
987         """
988         self._stage_final_image()
989
990         if self._img_compression_method:
991             if not self._img_name:
992                 raise CreatorError("Image name not set.")
993             rc = None
994             img_location = os.path.join(self._outdir,self._img_name)
995
996             print "Compressing %s with %s. Please wait..." % (img_location, self._img_compression_method)
997             if self._img_compression_method == "bz2":
998                 bzip2 = find_binary_path('bzip2')
999                 rc = subprocess.call([bzip2, "-f", img_location])
1000                 if rc:
1001                     raise CreatorError("Failed to compress image %s with %s." % (img_location, self._img_compression_method))
1002                 for bootimg in glob.glob(os.path.dirname(img_location) + "/*-boot.bin"):
1003                     print "Compressing %s with bzip2. Please wait..." % bootimg
1004                     rc = subprocess.call([bzip2, "-f", bootimg])
1005                     if rc:
1006                         raise CreatorError("Failed to compress image %s with %s." % (bootimg, self._img_compression_method))
1007             elif self._img_compression_method == "tar.bz2":
1008                 dst = "%s.tar.bz2" % (img_location)
1009
1010                 tar = tarfile.open(dst, "w:bz2")
1011                 # Add files to tarball and remove originals after packaging
1012                 tar.add(img_location, self._img_name)
1013                 os.unlink(img_location)
1014                 for bootimg in glob.glob(os.path.dirname(img_location) + "/*-boot.bin"):
1015                     tar.add(bootimg,os.path.basename(bootimg))
1016                     os.unlink(bootimg)
1017                 tar.close()
1018
1019
1020         if self._recording_pkgs:
1021             self._save_recording_pkgs(destdir)
1022
1023         """ For image formats with two or multiple image files, it will be better to put them under a directory """
1024         if self.image_format in ("raw", "vmdk", "vdi", "nand", "mrstnand"):
1025             destdir = os.path.join(destdir, "%s-%s" % (self.name, self.image_format))
1026             logging.debug("creating destination dir: %s" % destdir)
1027             makedirs(destdir)
1028
1029         # Ensure all data is flushed to _outdir
1030         synccmd = find_binary_path("sync")
1031         subprocess.call([synccmd])
1032
1033         for f in os.listdir(self._outdir):
1034             shutil.move(os.path.join(self._outdir, f),
1035                         os.path.join(destdir, f))
1036             self.outimage.append(os.path.join(destdir, f))
1037             self.do_genchecksum(os.path.join(destdir, f))
1038
1039     def create(self):
1040         """Install, configure and package an image.
1041
1042         This method is a utility method which creates and image by calling some
1043         of the other methods in the following order - mount(), install(),
1044         configure(), unmount and package().
1045
1046         """
1047         self.mount()
1048         self.install()
1049         self.configure()
1050         self.unmount()
1051         self.package()
1052
1053     def print_outimage_info(self):
1054         print "Your new image can be found here:"
1055         self.outimage.sort()
1056         for file in self.outimage:
1057             print os.path.abspath(file)
1058
1059     def check_depend_tools(self):
1060         for tool in self._dep_checks:
1061             find_binary_path(tool)
1062
1063     def package_output(self, image_format, destdir = ".", package="none"):
1064         if not package or package == "none":
1065             return
1066
1067         destdir = os.path.abspath(os.path.expanduser(destdir))
1068         (pkg, comp) = os.path.splitext(package)
1069         if comp:
1070             comp=comp.lstrip(".")
1071
1072         if pkg == "tar":
1073             if comp:
1074                 dst = "%s/%s-%s.tar.%s" % (destdir, self.name, image_format, comp)
1075             else:
1076                 dst = "%s/%s-%s.tar" % (destdir, self.name, image_format)
1077             print "creating %s" % dst
1078             tar = tarfile.open(dst, "w:" + comp)
1079
1080             for file in self.outimage:
1081                 print "adding %s to %s" % (file, dst)
1082                 tar.add(file, arcname=os.path.join("%s-%s" % (self.name, image_format), os.path.basename(file)))
1083                 if os.path.isdir(file):
1084                     shutil.rmtree(file, ignore_errors = True)
1085                 else:
1086                     os.remove(file)
1087
1088
1089             tar.close()
1090
1091             '''All the file in outimage has been packaged into tar.* file'''
1092             self.outimage = [dst]
1093
1094     def release_output(self, config, destdir, name, release):
1095         self.outimage = create_release(config, destdir, name, self.outimage, release)
1096
1097     def save_kernel(self, destdir):
1098         if not os.path.exists(destdir):
1099             makedirs(destdir)
1100         for kernel in glob.glob("%s/boot/vmlinuz-*" % self._instroot):
1101             kernelfilename = "%s/%s-%s" % (destdir, self.name, os.path.basename(kernel))
1102             shutil.copy(kernel, kernelfilename)
1103             self.outimage.append(kernelfilename)
1104
1105     def compress_disk_image(self, compression_method):
1106         """
1107         With this you can set the method that is used to compress the disk
1108         image after it is created.
1109         """
1110
1111         if compression_method not in ('bz2', 'tar.bz2'):
1112             raise CreatorError("Given disk image compression method ('%s') is not valid." % (compression_method))
1113
1114         self._img_compression_method = compression_method
1115         
1116     def set_pkg_manager(self, name):
1117         self.pkgmgr.set_default_pkg_manager(name)
1118
1119     def get_pkg_manager(self, recording_pkgs=None):
1120         pkgmgr_instance = self.pkgmgr.get_default_pkg_manager()
1121         if not pkgmgr_instance:
1122             raise CreatorError("No package manager available")
1123         return pkgmgr_instance(creator = self, recording_pkgs = recording_pkgs)
1124
1125 class LoopImageCreator(ImageCreator):
1126     """Installs a system into a loopback-mountable filesystem image.
1127
1128     LoopImageCreator is a straightforward ImageCreator subclass; the system
1129     is installed into an ext3 filesystem on a sparse file which can be
1130     subsequently loopback-mounted.
1131
1132     """
1133
1134     def __init__(self, ks, name, fslabel = None):
1135         """Initialize a LoopImageCreator instance.
1136
1137         This method takes the same arguments as ImageCreator.__init__() with
1138         the addition of:
1139
1140         fslabel -- A string used as a label for any filesystems created.
1141
1142         """
1143         ImageCreator.__init__(self, ks, name)
1144
1145         self.__fslabel = None
1146         self.fslabel = fslabel
1147
1148         self.__minsize_KB = 0
1149         self.__blocksize = 4096
1150         if self.ks:
1151             self.__fstype = kickstart.get_image_fstype(self.ks, "ext3")
1152             self.__fsopts = kickstart.get_image_fsopts(self.ks, "defaults,noatime")
1153         else:
1154             self.__fstype = None
1155             self.__fsopts = None
1156
1157         self.__instloop = None
1158         self.__imgdir = None
1159
1160         if self.ks:
1161             self.__image_size = kickstart.get_image_size(self.ks,
1162                                                          4096L * 1024 * 1024)
1163         else:
1164             self.__image_size = 0
1165
1166         self._img_name = self.name + ".img"
1167
1168     def _set_fstype(self, fstype):
1169         self.__fstype = fstype
1170
1171     def _set_image_size(self, imgsize):
1172         self.__image_size = imgsize
1173
1174     #
1175     # Properties
1176     #
1177     def __get_fslabel(self):
1178         if self.__fslabel is None:
1179             return self.name
1180         else:
1181             return self.__fslabel
1182     def __set_fslabel(self, val):
1183         if val is None:
1184             self.__fslabel = None
1185         else:
1186             self.__fslabel = val[:FSLABEL_MAXLEN]
1187     fslabel = property(__get_fslabel, __set_fslabel)
1188     """A string used to label any filesystems created.
1189
1190     Some filesystems impose a constraint on the maximum allowed size of the
1191     filesystem label. In the case of ext3 it's 16 characters, but in the case
1192     of ISO9660 it's 32 characters.
1193
1194     mke2fs silently truncates the label, but mkisofs aborts if the label is too
1195     long. So, for convenience sake, any string assigned to this attribute is
1196     silently truncated to FSLABEL_MAXLEN (32) characters.
1197
1198     """
1199
1200     def __get_image(self):
1201         if self.__imgdir is None:
1202             raise CreatorError("_image is not valid before calling mount()")
1203         return self.__imgdir + "/meego.img"
1204     _image = property(__get_image)
1205     """The location of the image file.
1206
1207     This is the path to the filesystem image. Subclasses may use this path
1208     in order to package the image in _stage_final_image().
1209
1210     Note, this directory does not exist before ImageCreator.mount() is called.
1211
1212     Note also, this is a read-only attribute.
1213
1214     """
1215
1216     def __get_blocksize(self):
1217         return self.__blocksize
1218     def __set_blocksize(self, val):
1219         if self.__instloop:
1220             raise CreatorError("_blocksize must be set before calling mount()")
1221         try:
1222             self.__blocksize = int(val)
1223         except ValueError:
1224             raise CreatorError("'%s' is not a valid integer value "
1225                                "for _blocksize" % val)
1226     _blocksize = property(__get_blocksize, __set_blocksize)
1227     """The block size used by the image's filesystem.
1228
1229     This is the block size used when creating the filesystem image. Subclasses
1230     may change this if they wish to use something other than a 4k block size.
1231
1232     Note, this attribute may only be set before calling mount().
1233
1234     """
1235
1236     def __get_fstype(self):
1237         return self.__fstype
1238     def __set_fstype(self, val):
1239         if val != "ext2" and val != "ext3":
1240             raise CreatorError("Unknown _fstype '%s' supplied" % val)
1241         self.__fstype = val
1242     _fstype = property(__get_fstype, __set_fstype)
1243     """The type of filesystem used for the image.
1244
1245     This is the filesystem type used when creating the filesystem image.
1246     Subclasses may change this if they wish to use something other ext3.
1247
1248     Note, only ext2 and ext3 are currently supported.
1249
1250     Note also, this attribute may only be set before calling mount().
1251
1252     """
1253
1254     def __get_fsopts(self):
1255         return self.__fsopts
1256     def __set_fsopts(self, val):
1257         self.__fsopts = val
1258     _fsopts = property(__get_fsopts, __set_fsopts)
1259     """Mount options of filesystem used for the image.
1260
1261     This can be specified by --fsoptions="xxx,yyy" in part command in
1262     kickstart file.
1263     """
1264
1265     #
1266     # Helpers for subclasses
1267     #
1268     def _resparse(self, size = None):
1269         """Rebuild the filesystem image to be as sparse as possible.
1270
1271         This method should be used by subclasses when staging the final image
1272         in order to reduce the actual space taken up by the sparse image file
1273         to be as little as possible.
1274
1275         This is done by resizing the filesystem to the minimal size (thereby
1276         eliminating any space taken up by deleted files) and then resizing it
1277         back to the supplied size.
1278
1279         size -- the size in, in bytes, which the filesystem image should be
1280                 resized to after it has been minimized; this defaults to None,
1281                 causing the original size specified by the kickstart file to
1282                 be used (or 4GiB if not specified in the kickstart).
1283
1284         """
1285         return self.__instloop.resparse(size)
1286
1287     def _base_on(self, base_on):
1288         shutil.copyfile(base_on, self._image)
1289
1290     #
1291     # Actual implementation
1292     #
1293     def _mount_instroot(self, base_on = None):
1294         self.__imgdir = self._mkdtemp()
1295
1296         if not base_on is None:
1297             self._base_on(base_on)
1298
1299         if self.__fstype in ("ext2", "ext3", "ext4"):
1300             MyDiskMount = ExtDiskMount
1301         elif self.__fstype == "btrfs":
1302             MyDiskMount = BtrfsDiskMount
1303
1304         self.__instloop = MyDiskMount(SparseLoopbackDisk(self._image, self.__image_size),
1305                                        self._instroot,
1306                                        self.__fstype,
1307                                        self.__blocksize,
1308                                        self.fslabel)
1309
1310         try:
1311             self.__instloop.mount()
1312         except MountError, e:
1313             raise CreatorError("Failed to loopback mount '%s' : %s" %
1314                                (self._image, e))
1315
1316     def _unmount_instroot(self):
1317         if not self.__instloop is None:
1318             self.__instloop.cleanup()
1319
1320     def _stage_final_image(self):
1321         self._resparse()
1322         shutil.move(self._image, self._outdir + "/" + self._img_name)
1323
1324
1325
1326 class FsImageCreator(ImageCreator):
1327     def __init__(self, ks, name):
1328         """Initialize a LoopImageCreator instance.
1329
1330         This method takes the same arguments as ImageCreator.__init__()
1331         """
1332         ImageCreator.__init__(self, ks, name)
1333
1334         self._fstype = None
1335         self._fsopts = None
1336
1337     def _stage_final_image(self):
1338         """ nothing to do """
1339         pass
1340
1341     def _exclude_filter(self, filename):
1342         # Drop instroot prefix from the filename
1343         filename = filename[len(self._instroot):]
1344         if filename not in self._ignores:
1345             return False
1346         else:
1347             return True
1348
1349     def package(self, destdir = "."):
1350         self._stage_final_image()
1351
1352         destdir = os.path.abspath(os.path.expanduser(destdir))
1353         if self._recording_pkgs:
1354             self._save_recording_pkgs(destdir)
1355
1356         self._ignores = ["/dev/fd", "/dev/stdin", "/dev/stdout", "/dev/stderr", "/etc/mtab"]
1357
1358         if self._img_compression_method == None:
1359             print "Copying %s to %s, please be patient to wait (it is slow if they are on different file systems/partitons/disks)" \
1360                 % (self._instroot, destdir + "/" + self.name)
1361
1362             copycmd = find_binary_path("cp")
1363             args = [ copycmd, "-af", self._instroot, destdir + "/" + self.name ]
1364             subprocess.call(args)
1365
1366             for exclude in self._ignores:
1367                 if os.path.exists(destdir + "/" + self.name + exclude):
1368                     os.unlink(destdir + "/" + self.name + exclude)
1369
1370             self.outimage.append(destdir + "/" + self.name)
1371
1372         elif self._img_compression_method == "tar.bz2":
1373             dst = "%s/%s.tar.bz2" % (destdir, self.name)
1374             print "Creating %s (compressing %s with %s). Please wait..." % (dst, self._instroot, self._img_compression_method)
1375
1376             tar = find_binary_path('tar')
1377             tar_cmdline = [tar, "--numeric-owner", "--preserve", "--one-file-system", "--directory", self._instroot]
1378             for ignore_entry in self._ignores:
1379                 if ignore_entry.startswith('/'):
1380                     ignore_entry = ignore_entry[1:]
1381                 
1382                 tar_cmdline.append("--exclude=%s" % (ignore_entry))
1383             
1384             tar_cmdline.extend(["-cjf", dst, "."])
1385             
1386             rc = subprocess.call(tar_cmdline)
1387             if rc:
1388                 raise CreatorError("Failed compress image with tar.bz2. Cmdline: %s" % (" ".join(tar_cmdline)))
1389
1390             self.outimage.append(dst)
1391
1392         else:
1393             raise CreatorError("Compression method '%s' not supported for 'fs' image format." % (self._img_compression_method))