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