rename all micng to mic
[tools/mic.git] / mic / imager / raw.py
1 #
2 # raw.py: RawImageCreator class
3 #
4 # Copyright 2007-2008, Red Hat  Inc.
5 # Copyright 2008, Daniel P. Berrange
6 #
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; version 2 of the License.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU Library General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19
20 import os
21 import stat
22 import glob
23 import shutil
24 import zipfile
25 import tarfile
26 import subprocess
27 import logging
28
29 import mic.utils.kickstart as kickstart 
30 import mic.utils.fs_related as fs_related
31 import urlgrabber.progress as progress
32 from baseimager import BaseImageCreator
33 from mic.utils.partitionedfs import PartitionedMount
34 from mic.utils.errors import *
35
36
37 class RawImageCreator(BaseImageCreator):
38     """Installs a system into a file containing a partitioned disk image.
39
40         ApplianceImageCreator is an advanced ImageCreator subclass; a sparse file
41         is formatted with a partition table, each partition loopback mounted
42         and the system installed into an virtual disk. The disk image can
43         subsequently be booted in a virtual machine or accessed with kpartx
44     """
45
46     def __init__(self, *args):
47         """Initialize a ApplianceImageCreator instance.
48
49             This method takes the same arguments as ImageCreator.__init__()
50         """
51         BaseImageCreator.__init__(self, *args)
52
53         self.__instloop = None
54         self.__imgdir = None
55         self.__disks = {}
56         self.__disk_format = "raw"
57         self.vmem = 512
58         self.vcpu = 1
59         self.checksum = False
60         self.appliance_version = None
61         self.appliance_release = None
62         #self.getsource = False
63         #self.listpkg = False
64
65         self._dep_checks.extend(["sync", "kpartx", "parted", "extlinux"])
66
67     def configure(self, repodata = None):
68         def chroot():
69
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"], preexec_fn = chroot)
75         BaseImageCreator.configure(self, repodata)
76         
77     def _get_fstab(self):
78         s = ""
79         for mp in self.__instloop.mountOrder:
80             p = None
81             for p1 in self.__instloop.partitions:
82                 if p1['mountpoint'] == mp:
83                     p = p1
84                     break
85
86             s += "%(device)s  %(mountpoint)s         %(fstype)s   %(fsopts)s 0 0\n" %  {
87                  'device': "/dev/%s%-d" % (p['disk'], p['num']),
88                  'mountpoint': p['mountpoint'],
89                  'fstype': p['fstype'],
90                  'fsopts': "defaults,noatime" if not p['fsopts'] else p['fsopts']}
91
92             if p['mountpoint'] == "/":
93                 for subvol in self.__instloop.subvolumes:
94                     if subvol['mountpoint'] == "/":
95                         continue
96                     s += "%(device)s  %(mountpoint)s         %(fstype)s   %(fsopts)s 0 0\n" %  {
97                          'device': "/dev/%s%-d" % (p['disk'], p['num']),
98                          'mountpoint': subvol['mountpoint'],
99                          'fstype': p['fstype'],
100                          'fsopts': "defaults,noatime" if not subvol['fsopts'] else subvol['fsopts']}
101
102         s += "devpts     /dev/pts  devpts  gid=5,mode=620   0 0\n"
103         s += "tmpfs      /dev/shm  tmpfs   defaults         0 0\n"
104         s += "proc       /proc     proc    defaults         0 0\n"
105         s += "sysfs      /sys      sysfs   defaults         0 0\n"
106         return s
107
108     def _create_mkinitrd_config(self):
109         #write  to tell which modules to be included in initrd
110
111         mkinitrd = ""
112         mkinitrd += "PROBE=\"no\"\n"
113         mkinitrd += "MODULES+=\"ext3 ata_piix sd_mod libata scsi_mod\"\n"
114         mkinitrd += "rootfs=\"ext3\"\n"
115         mkinitrd += "rootopts=\"defaults\"\n"
116
117         logging.debug("Writing mkinitrd config %s/etc/sysconfig/mkinitrd" % self._instroot)
118         os.makedirs(self._instroot + "/etc/sysconfig/",mode=644)
119         cfg = open(self._instroot + "/etc/sysconfig/mkinitrd", "w")
120         cfg.write(mkinitrd)
121         cfg.close()
122
123     #
124     # Actual implementation
125     #
126     def _mount_instroot(self, base_on = None):
127         self.__imgdir = self._mkdtemp()
128
129         #Set a default partition if no partition is given out
130         if not self.ks.handler.partition.partitions:
131             partstr = "part / --size 1900 --ondisk sda --fstype=ext3"
132             args = partstr.split()
133             pd = self.ks.handler.partition.parse(args[1:])
134             if pd not in self.ks.handler.partition.partitions:
135                 self.ks.handler.partition.partitions.append(pd)
136
137         #list of partitions from kickstart file
138         parts = kickstart.get_partitions(self.ks)
139
140         #list of disks where a disk is an dict with name: and size
141         disks = []
142
143         for i in range(len(parts)):
144             if parts[i].disk:
145                 disk = parts[i].disk
146             else:
147                 raise CreatorError("Failed to create disks, no --ondisk specified in partition line of ks file")
148
149             if not parts[i].fstype:
150                  raise CreatorError("Failed to create disks, no --fstype specified in partition line of ks file")
151
152             size =   parts[i].size * 1024L * 1024L
153             
154             found = False
155             for j in range(len(disks)):
156                 if disks[j]['name'] == disk:
157                     disks[j]['size'] = disks[j]['size'] + size
158                     found = True
159                     break
160                 else: 
161                     found = False
162             if not found:
163                 disks.append({ 'name': disk, 'size': size })
164
165         #create disk
166         for item in disks:
167             logging.debug("Adding disk %s as %s/%s-%s.raw" % (item['name'], self.__imgdir,self.name, item['name']))
168             disk = fs_related.SparseLoopbackDisk("%s/%s-%s.raw" % (self.__imgdir,self.name, item['name']),item['size'])
169             self.__disks[item['name']] = disk
170
171         self.__instloop = PartitionedMount(self.__disks, self._instroot)
172
173         for p in parts:
174             self.__instloop.add_partition(int(p.size), p.disk, p.mountpoint, p.fstype, fsopts = p.fsopts, boot = p.active)
175
176         try:
177             self.__instloop.mount()
178         except MountError, e:
179             raise CreatorError("Failed mount disks : %s" % e)
180
181         self._create_mkinitrd_config()
182
183     def _get_required_packages(self):
184         required_packages = BaseImageCreator._get_required_packages(self)
185         if not self.target_arch or not self.target_arch.startswith("arm"):
186             required_packages += ["syslinux", "syslinux-extlinux"]
187         return required_packages
188
189     def _get_excluded_packages(self):
190         return BaseImageCreator._get_excluded_packages(self)
191
192     def _get_syslinux_boot_config(self):
193         bootdevnum = None
194         rootdevnum = None
195         rootdev = None
196         for p in self.__instloop.partitions:
197             if p['mountpoint'] == "/boot":
198                 bootdevnum = p['num'] - 1
199             elif p['mountpoint'] == "/" and bootdevnum is None:
200                 bootdevnum = p['num'] - 1
201
202             if p['mountpoint'] == "/":
203                 rootdevnum = p['num'] - 1
204                 rootdev = "/dev/%s%-d" % (p['disk'], p['num'])
205
206         prefix = ""
207         if bootdevnum == rootdevnum:
208             prefix = "/boot"
209
210         return (bootdevnum, rootdevnum, rootdev, prefix)
211
212     def _create_syslinux_config(self):
213         #Copy splash
214         splash = "%s/usr/lib/anaconda-runtime/syslinux-vesa-splash.jpg" % self._instroot
215         if os.path.exists(splash):
216             shutil.copy(splash, "%s%s/splash.jpg" % (self._instroot, "/boot/extlinux"))
217             splashline = "menu background splash.jpg"
218         else:
219             splashline = ""
220
221         (bootdevnum, rootdevnum, rootdev, prefix) = self._get_syslinux_boot_config()
222         options = self.ks.handler.bootloader.appendLine
223
224         #XXX don't hardcode default kernel - see livecd code
225         syslinux_conf = ""
226         syslinux_conf += "prompt 0\n"
227         syslinux_conf += "timeout 1\n"
228         syslinux_conf += "\n"
229         syslinux_conf += "default vesamenu.c32\n"
230         syslinux_conf += "menu autoboot Starting %s...\n" % self.distro_name
231         syslinux_conf += "menu hidden\n"
232         syslinux_conf += "\n"
233         syslinux_conf += "%s\n" % splashline
234         syslinux_conf += "menu title Welcome to %s!\n" % self.distro_name
235         syslinux_conf += "menu color border 0 #ffffffff #00000000\n"
236         syslinux_conf += "menu color sel 7 #ffffffff #ff000000\n"
237         syslinux_conf += "menu color title 0 #ffffffff #00000000\n"
238         syslinux_conf += "menu color tabmsg 0 #ffffffff #00000000\n"
239         syslinux_conf += "menu color unsel 0 #ffffffff #00000000\n"
240         syslinux_conf += "menu color hotsel 0 #ff000000 #ffffffff\n"
241         syslinux_conf += "menu color hotkey 7 #ffffffff #ff000000\n"
242         syslinux_conf += "menu color timeout_msg 0 #ffffffff #00000000\n"
243         syslinux_conf += "menu color timeout 0 #ffffffff #00000000\n"
244         syslinux_conf += "menu color cmdline 0 #ffffffff #00000000\n"
245
246         versions = []
247         kernels = self._get_kernel_versions()
248         for kernel in kernels:
249             for version in kernels[kernel]:
250                 versions.append(version)
251
252         footlabel = 0
253         for v in versions:
254             shutil.copy("%s/boot/vmlinuz-%s" %(self._instroot, v),
255                         "%s%s/vmlinuz-%s" % (self._instroot, "/boot/extlinux/", v))
256             syslinux_conf += "label %s%d\n" % (self.distro_name.lower(), footlabel)
257             syslinux_conf += "\tmenu label %s (%s)\n" % (self.distro_name, v)
258             syslinux_conf += "\tkernel vmlinuz-%s\n" % v
259             syslinux_conf += "\tappend ro root=%s quiet vga=current %s\n" % (rootdev, options)
260             if footlabel == 0:
261                syslinux_conf += "\tmenu default\n"
262             footlabel += 1;
263
264         logging.debug("Writing syslinux config %s/boot/extlinux/extlinux.conf" % self._instroot)
265         cfg = open(self._instroot + "/boot/extlinux/extlinux.conf", "w")
266         cfg.write(syslinux_conf)
267         cfg.close()
268
269     def _install_syslinux(self):
270         i = 0
271         for name in self.__disks.keys():
272             loopdev = self.__disks[name].device
273             i =i+1
274
275         logging.debug("Installing syslinux bootloader to %s" % loopdev)
276
277         (bootdevnum, rootdevnum, rootdev, prefix) = self._get_syslinux_boot_config()
278
279
280         #Set MBR
281         mbrsize = os.stat("%s/usr/share/syslinux/mbr.bin" % self._instroot)[stat.ST_SIZE]
282         ddcmd = fs_related.find_binary_path("dd")
283         rc = subprocess.call([ddcmd, "if=%s/usr/share/syslinux/mbr.bin" % self._instroot, "of=" + loopdev])
284         if rc != 0:
285             raise MountError("Unable to set MBR to %s" % loopdev)
286
287         #Set Bootable flag
288         parted = fs_related.find_binary_path("parted")
289         dev_null = os.open("/dev/null", os.O_WRONLY)
290         rc = subprocess.call([parted, "-s", loopdev, "set", "%d" % (bootdevnum + 1), "boot", "on"],
291                              stdout = dev_null, stderr = dev_null)
292         os.close(dev_null)
293         #XXX disabled return code check because parted always fails to
294         #reload part table with loop devices. Annoying because we can't
295         #distinguish this failure from real partition failures :-(
296         if rc != 0 and 1 == 0:
297             raise MountError("Unable to set bootable flag to %sp%d" % (loopdev, (bootdevnum + 1)))
298
299
300         #Ensure all data is flushed to disk before doing syslinux install
301         subprocess.call(["sync"])
302
303         fullpathsyslinux = fs_related.find_binary_path("extlinux")
304         rc = subprocess.call([fullpathsyslinux, "-i", "%s/boot/extlinux" % self._instroot])
305         if rc != 0:
306             raise MountError("Unable to install syslinux bootloader to %sp%d" % (loopdev, (bootdevnum + 1)))
307
308     def _create_bootconfig(self):
309         #If syslinux is available do the required configurations.
310         if os.path.exists("%s/usr/share/syslinux/" % (self._instroot)) \
311            and os.path.exists("%s/boot/extlinux/" % (self._instroot)):
312             self._create_syslinux_config()
313             self._install_syslinux()
314
315     def _unmount_instroot(self):
316         if not self.__instloop is None:
317             self.__instloop.cleanup()
318
319     def _resparse(self, size = None):
320         return self.__instloop.resparse(size)
321
322     def _stage_final_image(self):
323         """Stage the final system image in _outdir.
324            write meta data
325         """
326         self._resparse()
327
328         logging.debug("moving disks to stage location")
329         for name in self.__disks.keys():
330             src = "%s/%s-%s.raw" % (self.__imgdir, self.name,name)
331             self._img_name = "%s-%s.%s" % (self.name, name, self.__disk_format)
332             dst = "%s/%s" % (self._outdir, self._img_name)
333             logging.debug("moving %s to %s" % (src,dst))
334             shutil.move(src,dst) 
335         self._write_image_xml()
336
337     def _write_image_xml(self):
338         imgarch = "i686"
339         if self.target_arch and self.target_arch.startswith("arm"):
340             imgarch = "arm"
341         xml = "<image>\n"
342
343         name_attributes = ""
344         if self.appliance_version:
345             name_attributes += " version='%s'" % self.appliance_version
346         if self.appliance_release:
347             name_attributes += " release='%s'" % self.appliance_release
348         xml += "  <name%s>%s</name>\n" % (name_attributes, self.name)
349         xml += "  <domain>\n"
350         # XXX don't hardcode - determine based on the kernel we installed for grub
351         # baremetal vs xen
352         xml += "    <boot type='hvm'>\n"
353         xml += "      <guest>\n"
354         xml += "        <arch>%s</arch>\n" % imgarch
355         xml += "      </guest>\n"
356         xml += "      <os>\n"
357         xml += "        <loader dev='hd'/>\n"
358         xml += "      </os>\n"
359
360         i = 0
361         for name in self.__disks.keys():
362             xml += "      <drive disk='%s-%s.%s' target='hd%s'/>\n" % (self.name,name, self.__disk_format,chr(ord('a')+i))
363             i = i + 1
364
365         xml += "    </boot>\n"
366         xml += "    <devices>\n"
367         xml += "      <vcpu>%s</vcpu>\n" % self.vcpu
368         xml += "      <memory>%d</memory>\n" %(self.vmem * 1024)
369         for network in self.ks.handler.network.network:
370             xml += "      <interface/>\n"
371         xml += "      <graphics/>\n"
372         xml += "    </devices>\n"
373         xml += "  </domain>\n"
374         xml += "  <storage>\n"
375
376         if self.checksum is True:
377             for name in self.__disks.keys():
378                 diskpath = "%s/%s-%s.%s" % (self._outdir,self.name,name, self.__disk_format)
379                 disk_size = os.path.getsize(diskpath)
380                 meter_ct = 0
381                 meter = progress.TextMeter()
382                 meter.start(size=disk_size, text="Generating disk signature for %s-%s.%s" % (self.name,name,self.__disk_format))
383                 xml += "    <disk file='%s-%s.%s' use='system' format='%s'>\n" % (self.name,name, self.__disk_format, self.__disk_format)
384
385                 try:
386                     import hashlib
387                     m1 = hashlib.sha1()
388                     m2 = hashlib.sha256()
389                 except:
390                     import sha
391                     m1 = sha.new()
392                     m2 = None
393                 f = open(diskpath,"r")
394                 while 1:
395                     chunk = f.read(65536)
396                     if not chunk:
397                         break
398                     m1.update(chunk)
399                     if m2:
400                        m2.update(chunk)
401                     meter.update(meter_ct)
402                     meter_ct = meter_ct + 65536
403
404                 sha1checksum = m1.hexdigest()
405                 xml +=  """      <checksum type='sha1'>%s</checksum>\n""" % sha1checksum
406
407                 if m2:
408                     sha256checksum = m2.hexdigest()
409                     xml += """      <checksum type='sha256'>%s</checksum>\n""" % sha256checksum
410                 xml += "    </disk>\n"
411         else:
412             for name in self.__disks.keys():
413                 xml += "    <disk file='%s-%s.%s' use='system' format='%s'/>\n" % (self.name,name, self.__disk_format, self.__disk_format)
414
415         xml += "  </storage>\n"
416         xml += "</image>\n"
417
418         logging.debug("writing image XML to %s/%s.xml" %  (self._outdir, self.name))
419         cfg = open("%s/%s.xml" % (self._outdir, self.name), "w")
420         cfg.write(xml)
421         cfg.close()
422         #print "Wrote: %s.xml" % self.name