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