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