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