raw.py: create device nodes to access own disk
[tools/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
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
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, "bootloader")
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 mount(self, base_on = None, cachedir = None):
222         """
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.
227
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).
232         """
233
234         def copy_devnode(src, dest):
235             """A helper function for copying device nodes."""
236
237             stat_obj = os.stat(src)
238             assert stat.S_ISBLK(stat_obj.st_mode)
239
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
245             # correct.
246             os.chmod(dest, stat_obj.st_mode)
247
248         BaseImageCreator.mount(self, base_on, cachedir)
249
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)
254
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'])
260
261     def unmount(self):
262         """
263         Remove loop/dm device nodes which we created in 'mount()' and call the
264         base class' 'unmount()' method.
265         """
266
267         for p in self.__instloop.partitions:
268             path = self._instroot + p['mapper_device']
269             if os.path.exists(path):
270                 os.unlink(path)
271
272         path = self._instroot + "/dev/mapper"
273         if os.path.exists(path):
274             os.rmdir(path)
275
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):
280                     os.unlink(path)
281
282         BaseImageCreator.unmount(self)
283
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
290
291     def _get_excluded_packages(self):
292         return BaseImageCreator._get_excluded_packages(self)
293
294     def _get_syslinux_boot_config(self):
295         rootdev = None
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']
301
302         return (rootdev, root_part_uuid)
303
304     def _create_syslinux_config(self):
305
306         splash = os.path.join(self._instroot, "boot/extlinux")
307         if os.path.exists(splash):
308             splashline = "menu background splash.jpg"
309         else:
310             splashline = ""
311
312         (rootdev, root_part_uuid) = self._get_syslinux_boot_config()
313         options = self.ks.handler.bootloader.appendLine
314
315         #XXX don't hardcode default kernel - see livecd code
316         syslinux_conf = ""
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"
336
337         versions = []
338         kernels = self._get_kernel_versions()
339         symkern = "%s/boot/vmlinuz" % self._instroot
340
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':
347                 rootstr = rootdev
348             else:
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"
354         else:
355             for kernel in kernels:
356                 for version in kernels[kernel]:
357                     versions.append(version)
358
359             footlabel = 0
360             for v in versions:
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" \
366                                  % (rootdev, options)
367                 if footlabel == 0:
368                     syslinux_conf += "\tmenu default\n"
369                 footlabel += 1;
370
371         msger.debug("Writing syslinux config %s/boot/extlinux/extlinux.conf" \
372                     % self._instroot)
373         cfg = open(self._instroot + "/boot/extlinux/extlinux.conf", "w")
374         cfg.write(syslinux_conf)
375         cfg.close()
376
377     def _install_syslinux(self):
378         for name in self.__disks.keys():
379             loopdev = self.__disks[name].device
380
381             # Set MBR
382             mbrfile = "%s/usr/share/syslinux/" % self._instroot
383             if self._ptable_format == 'gpt':
384                 mbrfile += "gptmbr.bin"
385             else:
386                 mbrfile += "mbr.bin"
387
388             msger.debug("Installing syslinux bootloader '%s' to %s" % \
389                         (mbrfile, loopdev))
390
391             rc = runner.show(['dd', 'if=%s' % mbrfile, 'of=' + loopdev])
392             if rc != 0:
393                 raise MountError("Unable to set MBR to %s" % loopdev)
394
395
396             # Ensure all data is flushed to disk before doing syslinux install
397             runner.quiet('sync')
398
399             fullpathsyslinux = fs_related.find_binary_path("extlinux")
400             rc = runner.show([fullpathsyslinux,
401                               "-i",
402                               "%s/boot/extlinux" % self._instroot])
403             if rc != 0:
404                 raise MountError("Unable to install syslinux bootloader to %s" \
405                                  % loopdev)
406
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()
414
415     def _unmount_instroot(self):
416         if not self.__instloop is None:
417             try:
418                 self.__instloop.cleanup()
419             except MountError, err:
420                 msger.warning("%s" % err)
421
422     def _resparse(self, size = None):
423         return self.__instloop.resparse(size)
424
425     def _get_post_scripts_env(self, in_chroot):
426         env = BaseImageCreator._get_post_scripts_env(self, in_chroot)
427
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",
433                                           p['mapper_device']))
434             env.update(self._set_part_env(p['ks_pnum'], "DISK_DEVNODE_NOW",
435                                           self.__disks[p['disk_name']].device))
436
437         return env
438
439     def _stage_final_image(self):
440         """Stage the final system image in _outdir.
441            write meta data
442         """
443         self._resparse()
444
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)
451
452         if self.pack_to:
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)
456         else:
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))
462                 shutil.move(src,dst)
463         self._write_image_xml()
464
465     def _write_image_xml(self):
466         imgarch = "i686"
467         if self.target_arch and self.target_arch.startswith("arm"):
468             imgarch = "arm"
469         xml = "<image>\n"
470
471         name_attributes = ""
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)
477         xml += "  <domain>\n"
478         # XXX don't hardcode - determine based on the kernel we installed for
479         # grub baremetal vs xen
480         xml += "    <boot type='hvm'>\n"
481         xml += "      <guest>\n"
482         xml += "        <arch>%s</arch>\n" % imgarch
483         xml += "      </guest>\n"
484         xml += "      <os>\n"
485         xml += "        <loader dev='hd'/>\n"
486         xml += "      </os>\n"
487
488         i = 0
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))
493             i = i + 1
494
495         xml += "    </boot>\n"
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"
505
506         if self.checksum is True:
507             for name in self.__disks.keys():
508                 diskpath = self._full_path(self._outdir, name, \
509                                            self.__disk_format)
510                 full_name = self._full_name(name, self.__disk_format)
511
512                 msger.debug("Generating disk signature for %s" % full_name)
513
514                 xml += "    <disk file='%s' use='system' format='%s'>\n" \
515                        % (full_name, self.__disk_format)
516
517                 hashes = misc.calc_hashes(diskpath, ('sha1', 'sha256'))
518
519                 xml +=  "      <checksum type='sha1'>%s</checksum>\n" \
520                         % hashes[0]
521                 xml += "      <checksum type='sha256'>%s</checksum>\n" \
522                        % hashes[1]
523                 xml += "    </disk>\n"
524         else:
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)
529
530         xml += "  </storage>\n"
531         xml += "</image>\n"
532
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")
535         cfg.write(xml)
536         cfg.close()
537
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.
545
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. """
550
551         if self.bmap_needed is None:
552             return
553
554         from mic.utils import BmapCreate
555         msger.info("Generating the map file(s)")
556
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")
560
561             msger.debug("Generating block map file '%s'" % bmap_file)
562
563             try:
564                 creator = BmapCreate.BmapCreate(image, bmap_file)
565                 creator.generate()
566                 del creator
567             except BmapCreate.Error as err:
568                 raise CreatorError("Failed to create bmap file: %s" % str(err))