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