summaryrefslogtreecommitdiffstats
path: root/scripts/lib/mic/imager
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/lib/mic/imager')
-rw-r--r--scripts/lib/mic/imager/__init__.py0
-rw-r--r--scripts/lib/mic/imager/baseimager.py1335
-rw-r--r--scripts/lib/mic/imager/fs.py99
-rw-r--r--scripts/lib/mic/imager/livecd.py750
-rw-r--r--scripts/lib/mic/imager/liveusb.py308
-rw-r--r--scripts/lib/mic/imager/loop.py418
-rw-r--r--scripts/lib/mic/imager/raw.py501
7 files changed, 3411 insertions, 0 deletions
diff --git a/scripts/lib/mic/imager/__init__.py b/scripts/lib/mic/imager/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/scripts/lib/mic/imager/__init__.py
diff --git a/scripts/lib/mic/imager/baseimager.py b/scripts/lib/mic/imager/baseimager.py
new file mode 100644
index 0000000000..6efc6c1294
--- /dev/null
+++ b/scripts/lib/mic/imager/baseimager.py
@@ -0,0 +1,1335 @@
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
20from __future__ import with_statement
21import os, sys
22import stat
23import tempfile
24import shutil
25import subprocess
26import re
27import tarfile
28import glob
29
30import rpm
31
32from mic import kickstart
33from mic import msger
34from mic.utils.errors import CreatorError, Abort
35from mic.utils import misc, grabber, runner, fs_related as fs
36
37class 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 subprocess.call([s.interp, script],
1083 preexec_fn = preexec,
1084 env = env,
1085 stdout = sys.stdout,
1086 stderr = sys.stderr)
1087 except OSError, (err, msg):
1088 raise CreatorError("Failed to execute %%post script "
1089 "with '%s' : %s" % (s.interp, msg))
1090 finally:
1091 os.unlink(path)
1092
1093 def __save_repo_keys(self, repodata):
1094 if not repodata:
1095 return None
1096
1097 gpgkeydir = "/etc/pki/rpm-gpg"
1098 fs.makedirs(self._instroot + gpgkeydir)
1099 for repo in repodata:
1100 if repo["repokey"]:
1101 repokey = gpgkeydir + "/RPM-GPG-KEY-%s" % repo["name"]
1102 shutil.copy(repo["repokey"], self._instroot + repokey)
1103
1104 def configure(self, repodata = None):
1105 """Configure the system image according to the kickstart.
1106
1107 This method applies the (e.g. keyboard or network) configuration
1108 specified in the kickstart and executes the kickstart %post scripts.
1109
1110 If necessary, it also prepares the image to be bootable by e.g.
1111 creating an initrd and bootloader configuration.
1112
1113 """
1114 ksh = self.ks.handler
1115
1116 msger.info('Applying configurations ...')
1117 try:
1118 kickstart.LanguageConfig(self._instroot).apply(ksh.lang)
1119 kickstart.KeyboardConfig(self._instroot).apply(ksh.keyboard)
1120 kickstart.TimezoneConfig(self._instroot).apply(ksh.timezone)
1121 #kickstart.AuthConfig(self._instroot).apply(ksh.authconfig)
1122 kickstart.FirewallConfig(self._instroot).apply(ksh.firewall)
1123 kickstart.RootPasswordConfig(self._instroot).apply(ksh.rootpw)
1124 kickstart.UserConfig(self._instroot).apply(ksh.user)
1125 kickstart.ServicesConfig(self._instroot).apply(ksh.services)
1126 kickstart.XConfig(self._instroot).apply(ksh.xconfig)
1127 kickstart.NetworkConfig(self._instroot).apply(ksh.network)
1128 kickstart.RPMMacroConfig(self._instroot).apply(self.ks)
1129 kickstart.DesktopConfig(self._instroot).apply(ksh.desktop)
1130 self.__save_repo_keys(repodata)
1131 kickstart.MoblinRepoConfig(self._instroot).apply(ksh.repo, repodata, self.repourl)
1132 except:
1133 msger.warning("Failed to apply configuration to image")
1134 raise
1135
1136 self._create_bootconfig()
1137 self.__run_post_scripts()
1138
1139 def launch_shell(self, launch):
1140 """Launch a shell in the install root.
1141
1142 This method is launches a bash shell chroot()ed in the install root;
1143 this can be useful for debugging.
1144
1145 """
1146 if launch:
1147 msger.info("Launching shell. Exit to continue.")
1148 subprocess.call(["/bin/bash"], preexec_fn = self._chroot)
1149
1150 def do_genchecksum(self, image_name):
1151 if not self._genchecksum:
1152 return
1153
1154 md5sum = misc.get_md5sum(image_name)
1155 with open(image_name + ".md5sum", "w") as f:
1156 f.write("%s %s" % (md5sum, os.path.basename(image_name)))
1157 self.outimage.append(image_name+".md5sum")
1158
1159 def package(self, destdir = "."):
1160 """Prepares the created image for final delivery.
1161
1162 In its simplest form, this method merely copies the install root to the
1163 supplied destination directory; other subclasses may choose to package
1164 the image by e.g. creating a bootable ISO containing the image and
1165 bootloader configuration.
1166
1167 destdir -- the directory into which the final image should be moved;
1168 this defaults to the current directory.
1169
1170 """
1171 self._stage_final_image()
1172
1173 if not os.path.exists(destdir):
1174 fs.makedirs(destdir)
1175
1176 if self._recording_pkgs:
1177 self._save_recording_pkgs(destdir)
1178
1179 # For image formats with two or multiple image files, it will be
1180 # better to put them under a directory
1181 if self.image_format in ("raw", "vmdk", "vdi", "nand", "mrstnand"):
1182 destdir = os.path.join(destdir, "%s-%s" \
1183 % (self.name, self.image_format))
1184 msger.debug("creating destination dir: %s" % destdir)
1185 fs.makedirs(destdir)
1186
1187 # Ensure all data is flushed to _outdir
1188 runner.quiet('sync')
1189
1190 misc.check_space_pre_cp(self._outdir, destdir)
1191 for f in os.listdir(self._outdir):
1192 shutil.move(os.path.join(self._outdir, f),
1193 os.path.join(destdir, f))
1194 self.outimage.append(os.path.join(destdir, f))
1195 self.do_genchecksum(os.path.join(destdir, f))
1196
1197 def print_outimage_info(self):
1198 msg = "The new image can be found here:\n"
1199 self.outimage.sort()
1200 for file in self.outimage:
1201 msg += ' %s\n' % os.path.abspath(file)
1202
1203 msger.info(msg)
1204
1205 def check_depend_tools(self):
1206 for tool in self._dep_checks:
1207 fs.find_binary_path(tool)
1208
1209 def package_output(self, image_format, destdir = ".", package="none"):
1210 if not package or package == "none":
1211 return
1212
1213 destdir = os.path.abspath(os.path.expanduser(destdir))
1214 (pkg, comp) = os.path.splitext(package)
1215 if comp:
1216 comp=comp.lstrip(".")
1217
1218 if pkg == "tar":
1219 if comp:
1220 dst = "%s/%s-%s.tar.%s" %\
1221 (destdir, self.name, image_format, comp)
1222 else:
1223 dst = "%s/%s-%s.tar" %\
1224 (destdir, self.name, image_format)
1225
1226 msger.info("creating %s" % dst)
1227 tar = tarfile.open(dst, "w:" + comp)
1228
1229 for file in self.outimage:
1230 msger.info("adding %s to %s" % (file, dst))
1231 tar.add(file,
1232 arcname=os.path.join("%s-%s" \
1233 % (self.name, image_format),
1234 os.path.basename(file)))
1235 if os.path.isdir(file):
1236 shutil.rmtree(file, ignore_errors = True)
1237 else:
1238 os.remove(file)
1239
1240 tar.close()
1241
1242 '''All the file in outimage has been packaged into tar.* file'''
1243 self.outimage = [dst]
1244
1245 def release_output(self, config, destdir, release):
1246 """ Create release directory and files
1247 """
1248
1249 def _rpath(fn):
1250 """ release path """
1251 return os.path.join(destdir, fn)
1252
1253 outimages = self.outimage
1254
1255 # new ks
1256 new_kspath = _rpath(self.name+'.ks')
1257 with open(config) as fr:
1258 with open(new_kspath, "w") as wf:
1259 # When building a release we want to make sure the .ks
1260 # file generates the same build even when --release not used.
1261 wf.write(fr.read().replace("@BUILD_ID@", release))
1262 outimages.append(new_kspath)
1263
1264 # save log file, logfile is only available in creator attrs
1265 if hasattr(self, 'logfile') and not self.logfile:
1266 log_path = _rpath(self.name + ".log")
1267 # touch the log file, else outimages will filter it out
1268 with open(log_path, 'w') as wf:
1269 wf.write('')
1270 msger.set_logfile(log_path)
1271 outimages.append(_rpath(self.name + ".log"))
1272
1273 # rename iso and usbimg
1274 for f in os.listdir(destdir):
1275 if f.endswith(".iso"):
1276 newf = f[:-4] + '.img'
1277 elif f.endswith(".usbimg"):
1278 newf = f[:-7] + '.img'
1279 else:
1280 continue
1281 os.rename(_rpath(f), _rpath(newf))
1282 outimages.append(_rpath(newf))
1283
1284 # generate MD5SUMS
1285 with open(_rpath("MD5SUMS"), "w") as wf:
1286 for f in os.listdir(destdir):
1287 if f == "MD5SUMS":
1288 continue
1289
1290 if os.path.isdir(os.path.join(destdir, f)):
1291 continue
1292
1293 md5sum = misc.get_md5sum(_rpath(f))
1294 # There needs to be two spaces between the sum and
1295 # filepath to match the syntax with md5sum.
1296 # This way also md5sum -c MD5SUMS can be used by users
1297 wf.write("%s *%s\n" % (md5sum, f))
1298
1299 outimages.append("%s/MD5SUMS" % destdir)
1300
1301 # Filter out the nonexist file
1302 for fp in outimages[:]:
1303 if not os.path.exists("%s" % fp):
1304 outimages.remove(fp)
1305
1306 def copy_kernel(self):
1307 """ Copy kernel files to the outimage directory.
1308 NOTE: This needs to be called before unmounting the instroot.
1309 """
1310
1311 if not self._need_copy_kernel:
1312 return
1313
1314 if not os.path.exists(self.destdir):
1315 os.makedirs(self.destdir)
1316
1317 for kernel in glob.glob("%s/boot/vmlinuz-*" % self._instroot):
1318 kernelfilename = "%s/%s-%s" % (self.destdir,
1319 self.name,
1320 os.path.basename(kernel))
1321 msger.info('copy kernel file %s as %s' % (os.path.basename(kernel),
1322 kernelfilename))
1323 shutil.copy(kernel, kernelfilename)
1324 self.outimage.append(kernelfilename)
1325
1326 def copy_attachment(self):
1327 """ Subclass implement it to handle attachment files
1328 NOTE: This needs to be called before unmounting the instroot.
1329 """
1330 pass
1331
1332 def get_pkg_manager(self):
1333 return self.pkgmgr(target_arch = self.target_arch,
1334 instroot = self._instroot,
1335 cachedir = self.cachedir)
diff --git a/scripts/lib/mic/imager/fs.py b/scripts/lib/mic/imager/fs.py
new file mode 100644
index 0000000000..d53b29cb47
--- /dev/null
+++ b/scripts/lib/mic/imager/fs.py
@@ -0,0 +1,99 @@
1#!/usr/bin/python -tt
2#
3# Copyright (c) 2011 Intel, Inc.
4#
5# This program is free software; you can redistribute it and/or modify it
6# under the terms of the GNU General Public License as published by the Free
7# Software Foundation; version 2 of the License
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12# for more details.
13#
14# You should have received a copy of the GNU General Public License along
15# with this program; if not, write to the Free Software Foundation, Inc., 59
16# Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17
18import os
19
20from mic import msger
21from mic.utils import runner, misc
22from mic.utils.errors import CreatorError
23from mic.utils.fs_related import find_binary_path
24from mic.imager.baseimager import BaseImageCreator
25
26class FsImageCreator(BaseImageCreator):
27 def __init__(self, cfgmgr = None, pkgmgr = None):
28 self.zips = {
29 "tar.bz2" : ""
30 }
31 BaseImageCreator.__init__(self, cfgmgr, pkgmgr)
32 self._fstype = None
33 self._fsopts = None
34 self._include_src = False
35
36 def package(self, destdir = "."):
37
38 ignores = ["/dev/fd",
39 "/dev/stdin",
40 "/dev/stdout",
41 "/dev/stderr",
42 "/etc/mtab"]
43
44 if not os.path.exists(destdir):
45 os.makedirs(destdir)
46
47 if self._recording_pkgs:
48 self._save_recording_pkgs(destdir)
49
50 if not self.pack_to:
51 fsdir = os.path.join(destdir, self.name)
52
53 misc.check_space_pre_cp(self._instroot, destdir)
54 msger.info("Copying %s to %s ..." % (self._instroot, fsdir))
55 runner.show(['cp', "-af", self._instroot, fsdir])
56
57 for exclude in ignores:
58 if os.path.exists(fsdir + exclude):
59 os.unlink(fsdir + exclude)
60
61 self.outimage.append(fsdir)
62
63 else:
64 (tar, comp) = os.path.splitext(self.pack_to)
65 try:
66 tarcreat = {'.tar': '-cf',
67 '.gz': '-czf',
68 '.bz2': '-cjf',
69 '.tgz': '-czf',
70 '.tbz': '-cjf'}[comp]
71 except KeyError:
72 raise CreatorError("Unsupported comression for this image type:"
73 " '%s', try '.tar', '.tar.gz', etc" % comp)
74
75 dst = os.path.join(destdir, self.pack_to)
76 msger.info("Pack rootfs to %s. Please wait..." % dst)
77
78 tar = find_binary_path('tar')
79 tar_cmdline = [tar, "--numeric-owner",
80 "--preserve-permissions",
81 "--preserve-order",
82 "--one-file-system",
83 "--directory",
84 self._instroot]
85 for ignore_entry in ignores:
86 if ignore_entry.startswith('/'):
87 ignore_entry = ignore_entry[1:]
88
89 tar_cmdline.append("--exclude=%s" % (ignore_entry))
90
91 tar_cmdline.extend([tarcreat, dst, "."])
92
93 rc = runner.show(tar_cmdline)
94 if rc:
95 raise CreatorError("Failed compress image with tar.bz2. "
96 "Cmdline: %s" % (" ".join(tar_cmdline)))
97
98 self.outimage.append(dst)
99
diff --git a/scripts/lib/mic/imager/livecd.py b/scripts/lib/mic/imager/livecd.py
new file mode 100644
index 0000000000..a992ee0706
--- /dev/null
+++ b/scripts/lib/mic/imager/livecd.py
@@ -0,0 +1,750 @@
1#!/usr/bin/python -tt
2#
3# Copyright (c) 2011 Intel, Inc.
4#
5# This program is free software; you can redistribute it and/or modify it
6# under the terms of the GNU General Public License as published by the Free
7# Software Foundation; version 2 of the License
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12# for more details.
13#
14# You should have received a copy of the GNU General Public License along
15# with this program; if not, write to the Free Software Foundation, Inc., 59
16# Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17
18import os, sys
19import glob
20import shutil
21
22from mic import kickstart, msger
23from mic.utils import fs_related, rpmmisc, runner, misc
24from mic.utils.errors import CreatorError
25from mic.imager.loop import LoopImageCreator
26
27
28class LiveImageCreatorBase(LoopImageCreator):
29 """A base class for LiveCD image creators.
30
31 This class serves as a base class for the architecture-specific LiveCD
32 image creator subclass, LiveImageCreator.
33
34 LiveImageCreator creates a bootable ISO containing the system image,
35 bootloader, bootloader configuration, kernel and initramfs.
36 """
37
38 def __init__(self, creatoropts = None, pkgmgr = None):
39 """Initialise a LiveImageCreator instance.
40
41 This method takes the same arguments as ImageCreator.__init__().
42 """
43 LoopImageCreator.__init__(self, creatoropts, pkgmgr)
44
45 #Controls whether to use squashfs to compress the image.
46 self.skip_compression = False
47
48 #Controls whether an image minimizing snapshot should be created.
49 #
50 #This snapshot can be used when copying the system image from the ISO in
51 #order to minimize the amount of data that needs to be copied; simply,
52 #it makes it possible to create a version of the image's filesystem with
53 #no spare space.
54 self.skip_minimize = False
55
56 #A flag which indicates i act as a convertor default false
57 self.actasconvertor = False
58
59 #The bootloader timeout from kickstart.
60 if self.ks:
61 self._timeout = kickstart.get_timeout(self.ks, 10)
62 else:
63 self._timeout = 10
64
65 #The default kernel type from kickstart.
66 if self.ks:
67 self._default_kernel = kickstart.get_default_kernel(self.ks,
68 "kernel")
69 else:
70 self._default_kernel = None
71
72 if self.ks:
73 parts = kickstart.get_partitions(self.ks)
74 if len(parts) > 1:
75 raise CreatorError("Can't support multi partitions in ks file "
76 "for this image type")
77 # FIXME: rename rootfs img to self.name,
78 # else can't find files when create iso
79 self._instloops[0]['name'] = self.name + ".img"
80
81 self.__isodir = None
82
83 self.__modules = ["=ata",
84 "sym53c8xx",
85 "aic7xxx",
86 "=usb",
87 "=firewire",
88 "=mmc",
89 "=pcmcia",
90 "mptsas"]
91 if self.ks:
92 self.__modules.extend(kickstart.get_modules(self.ks))
93
94 self._dep_checks.extend(["isohybrid",
95 "unsquashfs",
96 "mksquashfs",
97 "dd",
98 "genisoimage"])
99
100 #
101 # Hooks for subclasses
102 #
103 def _configure_bootloader(self, isodir):
104 """Create the architecture specific booloader configuration.
105
106 This is the hook where subclasses must create the booloader
107 configuration in order to allow a bootable ISO to be built.
108
109 isodir -- the directory where the contents of the ISO are to
110 be staged
111 """
112 raise CreatorError("Bootloader configuration is arch-specific, "
113 "but not implemented for this arch!")
114 def _get_menu_options(self):
115 """Return a menu options string for syslinux configuration.
116 """
117 if self.ks is None:
118 return "liveinst autoinst"
119 r = kickstart.get_menu_args(self.ks)
120 return r
121
122 def _get_kernel_options(self):
123 """Return a kernel options string for bootloader configuration.
124
125 This is the hook where subclasses may specify a set of kernel
126 options which should be included in the images bootloader
127 configuration.
128
129 A sensible default implementation is provided.
130 """
131
132 if self.ks is None:
133 r = "ro rd.live.image"
134 else:
135 r = kickstart.get_kernel_args(self.ks)
136
137 return r
138
139 def _get_mkisofs_options(self, isodir):
140 """Return the architecture specific mkisosfs options.
141
142 This is the hook where subclasses may specify additional arguments
143 to mkisofs, e.g. to enable a bootable ISO to be built.
144
145 By default, an empty list is returned.
146 """
147 return []
148
149 #
150 # Helpers for subclasses
151 #
152 def _has_checkisomd5(self):
153 """Check whether checkisomd5 is available in the install root."""
154 def _exists(path):
155 return os.path.exists(self._instroot + path)
156
157 if _exists("/usr/bin/checkisomd5") and os.path.exists("/usr/bin/implantisomd5"):
158 return True
159
160 return False
161
162 def __restore_file(self,path):
163 try:
164 os.unlink(path)
165 except:
166 pass
167 if os.path.exists(path + '.rpmnew'):
168 os.rename(path + '.rpmnew', path)
169
170 def _mount_instroot(self, base_on = None):
171 LoopImageCreator._mount_instroot(self, base_on)
172 self.__write_initrd_conf(self._instroot + "/etc/sysconfig/mkinitrd")
173 self.__write_dracut_conf(self._instroot + "/etc/dracut.conf.d/02livecd.conf")
174
175 def _unmount_instroot(self):
176 self.__restore_file(self._instroot + "/etc/sysconfig/mkinitrd")
177 self.__restore_file(self._instroot + "/etc/dracut.conf.d/02livecd.conf")
178 LoopImageCreator._unmount_instroot(self)
179
180 def __ensure_isodir(self):
181 if self.__isodir is None:
182 self.__isodir = self._mkdtemp("iso-")
183 return self.__isodir
184
185 def _get_isodir(self):
186 return self.__ensure_isodir()
187
188 def _set_isodir(self, isodir = None):
189 self.__isodir = isodir
190
191 def _create_bootconfig(self):
192 """Configure the image so that it's bootable."""
193 self._configure_bootloader(self.__ensure_isodir())
194
195 def _get_post_scripts_env(self, in_chroot):
196 env = LoopImageCreator._get_post_scripts_env(self, in_chroot)
197
198 if not in_chroot:
199 env["LIVE_ROOT"] = self.__ensure_isodir()
200
201 return env
202 def __write_dracut_conf(self, path):
203 if not os.path.exists(os.path.dirname(path)):
204 fs_related.makedirs(os.path.dirname(path))
205 f = open(path, "a")
206 f.write('add_dracutmodules+=" dmsquash-live pollcdrom "')
207 f.close()
208
209 def __write_initrd_conf(self, path):
210 content = ""
211 if not os.path.exists(os.path.dirname(path)):
212 fs_related.makedirs(os.path.dirname(path))
213 f = open(path, "w")
214
215 content += 'LIVEOS="yes"\n'
216 content += 'PROBE="no"\n'
217 content += 'MODULES+="squashfs ext3 ext2 vfat msdos "\n'
218 content += 'MODULES+="sr_mod sd_mod ide-cd cdrom "\n'
219
220 for module in self.__modules:
221 if module == "=usb":
222 content += 'MODULES+="ehci_hcd uhci_hcd ohci_hcd "\n'
223 content += 'MODULES+="usb_storage usbhid "\n'
224 elif module == "=firewire":
225 content += 'MODULES+="firewire-sbp2 firewire-ohci "\n'
226 content += 'MODULES+="sbp2 ohci1394 ieee1394 "\n'
227 elif module == "=mmc":
228 content += 'MODULES+="mmc_block sdhci sdhci-pci "\n'
229 elif module == "=pcmcia":
230 content += 'MODULES+="pata_pcmcia "\n'
231 else:
232 content += 'MODULES+="' + module + ' "\n'
233 f.write(content)
234 f.close()
235
236 def __create_iso(self, isodir):
237 iso = self._outdir + "/" + self.name + ".iso"
238 genisoimage = fs_related.find_binary_path("genisoimage")
239 args = [genisoimage,
240 "-J", "-r",
241 "-hide-rr-moved", "-hide-joliet-trans-tbl",
242 "-V", self.fslabel,
243 "-o", iso]
244
245 args.extend(self._get_mkisofs_options(isodir))
246
247 args.append(isodir)
248
249 if runner.show(args) != 0:
250 raise CreatorError("ISO creation failed!")
251
252 """ It should be ok still even if you haven't isohybrid """
253 isohybrid = None
254 try:
255 isohybrid = fs_related.find_binary_path("isohybrid")
256 except:
257 pass
258
259 if isohybrid:
260 args = [isohybrid, "-partok", iso ]
261 if runner.show(args) != 0:
262 raise CreatorError("Hybrid ISO creation failed!")
263
264 self.__implant_md5sum(iso)
265
266 def __implant_md5sum(self, iso):
267 """Implant an isomd5sum."""
268 if os.path.exists("/usr/bin/implantisomd5"):
269 implantisomd5 = "/usr/bin/implantisomd5"
270 else:
271 msger.warning("isomd5sum not installed; not setting up mediacheck")
272 implantisomd5 = ""
273 return
274
275 runner.show([implantisomd5, iso])
276
277 def _stage_final_image(self):
278 try:
279 fs_related.makedirs(self.__ensure_isodir() + "/LiveOS")
280
281 minimal_size = self._resparse()
282
283 if not self.skip_minimize:
284 fs_related.create_image_minimizer(self.__isodir + \
285 "/LiveOS/osmin.img",
286 self._image,
287 minimal_size)
288
289 if self.skip_compression:
290 shutil.move(self._image, self.__isodir + "/LiveOS/ext3fs.img")
291 else:
292 fs_related.makedirs(os.path.join(
293 os.path.dirname(self._image),
294 "LiveOS"))
295 shutil.move(self._image,
296 os.path.join(os.path.dirname(self._image),
297 "LiveOS", "ext3fs.img"))
298 fs_related.mksquashfs(os.path.dirname(self._image),
299 self.__isodir + "/LiveOS/squashfs.img")
300
301 self.__create_iso(self.__isodir)
302
303 if self.pack_to:
304 isoimg = os.path.join(self._outdir, self.name + ".iso")
305 packimg = os.path.join(self._outdir, self.pack_to)
306 misc.packing(packimg, isoimg)
307 os.unlink(isoimg)
308
309 finally:
310 shutil.rmtree(self.__isodir, ignore_errors = True)
311 self.__isodir = None
312
313class x86LiveImageCreator(LiveImageCreatorBase):
314 """ImageCreator for x86 machines"""
315 def _get_mkisofs_options(self, isodir):
316 return [ "-b", "isolinux/isolinux.bin",
317 "-c", "isolinux/boot.cat",
318 "-no-emul-boot", "-boot-info-table",
319 "-boot-load-size", "4" ]
320
321 def _get_required_packages(self):
322 return ["syslinux", "syslinux-extlinux"] + \
323 LiveImageCreatorBase._get_required_packages(self)
324
325 def _get_isolinux_stanzas(self, isodir):
326 return ""
327
328 def __find_syslinux_menu(self):
329 for menu in ["vesamenu.c32", "menu.c32"]:
330 if os.path.isfile(self._instroot + "/usr/share/syslinux/" + menu):
331 return menu
332
333 raise CreatorError("syslinux not installed : "
334 "no suitable /usr/share/syslinux/*menu.c32 found")
335
336 def __find_syslinux_mboot(self):
337 #
338 # We only need the mboot module if we have any xen hypervisors
339 #
340 if not glob.glob(self._instroot + "/boot/xen.gz*"):
341 return None
342
343 return "mboot.c32"
344
345 def __copy_syslinux_files(self, isodir, menu, mboot = None):
346 files = ["isolinux.bin", menu]
347 if mboot:
348 files += [mboot]
349
350 for f in files:
351 path = self._instroot + "/usr/share/syslinux/" + f
352
353 if not os.path.isfile(path):
354 raise CreatorError("syslinux not installed : "
355 "%s not found" % path)
356
357 shutil.copy(path, isodir + "/isolinux/")
358
359 def __copy_syslinux_background(self, isodest):
360 background_path = self._instroot + \
361 "/usr/share/branding/default/syslinux/syslinux-vesa-splash.jpg"
362
363 if not os.path.exists(background_path):
364 return False
365
366 shutil.copyfile(background_path, isodest)
367
368 return True
369
370 def __copy_kernel_and_initramfs(self, isodir, version, index):
371 bootdir = self._instroot + "/boot"
372 isDracut = False
373
374 if self._alt_initrd_name:
375 src_initrd_path = os.path.join(bootdir, self._alt_initrd_name)
376 else:
377 if os.path.exists(bootdir + "/initramfs-" + version + ".img"):
378 src_initrd_path = os.path.join(bootdir, "initramfs-" +version+ ".img")
379 isDracut = True
380 else:
381 src_initrd_path = os.path.join(bootdir, "initrd-" +version+ ".img")
382
383 try:
384 msger.debug("copy %s to %s" % (bootdir + "/vmlinuz-" + version, isodir + "/isolinux/vmlinuz" + index))
385 shutil.copyfile(bootdir + "/vmlinuz-" + version,
386 isodir + "/isolinux/vmlinuz" + index)
387
388 msger.debug("copy %s to %s" % (src_initrd_path, isodir + "/isolinux/initrd" + index + ".img"))
389 shutil.copyfile(src_initrd_path,
390 isodir + "/isolinux/initrd" + index + ".img")
391 except:
392 raise CreatorError("Unable to copy valid kernels or initrds, "
393 "please check the repo.")
394
395 is_xen = False
396 if os.path.exists(bootdir + "/xen.gz-" + version[:-3]):
397 shutil.copyfile(bootdir + "/xen.gz-" + version[:-3],
398 isodir + "/isolinux/xen" + index + ".gz")
399 is_xen = True
400
401 return (is_xen,isDracut)
402
403 def __is_default_kernel(self, kernel, kernels):
404 if len(kernels) == 1:
405 return True
406
407 if kernel == self._default_kernel:
408 return True
409
410 if kernel.startswith("kernel-") and kernel[7:] == self._default_kernel:
411 return True
412
413 return False
414
415 def __get_basic_syslinux_config(self, **args):
416 return """
417default %(menu)s
418timeout %(timeout)d
419
420%(background)s
421menu title Welcome to %(distroname)s!
422menu color border 0 #ffffffff #00000000
423menu color sel 7 #ff000000 #ffffffff
424menu color title 0 #ffffffff #00000000
425menu color tabmsg 0 #ffffffff #00000000
426menu color unsel 0 #ffffffff #00000000
427menu color hotsel 0 #ff000000 #ffffffff
428menu color hotkey 7 #ffffffff #ff000000
429menu color timeout_msg 0 #ffffffff #00000000
430menu color timeout 0 #ffffffff #00000000
431menu color cmdline 0 #ffffffff #00000000
432menu hidden
433menu clear
434""" % args
435
436 def __get_image_stanza(self, is_xen, isDracut, **args):
437 if isDracut:
438 args["rootlabel"] = "live:CDLABEL=%(fslabel)s" % args
439 else:
440 args["rootlabel"] = "CDLABEL=%(fslabel)s" % args
441 if not is_xen:
442 template = """label %(short)s
443 menu label %(long)s
444 kernel vmlinuz%(index)s
445 append initrd=initrd%(index)s.img root=%(rootlabel)s rootfstype=iso9660 %(liveargs)s %(extra)s
446"""
447 else:
448 template = """label %(short)s
449 menu label %(long)s
450 kernel mboot.c32
451 append xen%(index)s.gz --- vmlinuz%(index)s root=%(rootlabel)s rootfstype=iso9660 %(liveargs)s %(extra)s --- initrd%(index)s.img
452"""
453 return template % args
454
455 def __get_image_stanzas(self, isodir):
456 versions = []
457 kernels = self._get_kernel_versions()
458 for kernel in kernels:
459 for version in kernels[kernel]:
460 versions.append(version)
461
462 if not versions:
463 raise CreatorError("Unable to find valid kernels, "
464 "please check the repo")
465
466 kernel_options = self._get_kernel_options()
467
468 """ menu can be customized highly, the format is:
469
470 short_name1:long_name1:extra_opts1;short_name2:long_name2:extra_opts2
471
472 e.g.: autoinst:InstallationOnly:systemd.unit=installer-graphical.service
473 but in order to keep compatible with old format, these are still ok:
474
475 liveinst autoinst
476 liveinst;autoinst
477 liveinst::;autoinst::
478 """
479 oldmenus = {"basic": {
480 "short": "basic",
481 "long": "Installation Only (Text based)",
482 "extra": "basic nosplash 4"
483 },
484 "liveinst": {
485 "short": "liveinst",
486 "long": "Installation Only",
487 "extra": "liveinst nosplash 4"
488 },
489 "autoinst": {
490 "short": "autoinst",
491 "long": "Autoinstall (Deletes all existing content)",
492 "extra": "autoinst nosplash 4"
493 },
494 "netinst": {
495 "short": "netinst",
496 "long": "Network Installation",
497 "extra": "netinst 4"
498 },
499 "verify": {
500 "short": "check",
501 "long": "Verify and",
502 "extra": "check"
503 }
504 }
505 menu_options = self._get_menu_options()
506 menus = menu_options.split(";")
507 for i in range(len(menus)):
508 menus[i] = menus[i].split(":")
509 if len(menus) == 1 and len(menus[0]) == 1:
510 """ Keep compatible with the old usage way """
511 menus = menu_options.split()
512 for i in range(len(menus)):
513 menus[i] = [menus[i]]
514
515 cfg = ""
516
517 default_version = None
518 default_index = None
519 index = "0"
520 netinst = None
521 for version in versions:
522 (is_xen, isDracut) = self.__copy_kernel_and_initramfs(isodir, version, index)
523 if index == "0":
524 self._isDracut = isDracut
525
526 default = self.__is_default_kernel(kernel, kernels)
527
528 if default:
529 long = "Boot %s" % self.distro_name
530 elif kernel.startswith("kernel-"):
531 long = "Boot %s(%s)" % (self.name, kernel[7:])
532 else:
533 long = "Boot %s(%s)" % (self.name, kernel)
534
535 oldmenus["verify"]["long"] = "%s %s" % (oldmenus["verify"]["long"],
536 long)
537 # tell dracut not to ask for LUKS passwords or activate mdraid sets
538 if isDracut:
539 kern_opts = kernel_options + " rd.luks=0 rd.md=0 rd.dm=0"
540 else:
541 kern_opts = kernel_options
542
543 cfg += self.__get_image_stanza(is_xen, isDracut,
544 fslabel = self.fslabel,
545 liveargs = kern_opts,
546 long = long,
547 short = "linux" + index,
548 extra = "",
549 index = index)
550
551 if default:
552 cfg += "menu default\n"
553 default_version = version
554 default_index = index
555
556 for menu in menus:
557 if not menu[0]:
558 continue
559 short = menu[0] + index
560
561 if len(menu) >= 2:
562 long = menu[1]
563 else:
564 if menu[0] in oldmenus.keys():
565 if menu[0] == "verify" and not self._has_checkisomd5():
566 continue
567 if menu[0] == "netinst":
568 netinst = oldmenus[menu[0]]
569 continue
570 long = oldmenus[menu[0]]["long"]
571 extra = oldmenus[menu[0]]["extra"]
572 else:
573 long = short.upper() + " X" + index
574 extra = ""
575
576 if len(menu) >= 3:
577 extra = menu[2]
578
579 cfg += self.__get_image_stanza(is_xen, isDracut,
580 fslabel = self.fslabel,
581 liveargs = kernel_options,
582 long = long,
583 short = short,
584 extra = extra,
585 index = index)
586
587 index = str(int(index) + 1)
588
589 if not default_version:
590 default_version = versions[0]
591 if not default_index:
592 default_index = "0"
593
594 if netinst:
595 cfg += self.__get_image_stanza(is_xen, isDracut,
596 fslabel = self.fslabel,
597 liveargs = kernel_options,
598 long = netinst["long"],
599 short = netinst["short"],
600 extra = netinst["extra"],
601 index = default_index)
602
603 return cfg
604
605 def __get_memtest_stanza(self, isodir):
606 memtest = glob.glob(self._instroot + "/boot/memtest86*")
607 if not memtest:
608 return ""
609
610 shutil.copyfile(memtest[0], isodir + "/isolinux/memtest")
611
612 return """label memtest
613 menu label Memory Test
614 kernel memtest
615"""
616
617 def __get_local_stanza(self, isodir):
618 return """label local
619 menu label Boot from local drive
620 localboot 0xffff
621"""
622
623 def _configure_syslinux_bootloader(self, isodir):
624 """configure the boot loader"""
625 fs_related.makedirs(isodir + "/isolinux")
626
627 menu = self.__find_syslinux_menu()
628
629 self.__copy_syslinux_files(isodir, menu,
630 self.__find_syslinux_mboot())
631
632 background = ""
633 if self.__copy_syslinux_background(isodir + "/isolinux/splash.jpg"):
634 background = "menu background splash.jpg"
635
636 cfg = self.__get_basic_syslinux_config(menu = menu,
637 background = background,
638 name = self.name,
639 timeout = self._timeout * 10,
640 distroname = self.distro_name)
641
642 cfg += self.__get_image_stanzas(isodir)
643 cfg += self.__get_memtest_stanza(isodir)
644 cfg += self.__get_local_stanza(isodir)
645 cfg += self._get_isolinux_stanzas(isodir)
646
647 cfgf = open(isodir + "/isolinux/isolinux.cfg", "w")
648 cfgf.write(cfg)
649 cfgf.close()
650
651 def __copy_efi_files(self, isodir):
652 if not os.path.exists(self._instroot + "/boot/efi/EFI/redhat/grub.efi"):
653 return False
654 shutil.copy(self._instroot + "/boot/efi/EFI/redhat/grub.efi",
655 isodir + "/EFI/boot/grub.efi")
656 shutil.copy(self._instroot + "/boot/grub/splash.xpm.gz",
657 isodir + "/EFI/boot/splash.xpm.gz")
658
659 return True
660
661 def __get_basic_efi_config(self, **args):
662 return """
663default=0
664splashimage=/EFI/boot/splash.xpm.gz
665timeout %(timeout)d
666hiddenmenu
667
668""" %args
669
670 def __get_efi_image_stanza(self, **args):
671 return """title %(long)s
672 kernel /EFI/boot/vmlinuz%(index)s root=CDLABEL=%(fslabel)s rootfstype=iso9660 %(liveargs)s %(extra)s
673 initrd /EFI/boot/initrd%(index)s.img
674""" %args
675
676 def __get_efi_image_stanzas(self, isodir, name):
677 # FIXME: this only supports one kernel right now...
678
679 kernel_options = self._get_kernel_options()
680 checkisomd5 = self._has_checkisomd5()
681
682 cfg = ""
683
684 for index in range(0, 9):
685 # we don't support xen kernels
686 if os.path.exists("%s/EFI/boot/xen%d.gz" %(isodir, index)):
687 continue
688 cfg += self.__get_efi_image_stanza(fslabel = self.fslabel,
689 liveargs = kernel_options,
690 long = name,
691 extra = "", index = index)
692 if checkisomd5:
693 cfg += self.__get_efi_image_stanza(
694 fslabel = self.fslabel,
695 liveargs = kernel_options,
696 long = "Verify and Boot " + name,
697 extra = "check",
698 index = index)
699 break
700
701 return cfg
702
703 def _configure_efi_bootloader(self, isodir):
704 """Set up the configuration for an EFI bootloader"""
705 fs_related.makedirs(isodir + "/EFI/boot")
706
707 if not self.__copy_efi_files(isodir):
708 shutil.rmtree(isodir + "/EFI")
709 return
710
711 for f in os.listdir(isodir + "/isolinux"):
712 os.link("%s/isolinux/%s" %(isodir, f),
713 "%s/EFI/boot/%s" %(isodir, f))
714
715
716 cfg = self.__get_basic_efi_config(name = self.name,
717 timeout = self._timeout)
718 cfg += self.__get_efi_image_stanzas(isodir, self.name)
719
720 cfgf = open(isodir + "/EFI/boot/grub.conf", "w")
721 cfgf.write(cfg)
722 cfgf.close()
723
724 # first gen mactel machines get the bootloader name wrong apparently
725 if rpmmisc.getBaseArch() == "i386":
726 os.link(isodir + "/EFI/boot/grub.efi",
727 isodir + "/EFI/boot/boot.efi")
728 os.link(isodir + "/EFI/boot/grub.conf",
729 isodir + "/EFI/boot/boot.conf")
730
731 # for most things, we want them named boot$efiarch
732 efiarch = {"i386": "ia32", "x86_64": "x64"}
733 efiname = efiarch[rpmmisc.getBaseArch()]
734 os.rename(isodir + "/EFI/boot/grub.efi",
735 isodir + "/EFI/boot/boot%s.efi" %(efiname,))
736 os.link(isodir + "/EFI/boot/grub.conf",
737 isodir + "/EFI/boot/boot%s.conf" %(efiname,))
738
739
740 def _configure_bootloader(self, isodir):
741 self._configure_syslinux_bootloader(isodir)
742 self._configure_efi_bootloader(isodir)
743
744arch = rpmmisc.getBaseArch()
745if arch in ("i386", "x86_64"):
746 LiveCDImageCreator = x86LiveImageCreator
747elif arch.startswith("arm"):
748 LiveCDImageCreator = LiveImageCreatorBase
749else:
750 raise CreatorError("Architecture not supported!")
diff --git a/scripts/lib/mic/imager/liveusb.py b/scripts/lib/mic/imager/liveusb.py
new file mode 100644
index 0000000000..a909928a4c
--- /dev/null
+++ b/scripts/lib/mic/imager/liveusb.py
@@ -0,0 +1,308 @@
1#!/usr/bin/python -tt
2#
3# Copyright (c) 2011 Intel, Inc.
4#
5# This program is free software; you can redistribute it and/or modify it
6# under the terms of the GNU General Public License as published by the Free
7# Software Foundation; version 2 of the License
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12# for more details.
13#
14# You should have received a copy of the GNU General Public License along
15# with this program; if not, write to the Free Software Foundation, Inc., 59
16# Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17
18import os
19import shutil
20import re
21
22from mic import msger
23from mic.utils import misc, fs_related, runner
24from mic.utils.errors import CreatorError, MountError
25from mic.utils.partitionedfs import PartitionedMount
26from mic.imager.livecd import LiveCDImageCreator
27
28
29class LiveUSBImageCreator(LiveCDImageCreator):
30 def __init__(self, *args):
31 LiveCDImageCreator.__init__(self, *args)
32
33 self._dep_checks.extend(["kpartx", "parted"])
34
35 # remove dependency of genisoimage in parent class
36 if "genisoimage" in self._dep_checks:
37 self._dep_checks.remove("genisoimage")
38
39 def _create_usbimg(self, isodir):
40 overlaysizemb = 64 #default
41 #skipcompress = self.skip_compression?
42 fstype = "vfat"
43 homesizemb=0
44 swapsizemb=0
45 homefile="home.img"
46 plussize=128
47 kernelargs=None
48
49 if fstype == 'vfat':
50 if overlaysizemb > 2047:
51 raise CreatorError("Can't have an overlay of 2048MB or "
52 "greater on VFAT")
53
54 if homesizemb > 2047:
55 raise CreatorError("Can't have an home overlay of 2048MB or "
56 "greater on VFAT")
57
58 if swapsizemb > 2047:
59 raise CreatorError("Can't have an swap overlay of 2048MB or "
60 "greater on VFAT")
61
62 livesize = misc.get_file_size(isodir + "/LiveOS")
63
64 usbimgsize = (overlaysizemb + \
65 homesizemb + \
66 swapsizemb + \
67 livesize + \
68 plussize) * 1024L * 1024L
69
70 disk = fs_related.SparseLoopbackDisk("%s/%s.usbimg" \
71 % (self._outdir, self.name),
72 usbimgsize)
73 usbmnt = self._mkdtemp("usb-mnt")
74 usbloop = PartitionedMount(usbmnt)
75 usbloop.add_disk('/dev/sdb', disk)
76
77 usbloop.add_partition(usbimgsize/1024/1024,
78 "/dev/sdb",
79 "/",
80 fstype,
81 boot=True)
82
83 usbloop.mount()
84
85 try:
86 fs_related.makedirs(usbmnt + "/LiveOS")
87
88 if os.path.exists(isodir + "/LiveOS/squashfs.img"):
89 shutil.copyfile(isodir + "/LiveOS/squashfs.img",
90 usbmnt + "/LiveOS/squashfs.img")
91 else:
92 fs_related.mksquashfs(os.path.dirname(self._image),
93 usbmnt + "/LiveOS/squashfs.img")
94
95 if os.path.exists(isodir + "/LiveOS/osmin.img"):
96 shutil.copyfile(isodir + "/LiveOS/osmin.img",
97 usbmnt + "/LiveOS/osmin.img")
98
99 if fstype == "vfat" or fstype == "msdos":
100 uuid = usbloop.partitions[0]['mount'].uuid
101 label = usbloop.partitions[0]['mount'].fslabel
102 usblabel = "UUID=%s-%s" % (uuid[0:4], uuid[4:8])
103 overlaysuffix = "-%s-%s-%s" % (label, uuid[0:4], uuid[4:8])
104 else:
105 diskmount = usbloop.partitions[0]['mount']
106 usblabel = "UUID=%s" % diskmount.uuid
107 overlaysuffix = "-%s-%s" % (diskmount.fslabel, diskmount.uuid)
108
109 args = ['cp', "-Rf", isodir + "/isolinux", usbmnt + "/syslinux"]
110 rc = runner.show(args)
111 if rc:
112 raise CreatorError("Can't copy isolinux directory %s" \
113 % (isodir + "/isolinux/*"))
114
115 if os.path.isfile("/usr/share/syslinux/isolinux.bin"):
116 syslinux_path = "/usr/share/syslinux"
117 elif os.path.isfile("/usr/lib/syslinux/isolinux.bin"):
118 syslinux_path = "/usr/lib/syslinux"
119 else:
120 raise CreatorError("syslinux not installed : "
121 "cannot find syslinux installation path")
122
123 for f in ("isolinux.bin", "vesamenu.c32"):
124 path = os.path.join(syslinux_path, f)
125 if os.path.isfile(path):
126 args = ['cp', path, usbmnt + "/syslinux/"]
127 rc = runner.show(args)
128 if rc:
129 raise CreatorError("Can't copy syslinux file " + path)
130 else:
131 raise CreatorError("syslinux not installed: "
132 "syslinux file %s not found" % path)
133
134 fd = open(isodir + "/isolinux/isolinux.cfg", "r")
135 text = fd.read()
136 fd.close()
137 pattern = re.compile('CDLABEL=[^ ]*')
138 text = pattern.sub(usblabel, text)
139 pattern = re.compile('rootfstype=[^ ]*')
140 text = pattern.sub("rootfstype=" + fstype, text)
141 if kernelargs:
142 text = text.replace("rd.live.image", "rd.live.image " + kernelargs)
143
144 if overlaysizemb > 0:
145 msger.info("Initializing persistent overlay file")
146 overfile = "overlay" + overlaysuffix
147 if fstype == "vfat":
148 args = ['dd',
149 "if=/dev/zero",
150 "of=" + usbmnt + "/LiveOS/" + overfile,
151 "count=%d" % overlaysizemb,
152 "bs=1M"]
153 else:
154 args = ['dd',
155 "if=/dev/null",
156 "of=" + usbmnt + "/LiveOS/" + overfile,
157 "count=1",
158 "bs=1M",
159 "seek=%d" % overlaysizemb]
160 rc = runner.show(args)
161 if rc:
162 raise CreatorError("Can't create overlay file")
163 text = text.replace("rd.live.image", "rd.live.image rd.live.overlay=" + usblabel)
164 text = text.replace(" ro ", " rw ")
165
166 if swapsizemb > 0:
167 msger.info("Initializing swap file")
168 swapfile = usbmnt + "/LiveOS/" + "swap.img"
169 args = ['dd',
170 "if=/dev/zero",
171 "of=" + swapfile,
172 "count=%d" % swapsizemb,
173 "bs=1M"]
174 rc = runner.show(args)
175 if rc:
176 raise CreatorError("Can't create swap file")
177 args = ["mkswap", "-f", swapfile]
178 rc = runner.show(args)
179 if rc:
180 raise CreatorError("Can't mkswap on swap file")
181
182 if homesizemb > 0:
183 msger.info("Initializing persistent /home")
184 homefile = usbmnt + "/LiveOS/" + homefile
185 if fstype == "vfat":
186 args = ['dd',
187 "if=/dev/zero",
188 "of=" + homefile,
189 "count=%d" % homesizemb,
190 "bs=1M"]
191 else:
192 args = ['dd',
193 "if=/dev/null",
194 "of=" + homefile,
195 "count=1",
196 "bs=1M",
197 "seek=%d" % homesizemb]
198 rc = runner.show(args)
199 if rc:
200 raise CreatorError("Can't create home file")
201
202 mkfscmd = fs_related.find_binary_path("/sbin/mkfs." + fstype)
203 if fstype == "ext2" or fstype == "ext3":
204 args = [mkfscmd, "-F", "-j", homefile]
205 else:
206 args = [mkfscmd, homefile]
207 rc = runner.show(args)
208 if rc:
209 raise CreatorError("Can't mke2fs home file")
210 if fstype == "ext2" or fstype == "ext3":
211 tune2fs = fs_related.find_binary_path("tune2fs")
212 args = [tune2fs,
213 "-c0",
214 "-i0",
215 "-ouser_xattr,acl",
216 homefile]
217 rc = runner.show(args)
218 if rc:
219 raise CreatorError("Can't tune2fs home file")
220
221 if fstype == "vfat" or fstype == "msdos":
222 syslinuxcmd = fs_related.find_binary_path("syslinux")
223 syslinuxcfg = usbmnt + "/syslinux/syslinux.cfg"
224 args = [syslinuxcmd,
225 "-d",
226 "syslinux",
227 usbloop.partitions[0]["device"]]
228
229 elif fstype == "ext2" or fstype == "ext3":
230 extlinuxcmd = fs_related.find_binary_path("extlinux")
231 syslinuxcfg = usbmnt + "/syslinux/extlinux.conf"
232 args = [extlinuxcmd,
233 "-i",
234 usbmnt + "/syslinux"]
235
236 else:
237 raise CreatorError("Invalid file system type: %s" % (fstype))
238
239 os.unlink(usbmnt + "/syslinux/isolinux.cfg")
240 fd = open(syslinuxcfg, "w")
241 fd.write(text)
242 fd.close()
243 rc = runner.show(args)
244 if rc:
245 raise CreatorError("Can't install boot loader.")
246
247 finally:
248 usbloop.unmount()
249 usbloop.cleanup()
250
251 # Need to do this after image is unmounted and device mapper is closed
252 msger.info("set MBR")
253 mbrfile = "/usr/lib/syslinux/mbr.bin"
254 if not os.path.exists(mbrfile):
255 mbrfile = "/usr/share/syslinux/mbr.bin"
256 if not os.path.exists(mbrfile):
257 raise CreatorError("mbr.bin file didn't exist.")
258 mbrsize = os.path.getsize(mbrfile)
259 outimg = "%s/%s.usbimg" % (self._outdir, self.name)
260
261 args = ['dd',
262 "if=" + mbrfile,
263 "of=" + outimg,
264 "seek=0",
265 "conv=notrunc",
266 "bs=1",
267 "count=%d" % (mbrsize)]
268 rc = runner.show(args)
269 if rc:
270 raise CreatorError("Can't set MBR.")
271
272 def _stage_final_image(self):
273 try:
274 isodir = self._get_isodir()
275 fs_related.makedirs(isodir + "/LiveOS")
276
277 minimal_size = self._resparse()
278
279 if not self.skip_minimize:
280 fs_related.create_image_minimizer(isodir + "/LiveOS/osmin.img",
281 self._image,
282 minimal_size)
283
284 if self.skip_compression:
285 shutil.move(self._image,
286 isodir + "/LiveOS/ext3fs.img")
287 else:
288 fs_related.makedirs(os.path.join(
289 os.path.dirname(self._image),
290 "LiveOS"))
291 shutil.move(self._image,
292 os.path.join(os.path.dirname(self._image),
293 "LiveOS", "ext3fs.img"))
294 fs_related.mksquashfs(os.path.dirname(self._image),
295 isodir + "/LiveOS/squashfs.img")
296
297 self._create_usbimg(isodir)
298
299 if self.pack_to:
300 usbimg = os.path.join(self._outdir, self.name + ".usbimg")
301 packimg = os.path.join(self._outdir, self.pack_to)
302 misc.packing(packimg, usbimg)
303 os.unlink(usbimg)
304
305 finally:
306 shutil.rmtree(isodir, ignore_errors = True)
307 self._set_isodir(None)
308
diff --git a/scripts/lib/mic/imager/loop.py b/scripts/lib/mic/imager/loop.py
new file mode 100644
index 0000000000..4d05ef271d
--- /dev/null
+++ b/scripts/lib/mic/imager/loop.py
@@ -0,0 +1,418 @@
1#!/usr/bin/python -tt
2#
3# Copyright (c) 2011 Intel, Inc.
4#
5# This program is free software; you can redistribute it and/or modify it
6# under the terms of the GNU General Public License as published by the Free
7# Software Foundation; version 2 of the License
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12# for more details.
13#
14# You should have received a copy of the GNU General Public License along
15# with this program; if not, write to the Free Software Foundation, Inc., 59
16# Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17
18import os
19import glob
20import shutil
21
22from mic import kickstart, msger
23from mic.utils.errors import CreatorError, MountError
24from mic.utils import misc, runner, fs_related as fs
25from mic.imager.baseimager import BaseImageCreator
26
27
28# The maximum string length supported for LoopImageCreator.fslabel
29FSLABEL_MAXLEN = 32
30
31
32def save_mountpoints(fpath, loops, arch = None):
33 """Save mount points mapping to file
34
35 :fpath, the xml file to store partition info
36 :loops, dict of partition info
37 :arch, image arch
38 """
39
40 if not fpath or not loops:
41 return
42
43 from xml.dom import minidom
44 doc = minidom.Document()
45 imgroot = doc.createElement("image")
46 doc.appendChild(imgroot)
47 if arch:
48 imgroot.setAttribute('arch', arch)
49 for loop in loops:
50 part = doc.createElement("partition")
51 imgroot.appendChild(part)
52 for (key, val) in loop.items():
53 if isinstance(val, fs.Mount):
54 continue
55 part.setAttribute(key, str(val))
56
57 with open(fpath, 'w') as wf:
58 wf.write(doc.toprettyxml(indent=' '))
59
60 return
61
62def load_mountpoints(fpath):
63 """Load mount points mapping from file
64
65 :fpath, file path to load
66 """
67
68 if not fpath:
69 return
70
71 from xml.dom import minidom
72 mount_maps = []
73 with open(fpath, 'r') as rf:
74 dom = minidom.parse(rf)
75 imgroot = dom.documentElement
76 for part in imgroot.getElementsByTagName("partition"):
77 p = dict(part.attributes.items())
78
79 try:
80 mp = (p['mountpoint'], p['label'], p['name'],
81 int(p['size']), p['fstype'])
82 except KeyError:
83 msger.warning("Wrong format line in file: %s" % fpath)
84 except ValueError:
85 msger.warning("Invalid size '%s' in file: %s" % (p['size'], fpath))
86 else:
87 mount_maps.append(mp)
88
89 return mount_maps
90
91class LoopImageCreator(BaseImageCreator):
92 """Installs a system into a loopback-mountable filesystem image.
93
94 LoopImageCreator is a straightforward ImageCreator subclass; the system
95 is installed into an ext3 filesystem on a sparse file which can be
96 subsequently loopback-mounted.
97
98 When specifying multiple partitions in kickstart file, each partition
99 will be created as a separated loop image.
100 """
101
102 def __init__(self, creatoropts=None, pkgmgr=None,
103 compress_image=None,
104 shrink_image=False):
105 """Initialize a LoopImageCreator instance.
106
107 This method takes the same arguments as ImageCreator.__init__()
108 with the addition of:
109
110 fslabel -- A string used as a label for any filesystems created.
111 """
112
113 BaseImageCreator.__init__(self, creatoropts, pkgmgr)
114
115 self.compress_image = compress_image
116 self.shrink_image = shrink_image
117
118 self.__fslabel = None
119 self.fslabel = self.name
120
121 self.__blocksize = 4096
122 if self.ks:
123 self.__fstype = kickstart.get_image_fstype(self.ks,
124 "ext3")
125 self.__fsopts = kickstart.get_image_fsopts(self.ks,
126 "defaults,noatime")
127
128 allloops = []
129 for part in sorted(kickstart.get_partitions(self.ks),
130 key=lambda p: p.mountpoint):
131 if part.fstype == "swap":
132 continue
133
134 label = part.label
135 mp = part.mountpoint
136 if mp == '/':
137 # the base image
138 if not label:
139 label = self.name
140 else:
141 mp = mp.rstrip('/')
142 if not label:
143 msger.warning('no "label" specified for loop img at %s'
144 ', use the mountpoint as the name' % mp)
145 label = mp.split('/')[-1]
146
147 imgname = misc.strip_end(label, '.img') + '.img'
148 allloops.append({
149 'mountpoint': mp,
150 'label': label,
151 'name': imgname,
152 'size': part.size or 4096L * 1024 * 1024,
153 'fstype': part.fstype or 'ext3',
154 'extopts': part.extopts or None,
155 'loop': None, # to be created in _mount_instroot
156 })
157 self._instloops = allloops
158
159 else:
160 self.__fstype = None
161 self.__fsopts = None
162 self._instloops = []
163
164 self.__imgdir = None
165
166 if self.ks:
167 self.__image_size = kickstart.get_image_size(self.ks,
168 4096L * 1024 * 1024)
169 else:
170 self.__image_size = 0
171
172 self._img_name = self.name + ".img"
173
174 def get_image_names(self):
175 if not self._instloops:
176 return None
177
178 return [lo['name'] for lo in self._instloops]
179
180 def _set_fstype(self, fstype):
181 self.__fstype = fstype
182
183 def _set_image_size(self, imgsize):
184 self.__image_size = imgsize
185
186
187 #
188 # Properties
189 #
190 def __get_fslabel(self):
191 if self.__fslabel is None:
192 return self.name
193 else:
194 return self.__fslabel
195 def __set_fslabel(self, val):
196 if val is None:
197 self.__fslabel = None
198 else:
199 self.__fslabel = val[:FSLABEL_MAXLEN]
200 #A string used to label any filesystems created.
201 #
202 #Some filesystems impose a constraint on the maximum allowed size of the
203 #filesystem label. In the case of ext3 it's 16 characters, but in the case
204 #of ISO9660 it's 32 characters.
205 #
206 #mke2fs silently truncates the label, but mkisofs aborts if the label is
207 #too long. So, for convenience sake, any string assigned to this attribute
208 #is silently truncated to FSLABEL_MAXLEN (32) characters.
209 fslabel = property(__get_fslabel, __set_fslabel)
210
211 def __get_image(self):
212 if self.__imgdir is None:
213 raise CreatorError("_image is not valid before calling mount()")
214 return os.path.join(self.__imgdir, self._img_name)
215 #The location of the image file.
216 #
217 #This is the path to the filesystem image. Subclasses may use this path
218 #in order to package the image in _stage_final_image().
219 #
220 #Note, this directory does not exist before ImageCreator.mount() is called.
221 #
222 #Note also, this is a read-only attribute.
223 _image = property(__get_image)
224
225 def __get_blocksize(self):
226 return self.__blocksize
227 def __set_blocksize(self, val):
228 if self._instloops:
229 raise CreatorError("_blocksize must be set before calling mount()")
230 try:
231 self.__blocksize = int(val)
232 except ValueError:
233 raise CreatorError("'%s' is not a valid integer value "
234 "for _blocksize" % val)
235 #The block size used by the image's filesystem.
236 #
237 #This is the block size used when creating the filesystem image. Subclasses
238 #may change this if they wish to use something other than a 4k block size.
239 #
240 #Note, this attribute may only be set before calling mount().
241 _blocksize = property(__get_blocksize, __set_blocksize)
242
243 def __get_fstype(self):
244 return self.__fstype
245 def __set_fstype(self, val):
246 if val != "ext2" and val != "ext3":
247 raise CreatorError("Unknown _fstype '%s' supplied" % val)
248 self.__fstype = val
249 #The type of filesystem used for the image.
250 #
251 #This is the filesystem type used when creating the filesystem image.
252 #Subclasses may change this if they wish to use something other ext3.
253 #
254 #Note, only ext2 and ext3 are currently supported.
255 #
256 #Note also, this attribute may only be set before calling mount().
257 _fstype = property(__get_fstype, __set_fstype)
258
259 def __get_fsopts(self):
260 return self.__fsopts
261 def __set_fsopts(self, val):
262 self.__fsopts = val
263 #Mount options of filesystem used for the image.
264 #
265 #This can be specified by --fsoptions="xxx,yyy" in part command in
266 #kickstart file.
267 _fsopts = property(__get_fsopts, __set_fsopts)
268
269
270 #
271 # Helpers for subclasses
272 #
273 def _resparse(self, size=None):
274 """Rebuild the filesystem image to be as sparse as possible.
275
276 This method should be used by subclasses when staging the final image
277 in order to reduce the actual space taken up by the sparse image file
278 to be as little as possible.
279
280 This is done by resizing the filesystem to the minimal size (thereby
281 eliminating any space taken up by deleted files) and then resizing it
282 back to the supplied size.
283
284 size -- the size in, in bytes, which the filesystem image should be
285 resized to after it has been minimized; this defaults to None,
286 causing the original size specified by the kickstart file to
287 be used (or 4GiB if not specified in the kickstart).
288 """
289 minsize = 0
290 for item in self._instloops:
291 if item['name'] == self._img_name:
292 minsize = item['loop'].resparse(size)
293 else:
294 item['loop'].resparse(size)
295
296 return minsize
297
298 def _base_on(self, base_on=None):
299 if base_on and self._image != base_on:
300 shutil.copyfile(base_on, self._image)
301
302 def _check_imgdir(self):
303 if self.__imgdir is None:
304 self.__imgdir = self._mkdtemp()
305
306
307 #
308 # Actual implementation
309 #
310 def _mount_instroot(self, base_on=None):
311
312 if base_on and os.path.isfile(base_on):
313 self.__imgdir = os.path.dirname(base_on)
314 imgname = os.path.basename(base_on)
315 self._base_on(base_on)
316 self._set_image_size(misc.get_file_size(self._image))
317
318 # here, self._instloops must be []
319 self._instloops.append({
320 "mountpoint": "/",
321 "label": self.name,
322 "name": imgname,
323 "size": self.__image_size or 4096L,
324 "fstype": self.__fstype or "ext3",
325 "extopts": None,
326 "loop": None
327 })
328
329 self._check_imgdir()
330
331 for loop in self._instloops:
332 fstype = loop['fstype']
333 mp = os.path.join(self._instroot, loop['mountpoint'].lstrip('/'))
334 size = loop['size'] * 1024L * 1024L
335 imgname = loop['name']
336
337 if fstype in ("ext2", "ext3", "ext4"):
338 MyDiskMount = fs.ExtDiskMount
339 elif fstype == "btrfs":
340 MyDiskMount = fs.BtrfsDiskMount
341 elif fstype in ("vfat", "msdos"):
342 MyDiskMount = fs.VfatDiskMount
343 else:
344 msger.error('Cannot support fstype: %s' % fstype)
345
346 loop['loop'] = MyDiskMount(fs.SparseLoopbackDisk(
347 os.path.join(self.__imgdir, imgname),
348 size),
349 mp,
350 fstype,
351 self._blocksize,
352 loop['label'])
353
354 if fstype in ("ext2", "ext3", "ext4"):
355 loop['loop'].extopts = loop['extopts']
356
357 try:
358 msger.verbose('Mounting image "%s" on "%s"' % (imgname, mp))
359 fs.makedirs(mp)
360 loop['loop'].mount()
361 except MountError, e:
362 raise
363
364 def _unmount_instroot(self):
365 for item in reversed(self._instloops):
366 try:
367 item['loop'].cleanup()
368 except:
369 pass
370
371 def _stage_final_image(self):
372
373 if self.pack_to or self.shrink_image:
374 self._resparse(0)
375 else:
376 self._resparse()
377
378 for item in self._instloops:
379 imgfile = os.path.join(self.__imgdir, item['name'])
380 if item['fstype'] == "ext4":
381 runner.show('/sbin/tune2fs -O ^huge_file,extents,uninit_bg %s '
382 % imgfile)
383 if self.compress_image:
384 misc.compressing(imgfile, self.compress_image)
385
386 if not self.pack_to:
387 for item in os.listdir(self.__imgdir):
388 shutil.move(os.path.join(self.__imgdir, item),
389 os.path.join(self._outdir, item))
390 else:
391 msger.info("Pack all loop images together to %s" % self.pack_to)
392 dstfile = os.path.join(self._outdir, self.pack_to)
393 misc.packing(dstfile, self.__imgdir)
394
395 if self.pack_to:
396 mountfp_xml = os.path.splitext(self.pack_to)[0]
397 mountfp_xml = misc.strip_end(mountfp_xml, '.tar') + ".xml"
398 else:
399 mountfp_xml = self.name + ".xml"
400 # save mount points mapping file to xml
401 save_mountpoints(os.path.join(self._outdir, mountfp_xml),
402 self._instloops,
403 self.target_arch)
404
405 def copy_attachment(self):
406 if not hasattr(self, '_attachment') or not self._attachment:
407 return
408
409 self._check_imgdir()
410
411 msger.info("Copying attachment files...")
412 for item in self._attachment:
413 if not os.path.exists(item):
414 continue
415 dpath = os.path.join(self.__imgdir, os.path.basename(item))
416 msger.verbose("Copy attachment %s to %s" % (item, dpath))
417 shutil.copy(item, dpath)
418
diff --git a/scripts/lib/mic/imager/raw.py b/scripts/lib/mic/imager/raw.py
new file mode 100644
index 0000000000..838191a6f1
--- /dev/null
+++ b/scripts/lib/mic/imager/raw.py
@@ -0,0 +1,501 @@
1#!/usr/bin/python -tt
2#
3# Copyright (c) 2011 Intel, Inc.
4#
5# This program is free software; you can redistribute it and/or modify it
6# under the terms of the GNU General Public License as published by the Free
7# Software Foundation; version 2 of the License
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12# for more details.
13#
14# You should have received a copy of the GNU General Public License along
15# with this program; if not, write to the Free Software Foundation, Inc., 59
16# Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17
18import os
19import stat
20import shutil
21
22from mic import kickstart, msger
23from mic.utils import fs_related, runner, misc
24from mic.utils.partitionedfs import PartitionedMount
25from mic.utils.errors import CreatorError, MountError
26from mic.imager.baseimager import BaseImageCreator
27
28
29class RawImageCreator(BaseImageCreator):
30 """Installs a system into a file containing a partitioned disk image.
31
32 ApplianceImageCreator is an advanced ImageCreator subclass; a sparse file
33 is formatted with a partition table, each partition loopback mounted
34 and the system installed into an virtual disk. The disk image can
35 subsequently be booted in a virtual machine or accessed with kpartx
36 """
37
38 def __init__(self, creatoropts=None, pkgmgr=None, compress_image=None, generate_bmap=None, fstab_entry="uuid"):
39 """Initialize a ApplianceImageCreator instance.
40
41 This method takes the same arguments as ImageCreator.__init__()
42 """
43 BaseImageCreator.__init__(self, creatoropts, pkgmgr)
44
45 self.__instloop = None
46 self.__imgdir = None
47 self.__disks = {}
48 self.__disk_format = "raw"
49 self._disk_names = []
50 self._ptable_format = self.ks.handler.bootloader.ptable
51 self.vmem = 512
52 self.vcpu = 1
53 self.checksum = False
54 self.use_uuid = fstab_entry == "uuid"
55 self.appliance_version = None
56 self.appliance_release = None
57 self.compress_image = compress_image
58 self.bmap_needed = generate_bmap
59 self._need_extlinux = not kickstart.use_installerfw(self.ks, "extlinux")
60 #self.getsource = False
61 #self.listpkg = False
62
63 self._dep_checks.extend(["sync", "kpartx", "parted"])
64 if self._need_extlinux:
65 self._dep_checks.extend(["extlinux"])
66
67 def configure(self, repodata = None):
68 import subprocess
69 def chroot():
70 os.chroot(self._instroot)
71 os.chdir("/")
72
73 if os.path.exists(self._instroot + "/usr/bin/Xorg"):
74 subprocess.call(["/bin/chmod", "u+s", "/usr/bin/Xorg"],
75 preexec_fn = chroot)
76
77 BaseImageCreator.configure(self, repodata)
78
79 def _get_fstab(self):
80 if kickstart.use_installerfw(self.ks, "fstab"):
81 # The fstab file will be generated by installer framework scripts
82 # instead.
83 return None
84
85 s = ""
86 for mp in self.__instloop.mountOrder:
87 p = None
88 for p1 in self.__instloop.partitions:
89 if p1['mountpoint'] == mp:
90 p = p1
91 break
92
93 if self.use_uuid and p['uuid']:
94 device = "UUID=%s" % p['uuid']
95 else:
96 device = "/dev/%s%-d" % (p['disk_name'], p['num'])
97
98 s += "%(device)s %(mountpoint)s %(fstype)s %(fsopts)s 0 0\n" % {
99 'device': device,
100 'mountpoint': p['mountpoint'],
101 'fstype': p['fstype'],
102 'fsopts': "defaults,noatime" if not p['fsopts'] else p['fsopts']}
103
104 if p['mountpoint'] == "/":
105 for subvol in self.__instloop.subvolumes:
106 if subvol['mountpoint'] == "/":
107 continue
108 s += "%(device)s %(mountpoint)s %(fstype)s %(fsopts)s 0 0\n" % {
109 'device': "/dev/%s%-d" % (p['disk_name'], p['num']),
110 'mountpoint': subvol['mountpoint'],
111 'fstype': p['fstype'],
112 'fsopts': "defaults,noatime" if not subvol['fsopts'] else subvol['fsopts']}
113
114 s += "devpts /dev/pts devpts gid=5,mode=620 0 0\n"
115 s += "tmpfs /dev/shm tmpfs defaults 0 0\n"
116 s += "proc /proc proc defaults 0 0\n"
117 s += "sysfs /sys sysfs defaults 0 0\n"
118 return s
119
120 def _create_mkinitrd_config(self):
121 """write to tell which modules to be included in initrd"""
122
123 mkinitrd = ""
124 mkinitrd += "PROBE=\"no\"\n"
125 mkinitrd += "MODULES+=\"ext3 ata_piix sd_mod libata scsi_mod\"\n"
126 mkinitrd += "rootfs=\"ext3\"\n"
127 mkinitrd += "rootopts=\"defaults\"\n"
128
129 msger.debug("Writing mkinitrd config %s/etc/sysconfig/mkinitrd" \
130 % self._instroot)
131 os.makedirs(self._instroot + "/etc/sysconfig/",mode=644)
132 cfg = open(self._instroot + "/etc/sysconfig/mkinitrd", "w")
133 cfg.write(mkinitrd)
134 cfg.close()
135
136 def _get_parts(self):
137 if not self.ks:
138 raise CreatorError("Failed to get partition info, "
139 "please check your kickstart setting.")
140
141 # Set a default partition if no partition is given out
142 if not self.ks.handler.partition.partitions:
143 partstr = "part / --size 1900 --ondisk sda --fstype=ext3"
144 args = partstr.split()
145 pd = self.ks.handler.partition.parse(args[1:])
146 if pd not in self.ks.handler.partition.partitions:
147 self.ks.handler.partition.partitions.append(pd)
148
149 # partitions list from kickstart file
150 return kickstart.get_partitions(self.ks)
151
152 def get_disk_names(self):
153 """ Returns a list of physical target disk names (e.g., 'sdb') which
154 will be created. """
155
156 if self._disk_names:
157 return self._disk_names
158
159 #get partition info from ks handler
160 parts = self._get_parts()
161
162 for i in range(len(parts)):
163 if parts[i].disk:
164 disk_name = parts[i].disk
165 else:
166 raise CreatorError("Failed to create disks, no --ondisk "
167 "specified in partition line of ks file")
168
169 if parts[i].mountpoint and not parts[i].fstype:
170 raise CreatorError("Failed to create disks, no --fstype "
171 "specified for partition with mountpoint "
172 "'%s' in the ks file")
173
174 self._disk_names.append(disk_name)
175
176 return self._disk_names
177
178 def _full_name(self, name, extention):
179 """ Construct full file name for a file we generate. """
180 return "%s-%s.%s" % (self.name, name, extention)
181
182 def _full_path(self, path, name, extention):
183 """ Construct full file path to a file we generate. """
184 return os.path.join(path, self._full_name(name, extention))
185
186 #
187 # Actual implemention
188 #
189 def _mount_instroot(self, base_on = None):
190 parts = self._get_parts()
191 self.__instloop = PartitionedMount(self._instroot)
192
193 for p in parts:
194 self.__instloop.add_partition(int(p.size),
195 p.disk,
196 p.mountpoint,
197 p.fstype,
198 p.label,
199 fsopts = p.fsopts,
200 boot = p.active,
201 align = p.align,
202 part_type = p.part_type)
203
204 self.__instloop.layout_partitions(self._ptable_format)
205
206 # Create the disks
207 self.__imgdir = self._mkdtemp()
208 for disk_name, disk in self.__instloop.disks.items():
209 full_path = self._full_path(self.__imgdir, disk_name, "raw")
210 msger.debug("Adding disk %s as %s with size %s bytes" \
211 % (disk_name, full_path, disk['min_size']))
212
213 disk_obj = fs_related.SparseLoopbackDisk(full_path,
214 disk['min_size'])
215 self.__disks[disk_name] = disk_obj
216 self.__instloop.add_disk(disk_name, disk_obj)
217
218 self.__instloop.mount()
219 self._create_mkinitrd_config()
220
221 def _get_required_packages(self):
222 required_packages = BaseImageCreator._get_required_packages(self)
223 if self._need_extlinux:
224 if not self.target_arch or not self.target_arch.startswith("arm"):
225 required_packages += ["syslinux", "syslinux-extlinux"]
226 return required_packages
227
228 def _get_excluded_packages(self):
229 return BaseImageCreator._get_excluded_packages(self)
230
231 def _get_syslinux_boot_config(self):
232 rootdev = None
233 root_part_uuid = None
234 for p in self.__instloop.partitions:
235 if p['mountpoint'] == "/":
236 rootdev = "/dev/%s%-d" % (p['disk_name'], p['num'])
237 root_part_uuid = p['partuuid']
238
239 return (rootdev, root_part_uuid)
240
241 def _create_syslinux_config(self):
242
243 splash = os.path.join(self._instroot, "boot/extlinux")
244 if os.path.exists(splash):
245 splashline = "menu background splash.jpg"
246 else:
247 splashline = ""
248
249 (rootdev, root_part_uuid) = self._get_syslinux_boot_config()
250 options = self.ks.handler.bootloader.appendLine
251
252 #XXX don't hardcode default kernel - see livecd code
253 syslinux_conf = ""
254 syslinux_conf += "prompt 0\n"
255 syslinux_conf += "timeout 1\n"
256 syslinux_conf += "\n"
257 syslinux_conf += "default vesamenu.c32\n"
258 syslinux_conf += "menu autoboot Starting %s...\n" % self.distro_name
259 syslinux_conf += "menu hidden\n"
260 syslinux_conf += "\n"
261 syslinux_conf += "%s\n" % splashline
262 syslinux_conf += "menu title Welcome to %s!\n" % self.distro_name
263 syslinux_conf += "menu color border 0 #ffffffff #00000000\n"
264 syslinux_conf += "menu color sel 7 #ffffffff #ff000000\n"
265 syslinux_conf += "menu color title 0 #ffffffff #00000000\n"
266 syslinux_conf += "menu color tabmsg 0 #ffffffff #00000000\n"
267 syslinux_conf += "menu color unsel 0 #ffffffff #00000000\n"
268 syslinux_conf += "menu color hotsel 0 #ff000000 #ffffffff\n"
269 syslinux_conf += "menu color hotkey 7 #ffffffff #ff000000\n"
270 syslinux_conf += "menu color timeout_msg 0 #ffffffff #00000000\n"
271 syslinux_conf += "menu color timeout 0 #ffffffff #00000000\n"
272 syslinux_conf += "menu color cmdline 0 #ffffffff #00000000\n"
273
274 versions = []
275 kernels = self._get_kernel_versions()
276 symkern = "%s/boot/vmlinuz" % self._instroot
277
278 if os.path.lexists(symkern):
279 v = os.path.realpath(symkern).replace('%s-' % symkern, "")
280 syslinux_conf += "label %s\n" % self.distro_name.lower()
281 syslinux_conf += "\tmenu label %s (%s)\n" % (self.distro_name, v)
282 syslinux_conf += "\tlinux ../vmlinuz\n"
283 if self._ptable_format == 'msdos':
284 rootstr = rootdev
285 else:
286 if not root_part_uuid:
287 raise MountError("Cannot find the root GPT partition UUID")
288 rootstr = "PARTUUID=%s" % root_part_uuid
289 syslinux_conf += "\tappend ro root=%s %s\n" % (rootstr, options)
290 syslinux_conf += "\tmenu default\n"
291 else:
292 for kernel in kernels:
293 for version in kernels[kernel]:
294 versions.append(version)
295
296 footlabel = 0
297 for v in versions:
298 syslinux_conf += "label %s%d\n" \
299 % (self.distro_name.lower(), footlabel)
300 syslinux_conf += "\tmenu label %s (%s)\n" % (self.distro_name, v)
301 syslinux_conf += "\tlinux ../vmlinuz-%s\n" % v
302 syslinux_conf += "\tappend ro root=%s %s\n" \
303 % (rootdev, options)
304 if footlabel == 0:
305 syslinux_conf += "\tmenu default\n"
306 footlabel += 1;
307
308 msger.debug("Writing syslinux config %s/boot/extlinux/extlinux.conf" \
309 % self._instroot)
310 cfg = open(self._instroot + "/boot/extlinux/extlinux.conf", "w")
311 cfg.write(syslinux_conf)
312 cfg.close()
313
314 def _install_syslinux(self):
315 for name in self.__disks.keys():
316 loopdev = self.__disks[name].device
317
318 # Set MBR
319 mbrfile = "%s/usr/share/syslinux/" % self._instroot
320 if self._ptable_format == 'gpt':
321 mbrfile += "gptmbr.bin"
322 else:
323 mbrfile += "mbr.bin"
324
325 msger.debug("Installing syslinux bootloader '%s' to %s" % \
326 (mbrfile, loopdev))
327
328 mbrsize = os.stat(mbrfile)[stat.ST_SIZE]
329 rc = runner.show(['dd', 'if=%s' % mbrfile, 'of=' + loopdev])
330 if rc != 0:
331 raise MountError("Unable to set MBR to %s" % loopdev)
332
333
334 # Ensure all data is flushed to disk before doing syslinux install
335 runner.quiet('sync')
336
337 fullpathsyslinux = fs_related.find_binary_path("extlinux")
338 rc = runner.show([fullpathsyslinux,
339 "-i",
340 "%s/boot/extlinux" % self._instroot])
341 if rc != 0:
342 raise MountError("Unable to install syslinux bootloader to %s" \
343 % loopdev)
344
345 def _create_bootconfig(self):
346 #If syslinux is available do the required configurations.
347 if self._need_extlinux \
348 and os.path.exists("%s/usr/share/syslinux/" % (self._instroot)) \
349 and os.path.exists("%s/boot/extlinux/" % (self._instroot)):
350 self._create_syslinux_config()
351 self._install_syslinux()
352
353 def _unmount_instroot(self):
354 if not self.__instloop is None:
355 try:
356 self.__instloop.cleanup()
357 except MountError, err:
358 msger.warning("%s" % err)
359
360 def _resparse(self, size = None):
361 return self.__instloop.resparse(size)
362
363 def _get_post_scripts_env(self, in_chroot):
364 env = BaseImageCreator._get_post_scripts_env(self, in_chroot)
365
366 # Export the file-system UUIDs and partition UUIDs (AKA PARTUUIDs)
367 for p in self.__instloop.partitions:
368 env.update(self._set_part_env(p['ks_pnum'], "UUID", p['uuid']))
369 env.update(self._set_part_env(p['ks_pnum'], "PARTUUID", p['partuuid']))
370
371 return env
372
373 def _stage_final_image(self):
374 """Stage the final system image in _outdir.
375 write meta data
376 """
377 self._resparse()
378
379 if self.compress_image:
380 for imgfile in os.listdir(self.__imgdir):
381 if imgfile.endswith('.raw') or imgfile.endswith('bin'):
382 imgpath = os.path.join(self.__imgdir, imgfile)
383 misc.compressing(imgpath, self.compress_image)
384
385 if self.pack_to:
386 dst = os.path.join(self._outdir, self.pack_to)
387 msger.info("Pack all raw images to %s" % dst)
388 misc.packing(dst, self.__imgdir)
389 else:
390 msger.debug("moving disks to stage location")
391 for imgfile in os.listdir(self.__imgdir):
392 src = os.path.join(self.__imgdir, imgfile)
393 dst = os.path.join(self._outdir, imgfile)
394 msger.debug("moving %s to %s" % (src,dst))
395 shutil.move(src,dst)
396 self._write_image_xml()
397
398 def _write_image_xml(self):
399 imgarch = "i686"
400 if self.target_arch and self.target_arch.startswith("arm"):
401 imgarch = "arm"
402 xml = "<image>\n"
403
404 name_attributes = ""
405 if self.appliance_version:
406 name_attributes += " version='%s'" % self.appliance_version
407 if self.appliance_release:
408 name_attributes += " release='%s'" % self.appliance_release
409 xml += " <name%s>%s</name>\n" % (name_attributes, self.name)
410 xml += " <domain>\n"
411 # XXX don't hardcode - determine based on the kernel we installed for
412 # grub baremetal vs xen
413 xml += " <boot type='hvm'>\n"
414 xml += " <guest>\n"
415 xml += " <arch>%s</arch>\n" % imgarch
416 xml += " </guest>\n"
417 xml += " <os>\n"
418 xml += " <loader dev='hd'/>\n"
419 xml += " </os>\n"
420
421 i = 0
422 for name in self.__disks.keys():
423 full_name = self._full_name(name, self.__disk_format)
424 xml += " <drive disk='%s' target='hd%s'/>\n" \
425 % (full_name, chr(ord('a') + i))
426 i = i + 1
427
428 xml += " </boot>\n"
429 xml += " <devices>\n"
430 xml += " <vcpu>%s</vcpu>\n" % self.vcpu
431 xml += " <memory>%d</memory>\n" %(self.vmem * 1024)
432 for network in self.ks.handler.network.network:
433 xml += " <interface/>\n"
434 xml += " <graphics/>\n"
435 xml += " </devices>\n"
436 xml += " </domain>\n"
437 xml += " <storage>\n"
438
439 if self.checksum is True:
440 for name in self.__disks.keys():
441 diskpath = self._full_path(self._outdir, name, \
442 self.__disk_format)
443 full_name = self._full_name(name, self.__disk_format)
444
445 msger.debug("Generating disk signature for %s" % full_name)
446
447 xml += " <disk file='%s' use='system' format='%s'>\n" \
448 % (full_name, self.__disk_format)
449
450 hashes = misc.calc_hashes(diskpath, ('sha1', 'sha256'))
451
452 xml += " <checksum type='sha1'>%s</checksum>\n" \
453 % hashes[0]
454 xml += " <checksum type='sha256'>%s</checksum>\n" \
455 % hashes[1]
456 xml += " </disk>\n"
457 else:
458 for name in self.__disks.keys():
459 full_name = self._full_name(name, self.__disk_format)
460 xml += " <disk file='%s' use='system' format='%s'/>\n" \
461 % (full_name, self.__disk_format)
462
463 xml += " </storage>\n"
464 xml += "</image>\n"
465
466 msger.debug("writing image XML to %s/%s.xml" %(self._outdir, self.name))
467 cfg = open("%s/%s.xml" % (self._outdir, self.name), "w")
468 cfg.write(xml)
469 cfg.close()
470
471 def generate_bmap(self):
472 """ Generate block map file for the image. The idea is that while disk
473 images we generate may be large (e.g., 4GiB), they may actually contain
474 only little real data, e.g., 512MiB. This data are files, directories,
475 file-system meta-data, partition table, etc. In other words, when
476 flashing the image to the target device, you do not have to copy all the
477 4GiB of data, you can copy only 512MiB of it, which is 4 times faster.
478
479 This function generates the block map file for an arbitrary image that
480 mic has generated. The block map file is basically an XML file which
481 contains a list of blocks which have to be copied to the target device.
482 The other blocks are not used and there is no need to copy them. """
483
484 if self.bmap_needed is None:
485 return
486
487 from mic.utils import BmapCreate
488 msger.info("Generating the map file(s)")
489
490 for name in self.__disks.keys():
491 image = self._full_path(self.__imgdir, name, self.__disk_format)
492 bmap_file = self._full_path(self._outdir, name, "bmap")
493
494 msger.debug("Generating block map file '%s'" % bmap_file)
495
496 try:
497 creator = BmapCreate.BmapCreate(image, bmap_file)
498 creator.generate()
499 del creator
500 except BmapCreate.Error as err:
501 raise CreatorError("Failed to create bmap file: %s" % str(err))