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