summaryrefslogtreecommitdiffstats
path: root/scripts/lib/mic/imager/baseimager.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/lib/mic/imager/baseimager.py')
-rw-r--r--scripts/lib/mic/imager/baseimager.py1265
1 files changed, 1265 insertions, 0 deletions
diff --git a/scripts/lib/mic/imager/baseimager.py b/scripts/lib/mic/imager/baseimager.py
new file mode 100644
index 0000000000..4d6be29a0e
--- /dev/null
+++ b/scripts/lib/mic/imager/baseimager.py
@@ -0,0 +1,1265 @@
1#!/usr/bin/python -tt
2#
3# Copyright (c) 2007 Red Hat Inc.
4# Copyright (c) 2009, 2010, 2011 Intel, Inc.
5#
6# This program is free software; you can redistribute it and/or modify it
7# under the terms of the GNU General Public License as published by the Free
8# Software Foundation; version 2 of the License
9#
10# This program is distributed in the hope that it will be useful, but
11# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13# for more details.
14#
15# You should have received a copy of the GNU General Public License along
16# with this program; if not, write to the Free Software Foundation, Inc., 59
17# Temple Place - Suite 330, Boston, MA 02111-1307, USA.
18
19from __future__ import with_statement
20import os, sys
21import stat
22import tempfile
23import shutil
24import subprocess
25import re
26import tarfile
27import glob
28
29import rpm
30
31from mic import kickstart
32from mic import msger
33from mic.utils.errors import CreatorError, Abort
34from mic.utils import misc, grabber, runner, fs_related as fs
35
36class BaseImageCreator(object):
37 """Installs a system to a chroot directory.
38
39 ImageCreator is the simplest creator class available; it will install and
40 configure a system image according to the supplied kickstart file.
41
42 e.g.
43
44 import mic.imgcreate as imgcreate
45 ks = imgcreate.read_kickstart("foo.ks")
46 imgcreate.ImageCreator(ks, "foo").create()
47
48 """
49
50 def __del__(self):
51 self.cleanup()
52
53 def __init__(self, createopts = None, pkgmgr = None):
54 """Initialize an ImageCreator instance.
55
56 ks -- a pykickstart.KickstartParser instance; this instance will be
57 used to drive the install by e.g. providing the list of packages
58 to be installed, the system configuration and %post scripts
59
60 name -- a name for the image; used for e.g. image filenames or
61 filesystem labels
62 """
63
64 self.pkgmgr = pkgmgr
65
66 self.__builddir = None
67 self.__bindmounts = []
68
69 self.ks = None
70 self.name = "target"
71 self.tmpdir = "/var/tmp/wic"
72 self.cachedir = "/var/tmp/wic/cache"
73 self.workdir = "/var/tmp/wic/build"
74
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"] = "wic"
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 + "/wic-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 self._mount_instroot(base_on)
755
756 def unmount(self):
757 """Unmounts the target filesystem.
758
759 The ImageCreator class detaches the system from the install root, but
760 other subclasses may also detach the loopback mounted filesystem image
761 from the install root.
762
763 """
764 self._unmount_instroot()
765
766
767 def cleanup(self):
768 """Unmounts the target filesystem and deletes temporary files.
769
770 This method calls unmount() and then deletes any temporary files and
771 directories that were created on the host system while building the
772 image.
773
774 Note, make sure to call this method once finished with the creator
775 instance in order to ensure no stale files are left on the host e.g.:
776
777 creator = ImageCreator(ks, name)
778 try:
779 creator.create()
780 finally:
781 creator.cleanup()
782
783 """
784 if not self.__builddir:
785 return
786
787 self.unmount()
788
789 shutil.rmtree(self.__builddir, ignore_errors = True)
790 self.__builddir = None
791
792 self.__clean_tmpdir()
793
794 def __is_excluded_pkg(self, pkg):
795 if pkg in self._excluded_pkgs:
796 self._excluded_pkgs.remove(pkg)
797 return True
798
799 for xpkg in self._excluded_pkgs:
800 if xpkg.endswith('*'):
801 if pkg.startswith(xpkg[:-1]):
802 return True
803 elif xpkg.startswith('*'):
804 if pkg.endswith(xpkg[1:]):
805 return True
806
807 return None
808
809 def __select_packages(self, pkg_manager):
810 skipped_pkgs = []
811 for pkg in self._required_pkgs:
812 e = pkg_manager.selectPackage(pkg)
813 if e:
814 if kickstart.ignore_missing(self.ks):
815 skipped_pkgs.append(pkg)
816 elif self.__is_excluded_pkg(pkg):
817 skipped_pkgs.append(pkg)
818 else:
819 raise CreatorError("Failed to find package '%s' : %s" %
820 (pkg, e))
821
822 for pkg in skipped_pkgs:
823 msger.warning("Skipping missing package '%s'" % (pkg,))
824
825 def __select_groups(self, pkg_manager):
826 skipped_groups = []
827 for group in self._required_groups:
828 e = pkg_manager.selectGroup(group.name, group.include)
829 if e:
830 if kickstart.ignore_missing(self.ks):
831 skipped_groups.append(group)
832 else:
833 raise CreatorError("Failed to find group '%s' : %s" %
834 (group.name, e))
835
836 for group in skipped_groups:
837 msger.warning("Skipping missing group '%s'" % (group.name,))
838
839 def __deselect_packages(self, pkg_manager):
840 for pkg in self._excluded_pkgs:
841 pkg_manager.deselectPackage(pkg)
842
843 def __localinst_packages(self, pkg_manager):
844 for rpm_path in self._get_local_packages():
845 pkg_manager.installLocal(rpm_path)
846
847 def __preinstall_packages(self, pkg_manager):
848 if not self.ks:
849 return
850
851 self._preinstall_pkgs = kickstart.get_pre_packages(self.ks)
852 for pkg in self._preinstall_pkgs:
853 pkg_manager.preInstall(pkg)
854
855 def __attachment_packages(self, pkg_manager):
856 if not self.ks:
857 return
858
859 self._attachment = []
860 for item in kickstart.get_attachment(self.ks):
861 if item.startswith('/'):
862 fpaths = os.path.join(self._instroot, item.lstrip('/'))
863 for fpath in glob.glob(fpaths):
864 self._attachment.append(fpath)
865 continue
866
867 filelist = pkg_manager.getFilelist(item)
868 if filelist:
869 # found rpm in rootfs
870 for pfile in pkg_manager.getFilelist(item):
871 fpath = os.path.join(self._instroot, pfile.lstrip('/'))
872 self._attachment.append(fpath)
873 continue
874
875 # try to retrieve rpm file
876 (url, proxies) = pkg_manager.package_url(item)
877 if not url:
878 msger.warning("Can't get url from repo for %s" % item)
879 continue
880 fpath = os.path.join(self.cachedir, os.path.basename(url))
881 if not os.path.exists(fpath):
882 # download pkgs
883 try:
884 fpath = grabber.myurlgrab(url, fpath, proxies, None)
885 except CreatorError:
886 raise
887
888 tmpdir = self._mkdtemp()
889 misc.extract_rpm(fpath, tmpdir)
890 for (root, dirs, files) in os.walk(tmpdir):
891 for fname in files:
892 fpath = os.path.join(root, fname)
893 self._attachment.append(fpath)
894
895 def install(self, repo_urls=None):
896 """Install packages into the install root.
897
898 This function installs the packages listed in the supplied kickstart
899 into the install root. By default, the packages are installed from the
900 repository URLs specified in the kickstart.
901
902 repo_urls -- a dict which maps a repository name to a repository URL;
903 if supplied, this causes any repository URLs specified in
904 the kickstart to be overridden.
905
906 """
907
908 # initialize pkg list to install
909 if self.ks:
910 self.__sanity_check()
911
912 self._required_pkgs = \
913 kickstart.get_packages(self.ks, self._get_required_packages())
914 self._excluded_pkgs = \
915 kickstart.get_excluded(self.ks, self._get_excluded_packages())
916 self._required_groups = kickstart.get_groups(self.ks)
917 else:
918 self._required_pkgs = None
919 self._excluded_pkgs = None
920 self._required_groups = None
921
922 pkg_manager = self.get_pkg_manager()
923 pkg_manager.setup()
924
925 if hasattr(self, 'install_pkgs') and self.install_pkgs:
926 if 'debuginfo' in self.install_pkgs:
927 pkg_manager.install_debuginfo = True
928
929 for repo in kickstart.get_repos(self.ks, repo_urls):
930 (name, baseurl, mirrorlist, inc, exc,
931 proxy, proxy_username, proxy_password, debuginfo,
932 source, gpgkey, disable, ssl_verify, nocache,
933 cost, priority) = repo
934
935 yr = pkg_manager.addRepository(name, baseurl, mirrorlist, proxy,
936 proxy_username, proxy_password, inc, exc, ssl_verify,
937 nocache, cost, priority)
938
939 if kickstart.exclude_docs(self.ks):
940 rpm.addMacro("_excludedocs", "1")
941 rpm.addMacro("_dbpath", "/var/lib/rpm")
942 rpm.addMacro("__file_context_path", "%{nil}")
943 if kickstart.inst_langs(self.ks) != None:
944 rpm.addMacro("_install_langs", kickstart.inst_langs(self.ks))
945
946 try:
947 self.__preinstall_packages(pkg_manager)
948 self.__select_packages(pkg_manager)
949 self.__select_groups(pkg_manager)
950 self.__deselect_packages(pkg_manager)
951 self.__localinst_packages(pkg_manager)
952
953 BOOT_SAFEGUARD = 256L * 1024 * 1024 # 256M
954 checksize = self._root_fs_avail
955 if checksize:
956 checksize -= BOOT_SAFEGUARD
957 if self.target_arch:
958 pkg_manager._add_prob_flags(rpm.RPMPROB_FILTER_IGNOREARCH)
959 pkg_manager.runInstall(checksize)
960 except CreatorError, e:
961 raise
962 except KeyboardInterrupt:
963 raise
964 else:
965 self._pkgs_content = pkg_manager.getAllContent()
966 self._pkgs_license = pkg_manager.getPkgsLicense()
967 self._pkgs_vcsinfo = pkg_manager.getVcsInfo()
968 self.__attachment_packages(pkg_manager)
969 finally:
970 pkg_manager.close()
971
972 # hook post install
973 self.postinstall()
974
975 # do some clean up to avoid lvm info leakage. this sucks.
976 for subdir in ("cache", "backup", "archive"):
977 lvmdir = self._instroot + "/etc/lvm/" + subdir
978 try:
979 for f in os.listdir(lvmdir):
980 os.unlink(lvmdir + "/" + f)
981 except:
982 pass
983
984 def postinstall(self):
985 self.copy_attachment()
986
987 def __run_post_scripts(self):
988 msger.info("Running scripts ...")
989 if os.path.exists(self._instroot + "/tmp"):
990 shutil.rmtree(self._instroot + "/tmp")
991 os.mkdir (self._instroot + "/tmp", 0755)
992 for s in kickstart.get_post_scripts(self.ks):
993 (fd, path) = tempfile.mkstemp(prefix = "ks-script-",
994 dir = self._instroot + "/tmp")
995
996 s.script = s.script.replace("\r", "")
997 os.write(fd, s.script)
998 os.close(fd)
999 os.chmod(path, 0700)
1000
1001 env = self._get_post_scripts_env(s.inChroot)
1002
1003 if not s.inChroot:
1004 preexec = None
1005 script = path
1006 else:
1007 preexec = self._chroot
1008 script = "/tmp/" + os.path.basename(path)
1009
1010 try:
1011 try:
1012 subprocess.call([s.interp, script],
1013 preexec_fn = preexec,
1014 env = env,
1015 stdout = sys.stdout,
1016 stderr = sys.stderr)
1017 except OSError, (err, msg):
1018 raise CreatorError("Failed to execute %%post script "
1019 "with '%s' : %s" % (s.interp, msg))
1020 finally:
1021 os.unlink(path)
1022
1023 def __save_repo_keys(self, repodata):
1024 if not repodata:
1025 return None
1026
1027 gpgkeydir = "/etc/pki/rpm-gpg"
1028 fs.makedirs(self._instroot + gpgkeydir)
1029 for repo in repodata:
1030 if repo["repokey"]:
1031 repokey = gpgkeydir + "/RPM-GPG-KEY-%s" % repo["name"]
1032 shutil.copy(repo["repokey"], self._instroot + repokey)
1033
1034 def configure(self, repodata = None):
1035 """Configure the system image according to the kickstart.
1036
1037 This method applies the (e.g. keyboard or network) configuration
1038 specified in the kickstart and executes the kickstart %post scripts.
1039
1040 If necessary, it also prepares the image to be bootable by e.g.
1041 creating an initrd and bootloader configuration.
1042
1043 """
1044 ksh = self.ks.handler
1045
1046 msger.info('Applying configurations ...')
1047 try:
1048 kickstart.LanguageConfig(self._instroot).apply(ksh.lang)
1049 kickstart.KeyboardConfig(self._instroot).apply(ksh.keyboard)
1050 kickstart.TimezoneConfig(self._instroot).apply(ksh.timezone)
1051 #kickstart.AuthConfig(self._instroot).apply(ksh.authconfig)
1052 kickstart.FirewallConfig(self._instroot).apply(ksh.firewall)
1053 kickstart.RootPasswordConfig(self._instroot).apply(ksh.rootpw)
1054 kickstart.UserConfig(self._instroot).apply(ksh.user)
1055 kickstart.ServicesConfig(self._instroot).apply(ksh.services)
1056 kickstart.XConfig(self._instroot).apply(ksh.xconfig)
1057 kickstart.NetworkConfig(self._instroot).apply(ksh.network)
1058 kickstart.RPMMacroConfig(self._instroot).apply(self.ks)
1059 kickstart.DesktopConfig(self._instroot).apply(ksh.desktop)
1060 self.__save_repo_keys(repodata)
1061 kickstart.MoblinRepoConfig(self._instroot).apply(ksh.repo, repodata, self.repourl)
1062 except:
1063 msger.warning("Failed to apply configuration to image")
1064 raise
1065
1066 self._create_bootconfig()
1067 self.__run_post_scripts()
1068
1069 def launch_shell(self, launch):
1070 """Launch a shell in the install root.
1071
1072 This method is launches a bash shell chroot()ed in the install root;
1073 this can be useful for debugging.
1074
1075 """
1076 if launch:
1077 msger.info("Launching shell. Exit to continue.")
1078 subprocess.call(["/bin/bash"], preexec_fn = self._chroot)
1079
1080 def do_genchecksum(self, image_name):
1081 if not self._genchecksum:
1082 return
1083
1084 md5sum = misc.get_md5sum(image_name)
1085 with open(image_name + ".md5sum", "w") as f:
1086 f.write("%s %s" % (md5sum, os.path.basename(image_name)))
1087 self.outimage.append(image_name+".md5sum")
1088
1089 def package(self, destdir = "."):
1090 """Prepares the created image for final delivery.
1091
1092 In its simplest form, this method merely copies the install root to the
1093 supplied destination directory; other subclasses may choose to package
1094 the image by e.g. creating a bootable ISO containing the image and
1095 bootloader configuration.
1096
1097 destdir -- the directory into which the final image should be moved;
1098 this defaults to the current directory.
1099
1100 """
1101 self._stage_final_image()
1102
1103 if not os.path.exists(destdir):
1104 fs.makedirs(destdir)
1105
1106 if self._recording_pkgs:
1107 self._save_recording_pkgs(destdir)
1108
1109 # For image formats with two or multiple image files, it will be
1110 # better to put them under a directory
1111 if self.image_format in ("raw", "vmdk", "vdi", "nand", "mrstnand"):
1112 destdir = os.path.join(destdir, "%s-%s" \
1113 % (self.name, self.image_format))
1114 msger.debug("creating destination dir: %s" % destdir)
1115 fs.makedirs(destdir)
1116
1117 # Ensure all data is flushed to _outdir
1118 runner.quiet('sync')
1119
1120 misc.check_space_pre_cp(self._outdir, destdir)
1121 for f in os.listdir(self._outdir):
1122 shutil.move(os.path.join(self._outdir, f),
1123 os.path.join(destdir, f))
1124 self.outimage.append(os.path.join(destdir, f))
1125 self.do_genchecksum(os.path.join(destdir, f))
1126
1127 def print_outimage_info(self):
1128 msg = "The new image can be found here:\n"
1129 self.outimage.sort()
1130 for file in self.outimage:
1131 msg += ' %s\n' % os.path.abspath(file)
1132
1133 msger.info(msg)
1134
1135 def check_depend_tools(self):
1136 for tool in self._dep_checks:
1137 fs.find_binary_path(tool)
1138
1139 def package_output(self, image_format, destdir = ".", package="none"):
1140 if not package or package == "none":
1141 return
1142
1143 destdir = os.path.abspath(os.path.expanduser(destdir))
1144 (pkg, comp) = os.path.splitext(package)
1145 if comp:
1146 comp=comp.lstrip(".")
1147
1148 if pkg == "tar":
1149 if comp:
1150 dst = "%s/%s-%s.tar.%s" %\
1151 (destdir, self.name, image_format, comp)
1152 else:
1153 dst = "%s/%s-%s.tar" %\
1154 (destdir, self.name, image_format)
1155
1156 msger.info("creating %s" % dst)
1157 tar = tarfile.open(dst, "w:" + comp)
1158
1159 for file in self.outimage:
1160 msger.info("adding %s to %s" % (file, dst))
1161 tar.add(file,
1162 arcname=os.path.join("%s-%s" \
1163 % (self.name, image_format),
1164 os.path.basename(file)))
1165 if os.path.isdir(file):
1166 shutil.rmtree(file, ignore_errors = True)
1167 else:
1168 os.remove(file)
1169
1170 tar.close()
1171
1172 '''All the file in outimage has been packaged into tar.* file'''
1173 self.outimage = [dst]
1174
1175 def release_output(self, config, destdir, release):
1176 """ Create release directory and files
1177 """
1178
1179 def _rpath(fn):
1180 """ release path """
1181 return os.path.join(destdir, fn)
1182
1183 outimages = self.outimage
1184
1185 # new ks
1186 new_kspath = _rpath(self.name+'.ks')
1187 with open(config) as fr:
1188 with open(new_kspath, "w") as wf:
1189 # When building a release we want to make sure the .ks
1190 # file generates the same build even when --release not used.
1191 wf.write(fr.read().replace("@BUILD_ID@", release))
1192 outimages.append(new_kspath)
1193
1194 # save log file, logfile is only available in creator attrs
1195 if hasattr(self, 'logfile') and not self.logfile:
1196 log_path = _rpath(self.name + ".log")
1197 # touch the log file, else outimages will filter it out
1198 with open(log_path, 'w') as wf:
1199 wf.write('')
1200 msger.set_logfile(log_path)
1201 outimages.append(_rpath(self.name + ".log"))
1202
1203 # rename iso and usbimg
1204 for f in os.listdir(destdir):
1205 if f.endswith(".iso"):
1206 newf = f[:-4] + '.img'
1207 elif f.endswith(".usbimg"):
1208 newf = f[:-7] + '.img'
1209 else:
1210 continue
1211 os.rename(_rpath(f), _rpath(newf))
1212 outimages.append(_rpath(newf))
1213
1214 # generate MD5SUMS
1215 with open(_rpath("MD5SUMS"), "w") as wf:
1216 for f in os.listdir(destdir):
1217 if f == "MD5SUMS":
1218 continue
1219
1220 if os.path.isdir(os.path.join(destdir, f)):
1221 continue
1222
1223 md5sum = misc.get_md5sum(_rpath(f))
1224 # There needs to be two spaces between the sum and
1225 # filepath to match the syntax with md5sum.
1226 # This way also md5sum -c MD5SUMS can be used by users
1227 wf.write("%s *%s\n" % (md5sum, f))
1228
1229 outimages.append("%s/MD5SUMS" % destdir)
1230
1231 # Filter out the nonexist file
1232 for fp in outimages[:]:
1233 if not os.path.exists("%s" % fp):
1234 outimages.remove(fp)
1235
1236 def copy_kernel(self):
1237 """ Copy kernel files to the outimage directory.
1238 NOTE: This needs to be called before unmounting the instroot.
1239 """
1240
1241 if not self._need_copy_kernel:
1242 return
1243
1244 if not os.path.exists(self.destdir):
1245 os.makedirs(self.destdir)
1246
1247 for kernel in glob.glob("%s/boot/vmlinuz-*" % self._instroot):
1248 kernelfilename = "%s/%s-%s" % (self.destdir,
1249 self.name,
1250 os.path.basename(kernel))
1251 msger.info('copy kernel file %s as %s' % (os.path.basename(kernel),
1252 kernelfilename))
1253 shutil.copy(kernel, kernelfilename)
1254 self.outimage.append(kernelfilename)
1255
1256 def copy_attachment(self):
1257 """ Subclass implement it to handle attachment files
1258 NOTE: This needs to be called before unmounting the instroot.
1259 """
1260 pass
1261
1262 def get_pkg_manager(self):
1263 return self.pkgmgr(target_arch = self.target_arch,
1264 instroot = self._instroot,
1265 cachedir = self.cachedir)