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