Merge release-0.28.17 from 'tools/mic'
[platform/upstream/mic.git] / mic / imager / raw.py
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
18 import os
19 import stat
20 import shutil
21
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
28
29 class 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     img_format = 'raw'
38
39     def __init__(self, creatoropts=None, pkgmgr=None, compress_image=None, generate_bmap=None, fstab_entry="uuid"):
40         """Initialize a ApplianceImageCreator instance.
41
42             This method takes the same arguments as ImageCreator.__init__()
43         """
44         BaseImageCreator.__init__(self, creatoropts, pkgmgr)
45
46         self.__instloop = None
47         self.__imgdir = None
48         self.__disks = {}
49         self.__disk_format = "raw"
50         self._disk_names = []
51         self._ptable_format = self.ks.handler.bootloader.ptable
52         self.vmem = 512
53         self.vcpu = 1
54         self.checksum = False
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
62         #self.listpkg = False
63
64         self._dep_checks.extend(["sync", "kpartx", "parted"])
65         if self._need_extlinux:
66             self._dep_checks.extend(["extlinux"])
67
68     def configure(self, repodata = None):
69         import subprocess
70         def chroot():
71             os.chroot(self._instroot)
72             os.chdir("/")
73
74         if os.path.exists(self._instroot + "/usr/bin/Xorg"):
75             subprocess.call(["/bin/chmod", "u+s", "/usr/bin/Xorg"],
76                             preexec_fn = chroot)
77
78         BaseImageCreator.configure(self, repodata)
79
80     def _get_fstab(self):
81         s = ""
82         for mp in self.__instloop.mount_order:
83             p = None
84             for p1 in self.__instloop.partitions:
85                 if p1['mountpoint'] == mp:
86                     p = p1
87                     break
88
89             if self.use_uuid and p['uuid']:
90                 device = "UUID=%s" % p['uuid']
91             else:
92                 device = "/dev/%s%-d" % (p['disk_name'], p['num'])
93
94             s += "%(device)s  %(mountpoint)s  %(fstype)s  %(fsopts)s 0 0\n" % {
95                'device': device,
96                'mountpoint': p['mountpoint'],
97                'fstype': p['fstype'],
98                'fsopts': "defaults,noatime" if not p['fsopts'] else p['fsopts']}
99
100             if p['mountpoint'] == "/":
101                 for subvol in self.__instloop.subvolumes:
102                     if subvol['mountpoint'] == "/":
103                         continue
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']}
109
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"
114         return s
115
116     def _create_mkinitrd_config(self):
117         """write to tell which modules to be included in initrd"""
118
119         mkinitrd = ""
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"
124
125         msger.debug("Writing mkinitrd config %s/etc/sysconfig/mkinitrd" \
126                     % self._instroot)
127         os.makedirs(self._instroot + "/etc/sysconfig/",mode=644)
128         cfg = open(self._instroot + "/etc/sysconfig/mkinitrd", "w")
129         cfg.write(mkinitrd)
130         cfg.close()
131
132     def _get_parts(self):
133         if not self.ks:
134             raise CreatorError("Failed to get partition info, "
135                                "please check your kickstart setting.")
136
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)
144
145         # partitions list from kickstart file
146         return kickstart.get_partitions(self.ks)
147
148     def get_disk_names(self):
149         """ Returns a list of physical target disk names (e.g., 'sdb') which
150         will be created. """
151
152         if self._disk_names:
153             return self._disk_names
154
155         #get partition info from ks handler
156         parts = self._get_parts()
157
158         for i in range(len(parts)):
159             if parts[i].disk:
160                 disk_name = parts[i].disk
161             else:
162                 raise CreatorError("Failed to create disks, no --ondisk "
163                                    "specified in partition line of ks file")
164
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")
169
170             self._disk_names.append(disk_name)
171
172         return self._disk_names
173
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)
177
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))
181
182     #
183     # Actual implemention
184     #
185     def _mount_instroot(self, base_on = None):
186         parts = self._get_parts()
187         self.__instloop = PartitionedMount(self._instroot)
188
189         for p in parts:
190             self.__instloop.add_partition(int(p.size),
191                                           p.disk,
192                                           p.mountpoint,
193                                           p.fstype,
194                                           p.label,
195                                           fsopts = p.fsopts,
196                                           boot = p.active,
197                                           align = p.align,
198                                           part_type = p.part_type)
199
200         self.__instloop.layout_partitions(self._ptable_format)
201
202         # Create the disks
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']))
208
209             disk_obj = fs_related.SparseLoopbackDisk(full_path,
210                                                      disk['min_size'])
211             self.__disks[disk_name] = disk_obj
212             self.__instloop.add_disk(disk_name, disk_obj)
213
214         self.__instloop.mount()
215         self._create_mkinitrd_config()
216
217     def mount(self, base_on = None, cachedir = None):
218         """
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.
223
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).
228         """
229
230         def copy_devnode(src, dest):
231             """A helper function for copying device nodes."""
232
233             if not src:
234                 return
235
236             stat_obj = os.stat(src)
237             assert stat.S_ISBLK(stat_obj.st_mode)
238
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
244             # correct.
245             os.chmod(dest, stat_obj.st_mode)
246
247         BaseImageCreator.mount(self, base_on, cachedir)
248
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)
253
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'])
261
262     def unmount(self):
263         """
264         Remove loop/dm device nodes which we created in 'mount()' and call the
265         base class' 'unmount()' method.
266         """
267
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):
272                     os.unlink(path)
273             if p['mpath_device']:
274                 path = self._instroot + p['mpath_device']
275                 if os.path.exists(path):
276                     os.unlink(path)
277
278         path = self._instroot + "/dev/mapper"
279         if os.path.exists(path):
280             shutil.rmtree(path, ignore_errors=True)
281
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):
286                     os.unlink(path)
287
288         BaseImageCreator.unmount(self)
289
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
296
297     def _get_excluded_packages(self):
298         return BaseImageCreator._get_excluded_packages(self)
299
300     def _get_syslinux_boot_config(self):
301         rootdev = None
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']
307
308         return (rootdev, root_part_uuid)
309
310     def _create_syslinux_config(self):
311
312         splash = os.path.join(self._instroot, "boot/extlinux")
313         if os.path.exists(splash):
314             splashline = "menu background splash.jpg"
315         else:
316             splashline = ""
317
318         (rootdev, root_part_uuid) = self._get_syslinux_boot_config()
319         options = self.ks.handler.bootloader.appendLine
320
321         #XXX don't hardcode default kernel - see livecd code
322         syslinux_conf = ""
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"
342
343         versions = []
344         kernels = self._get_kernel_versions()
345         symkern = "%s/boot/vmlinuz" % self._instroot
346
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':
353                 rootstr = rootdev
354             else:
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"
360         else:
361             for kernel in kernels:
362                 for version in kernels[kernel]:
363                     versions.append(version)
364
365             footlabel = 0
366             for v in versions:
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" \
372                                  % (rootdev, options)
373                 if footlabel == 0:
374                     syslinux_conf += "\tmenu default\n"
375                 footlabel += 1
376
377         msger.debug("Writing syslinux config %s/boot/extlinux/extlinux.conf" \
378                     % self._instroot)
379         cfg = open(self._instroot + "/boot/extlinux/extlinux.conf", "w")
380         cfg.write(syslinux_conf)
381         cfg.close()
382
383     def _install_syslinux(self):
384         for name in self.__disks.keys():
385             loopdev = self.__disks[name].device
386
387             # Set MBR
388             mbrfile = "%s/usr/share/syslinux/" % self._instroot
389             if self._ptable_format == 'gpt':
390                 mbrfile += "gptmbr.bin"
391             else:
392                 mbrfile += "mbr.bin"
393
394             msger.debug("Installing syslinux bootloader '%s' to %s" % \
395                         (mbrfile, loopdev))
396
397             rc = runner.show(['dd', 'if=%s' % mbrfile, 'of=' + loopdev])
398             if rc != 0:
399                 raise MountError("Unable to set MBR to %s" % loopdev)
400
401
402             # Ensure all data is flushed to disk before doing syslinux install
403             runner.quiet('sync')
404
405             fullpathsyslinux = fs_related.find_binary_path("extlinux")
406             rc = runner.show([fullpathsyslinux,
407                               "-i",
408                               "%s/boot/extlinux" % self._instroot])
409             if rc != 0:
410                 raise MountError("Unable to install syslinux bootloader to %s" \
411                                  % loopdev)
412
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()
420
421     def _unmount_instroot(self):
422         if not self.__instloop is None:
423             try:
424                 self.__instloop.cleanup()
425             except MountError as err:
426                 msger.warning("%s" % err)
427
428     def _resparse(self, size = None):
429         return self.__instloop.resparse(size)
430
431     def _get_post_scripts_env(self, in_chroot):
432         env = BaseImageCreator._get_post_scripts_env(self, in_chroot)
433
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",
439                                           p['mapper_device']))
440             env.update(self._set_part_env(p['ks_pnum'], "DISK_DEVNODE_NOW",
441                                           self.__disks[p['disk_name']].device))
442
443         return env
444
445     def _stage_final_image(self):
446         """Stage the final system image in _outdir.
447            write meta data
448         """
449         self._resparse()
450         self.image_files.update({'disks': self.__disks.keys()})
451
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(
458                                    {'image': imgfile})
459                             self.image_files.setdefault('image_files',
460                                    []).append(imgfile)
461
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(
473                                    {'image': imgname})
474                             self.image_files.setdefault('image_files',
475                                     []).append(imgname)
476
477         if self.pack_to:
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})
482         else:
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))
488                 shutil.move(src,dst)
489
490         self._write_image_xml()
491
492     def _write_image_xml(self):
493         imgarch = "i686"
494         if self.target_arch and self.target_arch.startswith("arm"):
495             imgarch = "arm"
496         xml = "<image>\n"
497
498         name_attributes = ""
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)
504         xml += "  <domain>\n"
505         # XXX don't hardcode - determine based on the kernel we installed for
506         # grub baremetal vs xen
507         xml += "    <boot type='hvm'>\n"
508         xml += "      <guest>\n"
509         xml += "        <arch>%s</arch>\n" % imgarch
510         xml += "      </guest>\n"
511         xml += "      <os>\n"
512         xml += "        <loader dev='hd'/>\n"
513         xml += "      </os>\n"
514
515         i = 0
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))
520             i = i + 1
521
522         xml += "    </boot>\n"
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"
532
533         if self.checksum is True:
534             for name in self.__disks.keys():
535                 diskpath = self._full_path(self._outdir, name, \
536                                            self.__disk_format)
537                 full_name = self._full_name(name, self.__disk_format)
538
539                 msger.debug("Generating disk signature for %s" % full_name)
540
541                 xml += "    <disk file='%s' use='system' format='%s'>\n" \
542                        % (full_name, self.__disk_format)
543
544                 hashes = misc.calc_hashes(diskpath, ('sha1', 'sha256'))
545
546                 xml +=  "      <checksum type='sha1'>%s</checksum>\n" \
547                         % hashes[0]
548                 xml += "      <checksum type='sha256'>%s</checksum>\n" \
549                        % hashes[1]
550                 xml += "    </disk>\n"
551         else:
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)
556
557         xml += "  </storage>\n"
558         xml += "</image>\n"
559
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")
562         cfg.write(xml)
563         cfg.close()
564
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.
572
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. """
577
578         if self.bmap_needed is None:
579             return
580
581         msger.info("Generating the map file(s)")
582
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)})
588
589             msger.debug("Generating block map file '%s'" % bmap_file)
590             
591             bmaptoolcmd = misc.find_binary_path('bmaptool')
592             rc = runner.show([bmaptoolcmd, 'create', image, '-o', bmap_file])
593             if rc != 0:
594                 raise CreatorError("Failed to create bmap file: %s" % bmap_file)
595
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()
600
601     def remove_exclude_image(self):
602         pass