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
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
38 def __init__(self, creatoropts=None, pkgmgr=None, compress_image=None, generate_bmap=None, fstab_entry="uuid"):
39 """Initialize a ApplianceImageCreator instance.
41 This method takes the same arguments as ImageCreator.__init__()
43 BaseImageCreator.__init__(self, creatoropts, pkgmgr)
45 self.__instloop = None
48 self.__disk_format = "raw"
50 self._ptable_format = self.ks.handler.bootloader.ptable
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, "bootloader")
60 #self.getsource = False
63 self._dep_checks.extend(["sync", "kpartx", "parted"])
64 if self._need_extlinux:
65 self._dep_checks.extend(["extlinux"])
67 def configure(self, repodata = None):
70 os.chroot(self._instroot)
73 if os.path.exists(self._instroot + "/usr/bin/Xorg"):
74 subprocess.call(["/bin/chmod", "u+s", "/usr/bin/Xorg"],
77 BaseImageCreator.configure(self, repodata)
80 if kickstart.use_installerfw(self.ks, "fstab"):
81 # The fstab file will be generated by installer framework scripts
86 for mp in self.__instloop.mountOrder:
88 for p1 in self.__instloop.partitions:
89 if p1['mountpoint'] == mp:
93 if self.use_uuid and p['uuid']:
94 device = "UUID=%s" % p['uuid']
96 device = "/dev/%s%-d" % (p['disk_name'], p['num'])
98 s += "%(device)s %(mountpoint)s %(fstype)s %(fsopts)s 0 0\n" % {
100 'mountpoint': p['mountpoint'],
101 'fstype': p['fstype'],
102 'fsopts': "defaults,noatime" if not p['fsopts'] else p['fsopts']}
104 if p['mountpoint'] == "/":
105 for subvol in self.__instloop.subvolumes:
106 if subvol['mountpoint'] == "/":
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']}
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"
120 def _create_mkinitrd_config(self):
121 """write to tell which modules to be included in initrd"""
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"
129 msger.debug("Writing mkinitrd config %s/etc/sysconfig/mkinitrd" \
131 os.makedirs(self._instroot + "/etc/sysconfig/",mode=644)
132 cfg = open(self._instroot + "/etc/sysconfig/mkinitrd", "w")
136 def _get_parts(self):
138 raise CreatorError("Failed to get partition info, "
139 "please check your kickstart setting.")
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)
149 # partitions list from kickstart file
150 return kickstart.get_partitions(self.ks)
152 def get_disk_names(self):
153 """ Returns a list of physical target disk names (e.g., 'sdb') which
157 return self._disk_names
159 #get partition info from ks handler
160 parts = self._get_parts()
162 for i in range(len(parts)):
164 disk_name = parts[i].disk
166 raise CreatorError("Failed to create disks, no --ondisk "
167 "specified in partition line of ks file")
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")
174 self._disk_names.append(disk_name)
176 return self._disk_names
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)
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))
187 # Actual implemention
189 def _mount_instroot(self, base_on = None):
190 parts = self._get_parts()
191 self.__instloop = PartitionedMount(self._instroot)
194 self.__instloop.add_partition(int(p.size),
202 part_type = p.part_type)
204 self.__instloop.layout_partitions(self._ptable_format)
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']))
213 disk_obj = fs_related.SparseLoopbackDisk(full_path,
215 self.__disks[disk_name] = disk_obj
216 self.__instloop.add_disk(disk_name, disk_obj)
218 self.__instloop.mount()
219 self._create_mkinitrd_config()
221 def mount(self, base_on = None, cachedir = None):
223 This method calls the base class' 'mount()' method and then creates
224 block device nodes corresponding to the image's partitions in the image
225 itself. Namely, the image has /dev/loopX device corresponding to the
226 entire image, and per-partition /dev/mapper/* devices.
228 We copy these files to image's "/dev" directory in order to enable
229 scripts which run in the image chroot environment to access own raw
230 partitions. For example, this can be used to install the bootloader to
231 the MBR (say, from an installer framework plugin).
234 def copy_devnode(src, dest):
235 """A helper function for copying device nodes."""
237 stat_obj = os.stat(src)
238 assert stat.S_ISBLK(stat_obj.st_mode)
240 os.mknod(dest, stat_obj.st_mode,
241 os.makedev(os.major(stat_obj.st_rdev),
242 os.minor(stat_obj.st_rdev)))
243 # os.mknod uses process umask may create a nod with different
244 # permissions, so we use os.chmod to make sure permissions are
246 os.chmod(dest, stat_obj.st_mode)
248 BaseImageCreator.mount(self, base_on, cachedir)
250 # Copy the disk loop devices
251 for name in self.__disks.keys():
252 loopdev = self.__disks[name].device
253 copy_devnode(loopdev, self._instroot + loopdev)
255 # Copy per-partition dm nodes
256 os.mkdir(self._instroot + "/dev/mapper", os.stat("/dev/mapper").st_mode)
257 for p in self.__instloop.partitions:
258 copy_devnode(p['mapper_device'],
259 self._instroot + p['mapper_device'])
263 Remove loop/dm device nodes which we created in 'mount()' and call the
264 base class' 'unmount()' method.
267 for p in self.__instloop.partitions:
268 path = self._instroot + p['mapper_device']
269 if os.path.exists(path):
272 path = self._instroot + "/dev/mapper"
273 if os.path.exists(path):
276 for name in self.__disks.keys():
277 if self.__disks[name].device:
278 path = self._instroot + self.__disks[name].device
279 if os.path.exists(path):
282 BaseImageCreator.unmount(self)
284 def _get_required_packages(self):
285 required_packages = BaseImageCreator._get_required_packages(self)
286 if self._need_extlinux:
287 if not self.target_arch or not self.target_arch.startswith("arm"):
288 required_packages += ["syslinux", "syslinux-extlinux"]
289 return required_packages
291 def _get_excluded_packages(self):
292 return BaseImageCreator._get_excluded_packages(self)
294 def _get_syslinux_boot_config(self):
296 root_part_uuid = None
297 for p in self.__instloop.partitions:
298 if p['mountpoint'] == "/":
299 rootdev = "/dev/%s%-d" % (p['disk_name'], p['num'])
300 root_part_uuid = p['partuuid']
302 return (rootdev, root_part_uuid)
304 def _create_syslinux_config(self):
306 splash = os.path.join(self._instroot, "boot/extlinux")
307 if os.path.exists(splash):
308 splashline = "menu background splash.jpg"
312 (rootdev, root_part_uuid) = self._get_syslinux_boot_config()
313 options = self.ks.handler.bootloader.appendLine
315 #XXX don't hardcode default kernel - see livecd code
317 syslinux_conf += "prompt 0\n"
318 syslinux_conf += "timeout 1\n"
319 syslinux_conf += "\n"
320 syslinux_conf += "default vesamenu.c32\n"
321 syslinux_conf += "menu autoboot Starting %s...\n" % self.distro_name
322 syslinux_conf += "menu hidden\n"
323 syslinux_conf += "\n"
324 syslinux_conf += "%s\n" % splashline
325 syslinux_conf += "menu title Welcome to %s!\n" % self.distro_name
326 syslinux_conf += "menu color border 0 #ffffffff #00000000\n"
327 syslinux_conf += "menu color sel 7 #ffffffff #ff000000\n"
328 syslinux_conf += "menu color title 0 #ffffffff #00000000\n"
329 syslinux_conf += "menu color tabmsg 0 #ffffffff #00000000\n"
330 syslinux_conf += "menu color unsel 0 #ffffffff #00000000\n"
331 syslinux_conf += "menu color hotsel 0 #ff000000 #ffffffff\n"
332 syslinux_conf += "menu color hotkey 7 #ffffffff #ff000000\n"
333 syslinux_conf += "menu color timeout_msg 0 #ffffffff #00000000\n"
334 syslinux_conf += "menu color timeout 0 #ffffffff #00000000\n"
335 syslinux_conf += "menu color cmdline 0 #ffffffff #00000000\n"
338 kernels = self._get_kernel_versions()
339 symkern = "%s/boot/vmlinuz" % self._instroot
341 if os.path.lexists(symkern):
342 v = os.path.realpath(symkern).replace('%s-' % symkern, "")
343 syslinux_conf += "label %s\n" % self.distro_name.lower()
344 syslinux_conf += "\tmenu label %s (%s)\n" % (self.distro_name, v)
345 syslinux_conf += "\tlinux ../vmlinuz\n"
346 if self._ptable_format == 'msdos':
349 if not root_part_uuid:
350 raise MountError("Cannot find the root GPT partition UUID")
351 rootstr = "PARTUUID=%s" % root_part_uuid
352 syslinux_conf += "\tappend ro root=%s %s\n" % (rootstr, options)
353 syslinux_conf += "\tmenu default\n"
355 for kernel in kernels:
356 for version in kernels[kernel]:
357 versions.append(version)
361 syslinux_conf += "label %s%d\n" \
362 % (self.distro_name.lower(), footlabel)
363 syslinux_conf += "\tmenu label %s (%s)\n" % (self.distro_name, v)
364 syslinux_conf += "\tlinux ../vmlinuz-%s\n" % v
365 syslinux_conf += "\tappend ro root=%s %s\n" \
368 syslinux_conf += "\tmenu default\n"
371 msger.debug("Writing syslinux config %s/boot/extlinux/extlinux.conf" \
373 cfg = open(self._instroot + "/boot/extlinux/extlinux.conf", "w")
374 cfg.write(syslinux_conf)
377 def _install_syslinux(self):
378 for name in self.__disks.keys():
379 loopdev = self.__disks[name].device
382 mbrfile = "%s/usr/share/syslinux/" % self._instroot
383 if self._ptable_format == 'gpt':
384 mbrfile += "gptmbr.bin"
388 msger.debug("Installing syslinux bootloader '%s' to %s" % \
391 rc = runner.show(['dd', 'if=%s' % mbrfile, 'of=' + loopdev])
393 raise MountError("Unable to set MBR to %s" % loopdev)
396 # Ensure all data is flushed to disk before doing syslinux install
399 fullpathsyslinux = fs_related.find_binary_path("extlinux")
400 rc = runner.show([fullpathsyslinux,
402 "%s/boot/extlinux" % self._instroot])
404 raise MountError("Unable to install syslinux bootloader to %s" \
407 def _create_bootconfig(self):
408 #If syslinux is available do the required configurations.
409 if self._need_extlinux \
410 and os.path.exists("%s/usr/share/syslinux/" % (self._instroot)) \
411 and os.path.exists("%s/boot/extlinux/" % (self._instroot)):
412 self._create_syslinux_config()
413 self._install_syslinux()
415 def _unmount_instroot(self):
416 if not self.__instloop is None:
418 self.__instloop.cleanup()
419 except MountError, err:
420 msger.warning("%s" % err)
422 def _resparse(self, size = None):
423 return self.__instloop.resparse(size)
425 def _get_post_scripts_env(self, in_chroot):
426 env = BaseImageCreator._get_post_scripts_env(self, in_chroot)
428 # Export the file-system UUIDs and partition UUIDs (AKA PARTUUIDs)
429 for p in self.__instloop.partitions:
430 env.update(self._set_part_env(p['ks_pnum'], "UUID", p['uuid']))
431 env.update(self._set_part_env(p['ks_pnum'], "PARTUUID", p['partuuid']))
432 env.update(self._set_part_env(p['ks_pnum'], "DEVNODE_NOW",
434 env.update(self._set_part_env(p['ks_pnum'], "DISK_DEVNODE_NOW",
435 self.__disks[p['disk_name']].device))
439 def _stage_final_image(self):
440 """Stage the final system image in _outdir.
445 if self.compress_image:
446 for imgfile in os.listdir(self.__imgdir):
447 if imgfile.endswith('.raw') or imgfile.endswith('bin'):
448 imgpath = os.path.join(self.__imgdir, imgfile)
449 msger.info("Compressing image %s" % imgfile)
450 misc.compressing(imgpath, self.compress_image)
453 dst = os.path.join(self._outdir, self.pack_to)
454 msger.info("Pack all raw images to %s" % dst)
455 misc.packing(dst, self.__imgdir)
457 msger.debug("moving disks to stage location")
458 for imgfile in os.listdir(self.__imgdir):
459 src = os.path.join(self.__imgdir, imgfile)
460 dst = os.path.join(self._outdir, imgfile)
461 msger.debug("moving %s to %s" % (src,dst))
463 self._write_image_xml()
465 def _write_image_xml(self):
467 if self.target_arch and self.target_arch.startswith("arm"):
472 if self.appliance_version:
473 name_attributes += " version='%s'" % self.appliance_version
474 if self.appliance_release:
475 name_attributes += " release='%s'" % self.appliance_release
476 xml += " <name%s>%s</name>\n" % (name_attributes, self.name)
478 # XXX don't hardcode - determine based on the kernel we installed for
479 # grub baremetal vs xen
480 xml += " <boot type='hvm'>\n"
482 xml += " <arch>%s</arch>\n" % imgarch
485 xml += " <loader dev='hd'/>\n"
489 for name in self.__disks.keys():
490 full_name = self._full_name(name, self.__disk_format)
491 xml += " <drive disk='%s' target='hd%s'/>\n" \
492 % (full_name, chr(ord('a') + i))
496 xml += " <devices>\n"
497 xml += " <vcpu>%s</vcpu>\n" % self.vcpu
498 xml += " <memory>%d</memory>\n" %(self.vmem * 1024)
499 for network in self.ks.handler.network.network:
500 xml += " <interface/>\n"
501 xml += " <graphics/>\n"
502 xml += " </devices>\n"
503 xml += " </domain>\n"
504 xml += " <storage>\n"
506 if self.checksum is True:
507 for name in self.__disks.keys():
508 diskpath = self._full_path(self._outdir, name, \
510 full_name = self._full_name(name, self.__disk_format)
512 msger.debug("Generating disk signature for %s" % full_name)
514 xml += " <disk file='%s' use='system' format='%s'>\n" \
515 % (full_name, self.__disk_format)
517 hashes = misc.calc_hashes(diskpath, ('sha1', 'sha256'))
519 xml += " <checksum type='sha1'>%s</checksum>\n" \
521 xml += " <checksum type='sha256'>%s</checksum>\n" \
525 for name in self.__disks.keys():
526 full_name = self._full_name(name, self.__disk_format)
527 xml += " <disk file='%s' use='system' format='%s'/>\n" \
528 % (full_name, self.__disk_format)
530 xml += " </storage>\n"
533 msger.debug("writing image XML to %s/%s.xml" %(self._outdir, self.name))
534 cfg = open("%s/%s.xml" % (self._outdir, self.name), "w")
538 def generate_bmap(self):
539 """ Generate block map file for the image. The idea is that while disk
540 images we generate may be large (e.g., 4GiB), they may actually contain
541 only little real data, e.g., 512MiB. This data are files, directories,
542 file-system meta-data, partition table, etc. In other words, when
543 flashing the image to the target device, you do not have to copy all the
544 4GiB of data, you can copy only 512MiB of it, which is 4 times faster.
546 This function generates the block map file for an arbitrary image that
547 mic has generated. The block map file is basically an XML file which
548 contains a list of blocks which have to be copied to the target device.
549 The other blocks are not used and there is no need to copy them. """
551 if self.bmap_needed is None:
554 from mic.utils import BmapCreate
555 msger.info("Generating the map file(s)")
557 for name in self.__disks.keys():
558 image = self._full_path(self.__imgdir, name, self.__disk_format)
559 bmap_file = self._full_path(self._outdir, name, "bmap")
561 msger.debug("Generating block map file '%s'" % bmap_file)
564 creator = BmapCreate.BmapCreate(image, bmap_file)
567 except BmapCreate.Error as err:
568 raise CreatorError("Failed to create bmap file: %s" % str(err))