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