3 # Copyright (c) 2011 Intel, Inc.
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
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
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.
22 from mic import kickstart, msger
23 from mic.utils import fs_related, runner, misc
24 from mic.utils.partitionedfs import PartitionedMount
25 from mic.utils.errors import CreatorError, MountError
26 from mic.imager.baseimager import BaseImageCreator
27 from mic.archive import packing, compressing
29 class RawImageCreator(BaseImageCreator):
30 """Installs a system into a file containing a partitioned disk image.
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
39 def __init__(self, creatoropts=None, pkgmgr=None, compress_image=None, generate_bmap=None, fstab_entry="uuid"):
40 """Initialize a ApplianceImageCreator instance.
42 This method takes the same arguments as ImageCreator.__init__()
44 BaseImageCreator.__init__(self, creatoropts, pkgmgr)
46 self.__instloop = None
49 self.__disk_format = "raw"
51 self._ptable_format = self.ks.handler.bootloader.ptable
55 self.use_uuid = fstab_entry == "uuid"
56 self.appliance_version = None
57 self.appliance_release = None
58 self.compress_image = compress_image
59 self.bmap_needed = generate_bmap
60 self._need_extlinux = not kickstart.use_installerfw(self.ks, "bootloader")
61 #self.getsource = False
64 self._dep_checks.extend(["sync", "kpartx", "parted"])
65 if self._need_extlinux:
66 self._dep_checks.extend(["extlinux"])
68 def configure(self, repodata = None):
71 os.chroot(self._instroot)
74 if os.path.exists(self._instroot + "/usr/bin/Xorg"):
75 subprocess.call(["/bin/chmod", "u+s", "/usr/bin/Xorg"],
78 BaseImageCreator.configure(self, repodata)
82 for mp in self.__instloop.mount_order:
84 for p1 in self.__instloop.partitions:
85 if p1['mountpoint'] == mp:
89 if self.use_uuid and p['uuid']:
90 device = "UUID=%s" % p['uuid']
92 device = "/dev/%s%-d" % (p['disk_name'], p['num'])
94 s += "%(device)s %(mountpoint)s %(fstype)s %(fsopts)s 0 0\n" % {
96 'mountpoint': p['mountpoint'],
97 'fstype': p['fstype'],
98 'fsopts': "defaults,noatime" if not p['fsopts'] else p['fsopts']}
100 if p['mountpoint'] == "/":
101 for subvol in self.__instloop.subvolumes:
102 if subvol['mountpoint'] == "/":
104 s += "%(device)s %(mountpoint)s %(fstype)s %(fsopts)s 0 0\n" % {
105 'device': "/dev/%s%-d" % (p['disk_name'], p['num']),
106 'mountpoint': subvol['mountpoint'],
107 'fstype': p['fstype'],
108 'fsopts': "defaults,noatime" if not subvol['fsopts'] else subvol['fsopts']}
110 s += "devpts /dev/pts devpts gid=5,mode=620 0 0\n"
111 s += "tmpfs /dev/shm tmpfs defaults 0 0\n"
112 s += "proc /proc proc defaults 0 0\n"
113 s += "sysfs /sys sysfs defaults 0 0\n"
116 def _create_mkinitrd_config(self):
117 """write to tell which modules to be included in initrd"""
120 mkinitrd += "PROBE=\"no\"\n"
121 mkinitrd += "MODULES+=\"ext3 ata_piix sd_mod libata scsi_mod\"\n"
122 mkinitrd += "rootfs=\"ext3\"\n"
123 mkinitrd += "rootopts=\"defaults\"\n"
125 msger.debug("Writing mkinitrd config %s/etc/sysconfig/mkinitrd" \
127 os.makedirs(self._instroot + "/etc/sysconfig/",mode=644)
128 cfg = open(self._instroot + "/etc/sysconfig/mkinitrd", "w")
132 def _get_parts(self):
134 raise CreatorError("Failed to get partition info, "
135 "please check your kickstart setting.")
137 # Set a default partition if no partition is given out
138 if not self.ks.handler.partition.partitions:
139 partstr = "part / --size 1900 --ondisk sda --fstype=ext3"
140 args = partstr.split()
141 pd = self.ks.handler.partition.parse(args[1:])
142 if pd not in self.ks.handler.partition.partitions:
143 self.ks.handler.partition.partitions.append(pd)
145 # partitions list from kickstart file
146 return kickstart.get_partitions(self.ks)
148 def get_disk_names(self):
149 """ Returns a list of physical target disk names (e.g., 'sdb') which
153 return self._disk_names
155 #get partition info from ks handler
156 parts = self._get_parts()
158 for i in range(len(parts)):
160 disk_name = parts[i].disk
162 raise CreatorError("Failed to create disks, no --ondisk "
163 "specified in partition line of ks file")
165 if parts[i].mountpoint and not parts[i].fstype:
166 raise CreatorError("Failed to create disks, no --fstype "
167 "specified for partition with mountpoint "
168 "'%s' in the ks file")
170 self._disk_names.append(disk_name)
172 return self._disk_names
174 def _full_name(self, name, extention):
175 """ Construct full file name for a file we generate. """
176 return "%s-%s.%s" % (self.name, name, extention)
178 def _full_path(self, path, name, extention):
179 """ Construct full file path to a file we generate. """
180 return os.path.join(path, self._full_name(name, extention))
183 # Actual implemention
185 def _mount_instroot(self, base_on = None):
186 parts = self._get_parts()
187 self.__instloop = PartitionedMount(self._instroot)
190 self.__instloop.add_partition(int(p.size),
198 part_type = p.part_type)
200 self.__instloop.layout_partitions(self._ptable_format)
203 self.__imgdir = self._mkdtemp()
204 for disk_name, disk in self.__instloop.disks.items():
205 full_path = self._full_path(self.__imgdir, disk_name, "raw")
206 msger.debug("Adding disk %s as %s with size %s bytes" \
207 % (disk_name, full_path, disk['min_size']))
209 disk_obj = fs_related.SparseLoopbackDisk(full_path,
211 self.__disks[disk_name] = disk_obj
212 self.__instloop.add_disk(disk_name, disk_obj)
214 self.__instloop.mount()
215 self._create_mkinitrd_config()
217 def mount(self, base_on = None, cachedir = None):
219 This method calls the base class' 'mount()' method and then creates
220 block device nodes corresponding to the image's partitions in the image
221 itself. Namely, the image has /dev/loopX device corresponding to the
222 entire image, and per-partition /dev/mapper/* devices.
224 We copy these files to image's "/dev" directory in order to enable
225 scripts which run in the image chroot environment to access own raw
226 partitions. For example, this can be used to install the bootloader to
227 the MBR (say, from an installer framework plugin).
230 def copy_devnode(src, dest):
231 """A helper function for copying device nodes."""
236 stat_obj = os.stat(src)
237 assert stat.S_ISBLK(stat_obj.st_mode)
239 os.mknod(dest, stat_obj.st_mode,
240 os.makedev(os.major(stat_obj.st_rdev),
241 os.minor(stat_obj.st_rdev)))
242 # os.mknod uses process umask may create a nod with different
243 # permissions, so we use os.chmod to make sure permissions are
245 os.chmod(dest, stat_obj.st_mode)
247 BaseImageCreator.mount(self, base_on, cachedir)
249 # Copy the disk loop devices
250 for name in self.__disks.keys():
251 loopdev = self.__disks[name].device
252 copy_devnode(loopdev, self._instroot + loopdev)
254 # Copy per-partition dm nodes
255 os.mkdir(self._instroot + "/dev/mapper", os.stat("/dev/mapper").st_mode)
256 for p in self.__instloop.partitions:
257 copy_devnode(p['mapper_device'],
258 self._instroot + p['mapper_device'])
259 copy_devnode(p['mpath_device'],
260 self._instroot + p['mpath_device'])
264 Remove loop/dm device nodes which we created in 'mount()' and call the
265 base class' 'unmount()' method.
268 for p in self.__instloop.partitions:
269 if p['mapper_device']:
270 path = self._instroot + p['mapper_device']
271 if os.path.exists(path):
273 if p['mpath_device']:
274 path = self._instroot + p['mpath_device']
275 if os.path.exists(path):
278 path = self._instroot + "/dev/mapper"
279 if os.path.exists(path):
280 shutil.rmtree(path, ignore_errors=True)
282 for name in self.__disks.keys():
283 if self.__disks[name].device:
284 path = self._instroot + self.__disks[name].device
285 if os.path.exists(path):
288 BaseImageCreator.unmount(self)
290 def _get_required_packages(self):
291 required_packages = BaseImageCreator._get_required_packages(self)
292 if self._need_extlinux:
293 if not self.target_arch or not self.target_arch.startswith("arm"):
294 required_packages += ["syslinux", "syslinux-extlinux"]
295 return required_packages
297 def _get_excluded_packages(self):
298 return BaseImageCreator._get_excluded_packages(self)
300 def _get_syslinux_boot_config(self):
302 root_part_uuid = None
303 for p in self.__instloop.partitions:
304 if p['mountpoint'] == "/":
305 rootdev = "/dev/%s%-d" % (p['disk_name'], p['num'])
306 root_part_uuid = p['partuuid']
308 return (rootdev, root_part_uuid)
310 def _create_syslinux_config(self):
312 splash = os.path.join(self._instroot, "boot/extlinux")
313 if os.path.exists(splash):
314 splashline = "menu background splash.jpg"
318 (rootdev, root_part_uuid) = self._get_syslinux_boot_config()
319 options = self.ks.handler.bootloader.appendLine
321 #XXX don't hardcode default kernel - see livecd code
323 syslinux_conf += "prompt 0\n"
324 syslinux_conf += "timeout 1\n"
325 syslinux_conf += "\n"
326 syslinux_conf += "default vesamenu.c32\n"
327 syslinux_conf += "menu autoboot Starting %s...\n" % self.distro_name
328 syslinux_conf += "menu hidden\n"
329 syslinux_conf += "\n"
330 syslinux_conf += "%s\n" % splashline
331 syslinux_conf += "menu title Welcome to %s!\n" % self.distro_name
332 syslinux_conf += "menu color border 0 #ffffffff #00000000\n"
333 syslinux_conf += "menu color sel 7 #ffffffff #ff000000\n"
334 syslinux_conf += "menu color title 0 #ffffffff #00000000\n"
335 syslinux_conf += "menu color tabmsg 0 #ffffffff #00000000\n"
336 syslinux_conf += "menu color unsel 0 #ffffffff #00000000\n"
337 syslinux_conf += "menu color hotsel 0 #ff000000 #ffffffff\n"
338 syslinux_conf += "menu color hotkey 7 #ffffffff #ff000000\n"
339 syslinux_conf += "menu color timeout_msg 0 #ffffffff #00000000\n"
340 syslinux_conf += "menu color timeout 0 #ffffffff #00000000\n"
341 syslinux_conf += "menu color cmdline 0 #ffffffff #00000000\n"
344 kernels = self._get_kernel_versions()
345 symkern = "%s/boot/vmlinuz" % self._instroot
347 if os.path.lexists(symkern):
348 v = os.path.realpath(symkern).replace('%s-' % symkern, "")
349 syslinux_conf += "label %s\n" % self.distro_name.lower()
350 syslinux_conf += "\tmenu label %s (%s)\n" % (self.distro_name, v)
351 syslinux_conf += "\tlinux ../vmlinuz\n"
352 if self._ptable_format == 'msdos':
355 if not root_part_uuid:
356 raise MountError("Cannot find the root GPT partition UUID")
357 rootstr = "PARTUUID=%s" % root_part_uuid
358 syslinux_conf += "\tappend ro root=%s %s\n" % (rootstr, options)
359 syslinux_conf += "\tmenu default\n"
361 for kernel in kernels:
362 for version in kernels[kernel]:
363 versions.append(version)
367 syslinux_conf += "label %s%d\n" \
368 % (self.distro_name.lower(), footlabel)
369 syslinux_conf += "\tmenu label %s (%s)\n" % (self.distro_name, v)
370 syslinux_conf += "\tlinux ../vmlinuz-%s\n" % v
371 syslinux_conf += "\tappend ro root=%s %s\n" \
374 syslinux_conf += "\tmenu default\n"
377 msger.debug("Writing syslinux config %s/boot/extlinux/extlinux.conf" \
379 cfg = open(self._instroot + "/boot/extlinux/extlinux.conf", "w")
380 cfg.write(syslinux_conf)
383 def _install_syslinux(self):
384 for name in self.__disks.keys():
385 loopdev = self.__disks[name].device
388 mbrfile = "%s/usr/share/syslinux/" % self._instroot
389 if self._ptable_format == 'gpt':
390 mbrfile += "gptmbr.bin"
394 msger.debug("Installing syslinux bootloader '%s' to %s" % \
397 rc = runner.show(['dd', 'if=%s' % mbrfile, 'of=' + loopdev])
399 raise MountError("Unable to set MBR to %s" % loopdev)
402 # Ensure all data is flushed to disk before doing syslinux install
405 fullpathsyslinux = fs_related.find_binary_path("extlinux")
406 rc = runner.show([fullpathsyslinux,
408 "%s/boot/extlinux" % self._instroot])
410 raise MountError("Unable to install syslinux bootloader to %s" \
413 def _create_bootconfig(self):
414 #If syslinux is available do the required configurations.
415 if self._need_extlinux \
416 and os.path.exists("%s/usr/share/syslinux/" % (self._instroot)) \
417 and os.path.exists("%s/boot/extlinux/" % (self._instroot)):
418 self._create_syslinux_config()
419 self._install_syslinux()
421 def _unmount_instroot(self):
422 if not self.__instloop is None:
424 self.__instloop.cleanup()
425 except MountError as err:
426 msger.warning("%s" % err)
428 def _resparse(self, size = None):
429 return self.__instloop.resparse(size)
431 def _get_post_scripts_env(self, in_chroot):
432 env = BaseImageCreator._get_post_scripts_env(self, in_chroot)
434 # Export the file-system UUIDs and partition UUIDs (AKA PARTUUIDs)
435 for p in self.__instloop.partitions:
436 env.update(self._set_part_env(p['ks_pnum'], "UUID", p['uuid']))
437 env.update(self._set_part_env(p['ks_pnum'], "PARTUUID", p['partuuid']))
438 env.update(self._set_part_env(p['ks_pnum'], "DEVNODE_NOW",
440 env.update(self._set_part_env(p['ks_pnum'], "DISK_DEVNODE_NOW",
441 self.__disks[p['disk_name']].device))
445 def _stage_final_image(self):
446 """Stage the final system image in _outdir.
450 self.image_files.update({'disks': self.__disks.keys()})
452 if not (self.compress_image or self.pack_to):
453 for imgfile in os.listdir(self.__imgdir):
454 if imgfile.endswith('.raw'):
455 for disk in self.__disks.keys():
456 if imgfile.find(disk) != -1:
457 self.image_files.setdefault(disk, {}).update(
459 self.image_files.setdefault('image_files',
462 if self.compress_image:
463 for imgfile in os.listdir(self.__imgdir):
464 if imgfile.endswith('.raw') or imgfile.endswith('bin'):
465 imgpath = os.path.join(self.__imgdir, imgfile)
466 msger.info("Compressing image %s" % imgfile)
467 compressing(imgpath, self.compress_image)
468 if imgfile.endswith('.raw') and not self.pack_to:
469 for disk in self.__disks.keys():
470 if imgfile.find(disk) != -1:
471 imgname = '%s.%s' % (imgfile, self.compress_image)
472 self.image_files.setdefault(disk, {}).update(
474 self.image_files.setdefault('image_files',
478 dst = os.path.join(self._outdir, self.pack_to)
479 msger.info("Pack all raw images to %s" % dst)
480 packing(dst, self.__imgdir)
481 self.image_files.update({'image_files': self.pack_to})
483 msger.debug("moving disks to stage location")
484 for imgfile in os.listdir(self.__imgdir):
485 src = os.path.join(self.__imgdir, imgfile)
486 dst = os.path.join(self._outdir, imgfile)
487 msger.debug("moving %s to %s" % (src,dst))
490 self._write_image_xml()
492 def _write_image_xml(self):
494 if self.target_arch and self.target_arch.startswith("arm"):
499 if self.appliance_version:
500 name_attributes += " version='%s'" % self.appliance_version
501 if self.appliance_release:
502 name_attributes += " release='%s'" % self.appliance_release
503 xml += " <name%s>%s</name>\n" % (name_attributes, self.name)
505 # XXX don't hardcode - determine based on the kernel we installed for
506 # grub baremetal vs xen
507 xml += " <boot type='hvm'>\n"
509 xml += " <arch>%s</arch>\n" % imgarch
512 xml += " <loader dev='hd'/>\n"
516 for name in self.__disks.keys():
517 full_name = self._full_name(name, self.__disk_format)
518 xml += " <drive disk='%s' target='hd%s'/>\n" \
519 % (full_name, chr(ord('a') + i))
523 xml += " <devices>\n"
524 xml += " <vcpu>%s</vcpu>\n" % self.vcpu
525 xml += " <memory>%d</memory>\n" %(self.vmem * 1024)
526 for network in self.ks.handler.network.network:
527 xml += " <interface/>\n"
528 xml += " <graphics/>\n"
529 xml += " </devices>\n"
530 xml += " </domain>\n"
531 xml += " <storage>\n"
533 if self.checksum is True:
534 for name in self.__disks.keys():
535 diskpath = self._full_path(self._outdir, name, \
537 full_name = self._full_name(name, self.__disk_format)
539 msger.debug("Generating disk signature for %s" % full_name)
541 xml += " <disk file='%s' use='system' format='%s'>\n" \
542 % (full_name, self.__disk_format)
544 hashes = misc.calc_hashes(diskpath, ('sha1', 'sha256'))
546 xml += " <checksum type='sha1'>%s</checksum>\n" \
548 xml += " <checksum type='sha256'>%s</checksum>\n" \
552 for name in self.__disks.keys():
553 full_name = self._full_name(name, self.__disk_format)
554 xml += " <disk file='%s' use='system' format='%s'/>\n" \
555 % (full_name, self.__disk_format)
557 xml += " </storage>\n"
560 msger.debug("writing image XML to %s/%s.xml" %(self._outdir, self.name))
561 cfg = open("%s/%s.xml" % (self._outdir, self.name), "w")
565 def generate_bmap(self):
566 """ Generate block map file for the image. The idea is that while disk
567 images we generate may be large (e.g., 4GiB), they may actually contain
568 only little real data, e.g., 512MiB. This data are files, directories,
569 file-system meta-data, partition table, etc. In other words, when
570 flashing the image to the target device, you do not have to copy all the
571 4GiB of data, you can copy only 512MiB of it, which is 4 times faster.
573 This function generates the block map file for an arbitrary image that
574 mic has generated. The block map file is basically an XML file which
575 contains a list of blocks which have to be copied to the target device.
576 The other blocks are not used and there is no need to copy them. """
578 if self.bmap_needed is None:
581 msger.info("Generating the map file(s)")
583 for name in self.__disks.keys():
584 image = self._full_path(self.__imgdir, name, self.__disk_format)
585 bmap_file = self._full_path(self._outdir, name, "bmap")
586 self.image_files.setdefault(name, {}).update({'bmap': \
587 os.path.basename(bmap_file)})
589 msger.debug("Generating block map file '%s'" % bmap_file)
591 bmaptoolcmd = misc.find_binary_path('bmaptool')
592 rc = runner.show([bmaptoolcmd, 'create', image, '-o', bmap_file])
594 raise CreatorError("Failed to create bmap file: %s" % bmap_file)
596 def create_manifest(self):
597 if self.compress_image:
598 self.image_files.update({'compress': self.compress_image})
599 super(RawImageCreator, self).create_manifest()
601 def remove_exclude_image(self):