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