diff --git a/API b/API index 9783c74..ef9c1aa 100644 --- a/API +++ b/API @@ -76,3 +76,7 @@ switching from the LiveImageCreator to another ImageCreator object. build live images which use dm-snapshot, etc. This is what is used by livecd-creator. +* DiskImageCreator: This generates disk images containing multiple + partitions in a loopback file. It installs grub in the MBR and + can be directly booted in any virtual machine + diff --git a/Makefile b/Makefile index 050fe88..d3d4ab7 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,7 @@ all: install: $(INSTALL_PROGRAM) -D tools/livecd-creator $(DESTDIR)/usr/bin/livecd-creator $(INSTALL_PROGRAM) -D tools/image-creator $(DESTDIR)/usr/bin/image-creator + $(INSTALL_PROGRAM) -D tools/disk-creator $(DESTDIR)/usr/bin/disk-creator $(INSTALL_PROGRAM) -D tools/livecd-iso-to-disk.sh $(DESTDIR)/usr/bin/livecd-iso-to-disk $(INSTALL_PROGRAM) -D tools/livecd-iso-to-pxeboot.sh $(DESTDIR)/usr/bin/livecd-iso-to-pxeboot $(INSTALL_PROGRAM) -D tools/mayflower $(DESTDIR)/usr/lib/livecd-creator/mayflower @@ -34,6 +35,8 @@ install: uninstall: rm -f $(DESTDIR)/usr/bin/livecd-creator rm -rf $(DESTDIR)/usr/lib/livecd-creator + rm -rf $(DESTDIR)/usr/bin/disk-creator + rm -rf $(DESTDIR)/usr/bin/image-creator rm -rf $(DESTDIR)/usr/share/doc/livecd-tools-$(VERSION) rm -rf $(DESTDIR)/usr/share/livecd-tools diff --git a/imgcreate/__init__.py b/imgcreate/__init__.py index e535014..44f3ec5 100644 --- a/imgcreate/__init__.py +++ b/imgcreate/__init__.py @@ -21,6 +21,7 @@ from imgcreate.creator import * from imgcreate.yuminst import * from imgcreate.kickstart import * from imgcreate.fs import * +from imgcreate.disk import * """A set of classes for building Fedora system images. @@ -28,6 +29,7 @@ The following image creators are available: - ImageCreator - installs to a directory - LoopImageCreator - installs to an ext3 image - LiveImageCreator - installs to a bootable ISO + - DiskImageCreator - installs to a partitioned disk image Also exported are: - CreatorError - all exceptions throw are of this type @@ -60,6 +62,7 @@ __all__ = ( 'ImageCreator', 'LiveImageCreator', 'LoopImageCreator', + 'DiskImageCreator', 'FSLABEL_MAXLEN', 'read_kickstart', 'construct_name' diff --git a/imgcreate/creator.py b/imgcreate/creator.py index c2ed770..fb3b309 100644 --- a/imgcreate/creator.py +++ b/imgcreate/creator.py @@ -207,7 +207,11 @@ class ImageCreator(object): """ s = "/dev/root / %s defaults,noatime 0 0\n" %(self._fstype) - s += "devpts /dev/pts devpts gid=5,mode=620 0 0\n" + s += self._get_fstab_special() + return s + + def _get_fstab_special(self): + s = "devpts /dev/pts devpts gid=5,mode=620 0 0\n" s += "tmpfs /dev/shm tmpfs defaults 0 0\n" s += "proc /proc proc defaults 0 0\n" s += "sysfs /sys sysfs defaults 0 0\n" @@ -816,12 +820,11 @@ class LoopImageCreator(ImageCreator): if not base_on is None: shutil.copyfile(base_on, self._image) - self.__instloop = SparseExtLoopbackMount(self._image, - self._instroot, - self.__image_size, - self.__fstype, - self.__blocksize, - self.fslabel) + self.__instloop = ExtDiskMount(SparseLoopbackDisk(self._image, self.__image_size), + self._instroot, + self.__fstype, + self.__blocksize, + self.fslabel) try: self.__instloop.mount() diff --git a/imgcreate/disk.py b/imgcreate/disk.py index a0dd6e9..6027b75 100644 --- a/imgcreate/disk.py +++ b/imgcreate/disk.py @@ -37,7 +37,7 @@ class DiskImageCreator(ImageCreator): """ - def __init__(self, ks, name): + def __init__(self, ks, name, disks, format="raw"): """Initialize a DiskImageCreator instance. This method takes the same arguments as ImageCreator.__init__() @@ -47,32 +47,20 @@ class DiskImageCreator(ImageCreator): self.__instloop = None self.__imgdir = None - - # XXX configure me :-) - self.__disk_size = 8192L * 1024 * 1024 - - def __get_disk(self): - if self.__imgdir is None: - raise CreatorError("_disk is not valid before calling mount()") - return self.__imgdir + "/disk.dsk" - _disk = property(__get_disk) - """The location of the disk file. - - This is the path to the filesystem disk. Subclasses may use this path - in order to package the disk in _stage_final_disk(). - - Note, this directory does not exist before DiskCreator.mount() is called. - - Note also, this is a read-only attribute. - - """ + self.__disks = disks + self.__format = format def _get_fstab(self): s = "" - # XXX > 1 disk, don't assume sda - for p in self.__instloop.partitions: + for mp in self.__instloop.mountOrder: + p = None + for p1 in self.__instloop.partitions: + if p1['mountpoint'] == mp: + p = p1 + break + s += "%(device)s %(mountpoint)s %(fstype)s defaults,noatime 0 0\n" % { - 'device': "/dev/sda%-d" % p['num'], + 'device': "/dev/%s%-d" % (p['disk'], p['num']), 'mountpoint': p['mountpoint'], 'fstype': p['fstype'] } @@ -86,18 +74,35 @@ class DiskImageCreator(ImageCreator): self.__imgdir = self._mkdtemp() parts = kickstart.get_partitions(self.ks) + + devnames = [] + for p in parts: + dev = p.disk + if not dev in devnames: + devnames.append(dev) + devnames.sort() - self.__instloop = PartitionedMount(SparseLoopbackDisk(self._disk, self.__disk_size), + disks = [] + + if len(devnames) != len(self.__disks): + raise CreatorError("Not enough disks for declared partitions") + + for i in range(len(self.__disks)): + disk = SparseLoopbackDisk("%s/disk%d.raw" % (self.__imgdir, i), + self.__disks[i]['size']) + self.__disks[i]['disk'] = disk + disks.append(disk) + + self.__instloop = PartitionedMount(disks, devnames, self._instroot) for p in parts: - self.__instloop.add_partition(int(p.size), p.mountpoint, p.fstype) + self.__instloop.add_partition(int(p.size), p.disk, p.mountpoint, p.fstype) try: self.__instloop.mount() except MountError, e: - raise CreatorError("Failed mount '%s' : %s" % - (self._disk, e)) + raise CreatorError("Failed mount disks : %s" % e) def _get_required_packages(self): return ["grub"] @@ -110,6 +115,8 @@ class DiskImageCreator(ImageCreator): if not dev in devs: devs.append(dev) + devs.sort() + n = 0 devmap = "" for dev in devs: @@ -124,7 +131,6 @@ class DiskImageCreator(ImageCreator): bootdevnum = None rootdevnum = None rootdev = None - # XXX > 1 disk, don't assume sda for p in self.__instloop.partitions: if p['mountpoint'] == "/boot": bootdevnum = p['num'] - 1 @@ -133,7 +139,7 @@ class DiskImageCreator(ImageCreator): if p['mountpoint'] == "/": rootdevnum = p['num'] - 1 - rootdev = "/dev/sda%-d" % p['num'] + rootdev = "/dev/%s%-d" % (p['disk'], p['num']) prefix = "" if bootdevnum == rootdevnum: @@ -144,11 +150,15 @@ class DiskImageCreator(ImageCreator): def _create_grub_config(self): (bootdevnum, rootdevnum, rootdev, prefix) = self._get_grub_boot_config() + # NB we're assuming that grub config is on the first physical disk + # ie /boot must be on sda, or if there's no /boot, then / must be sda + + # XXX don't hardcode default kernel - see livecd code grub = "" grub += "default=0\n" grub += "timeout=5\n" - grub += "splashimage=(hd0,%-d)%s/grub/splash.xpm.gz\n" % (rootdevnum, prefix) - grub += "hiddenemnu\n" + grub += "splashimage=(hd0,%d)%s/grub/splash.xpm.gz\n" % (bootdevnum, prefix) + grub += "hiddenmenu\n" versions = [] kernels = self._get_kernel_versions() @@ -158,11 +168,11 @@ class DiskImageCreator(ImageCreator): for v in versions: grub += "title Fedora (%s)\n" % v - grub += " root (hd0,%-d)\n" % bootdevnum + grub += " root (hd0,%d)\n" % bootdevnum grub += " kernel %s/vmlinuz-%s ro root=%s\n" % (prefix, v, rootdev) grub += " initrd %s/initrd-%s.img\n" % (prefix, v) - cfg = open(self._instroot + "/boot/grub/grub.cfg", "w") + cfg = open(self._instroot + "/boot/grub/grub.conf", "w") cfg.write(grub) cfg.close() @@ -185,11 +195,18 @@ class DiskImageCreator(ImageCreator): def _install_grub(self): (bootdevnum, rootdevnum, rootdev, prefix) = self._get_grub_boot_config() + # Ensure all data is flushed to disk before doing grub install + subprocess.call(["sync"]) + + stage2 = self._instroot + "/boot/grub/stage2" + setup = "" - setup += "device (hd0) %s\n" % (self.__instloop.disk.device) - setup += "root (hd0,%d)\n" % rootdevnum - setup += "configfile (hd0,%d)%s/grub/grub.cfg" % (bootdevnum, prefix) - setup += "setup --prefix=%s/grub (hd0) (hd0,%d)\n" % (prefix, bootdevnum) + for i in range(len(self.__disks)): + loopdev = self.__disks[i]['disk'].device + setup += "device (hd%d) %s\n" % (i, loopdev) + setup += "root (hd0,%d)\n" % bootdevnum + setup += "setup --stage2=%s --prefix=%s/grub (hd0)\n" % (stage2, prefix) + setup += "quit\n" grub = subprocess.Popen(["grub", "--batch", "--no-floppy"], stdin=subprocess.PIPE) @@ -211,7 +228,58 @@ class DiskImageCreator(ImageCreator): def _resparse(self, size = None): return self.__instloop.resparse(size) + + def _write_image_xml(self): + xml = "\n" + xml += " %s\n" % self.name + xml += " \n" + # XXX don't hardcode - determine based on the kernel we installed for grub + # baremetal vs xen + xml += " \n" + xml += " \n" + xml += " %s\n" % os.uname()[4] + xml += " \n" + xml += " \n" + xml += " \n" + xml += " \n" + for i in range(len(self.__disks)): + xml += " \n" % (self.__disks[i]['name'], self.__format, chr(ord('a')+i)) + xml += " \n" + xml += " \n" + xml += " 1\n" + xml += " %d\n" %(256 * 1024) + xml += " \n" + xml += " \n" + xml += " \n" + xml += " \n" + xml += " \n" + for i in range(len(self.__disks)): + # XXX don't hardcode raw + xml += " \n" % (self.__disks[i]['name'], self.__format, self.__format) + xml += " \n" + xml += "\n" + + print "Writing %s/%s.xml" % (self._outdir, self.name) + cfg = open("%s/%s.xml" % (self._outdir, self.name), "w") + cfg.write(xml) + cfg.close() + + def _stage_final_image(self): self._resparse() - print "Copying image to %s " % (self._outdir + "/" + self.name + ".dsk",) - shutil.move(self._disk, self._outdir + "/" + self.name + ".dsk") + + print "Writing image XML" + self._write_image_xml() + print "Moving disks to final location" + for i in range(len(self.__disks)): + dst = "%s/%s.%s" % (self._outdir, self.__disks[i]['name'], self.__format) + if self.__format == "raw": + print "Moving tmp %s image to %s " % (self.__disks[i]['disk'].lofile, dst) + shutil.move(self.__disks[i]['disk'].lofile, dst) + else: + rc = subprocess.call(["qemu-img", "convert", + "-f", "raw", self.__disks[i]['disk'].lofile, + "-O", self.__format, dst]) + if rc != 0: + raise CreatorError("Unable to convert disk to %s" % (self.__format)) + print "All done" diff --git a/imgcreate/fs.py b/imgcreate/fs.py index 9ca3a3e..8d73102 100644 --- a/imgcreate/fs.py +++ b/imgcreate/fs.py @@ -86,42 +86,51 @@ class BindChrootMount: subprocess.call(["/bin/umount", self.dest]) self.mounted = False -class LoopbackMount: - def __init__(self, lofile, mountdir, fstype = None): - self.lofile = lofile - self.mountdir = mountdir - self.fstype = fstype +class Disk: + def __init__(self, size, device = None): + self._device = device + self._size = size - self.mounted = False - self.losetup = False - self.rmdir = False - self.loopdev = None + def create(self): + pass def cleanup(self): - self.unmount() - self.lounsetup() + pass - def unmount(self): - if self.mounted: - rc = subprocess.call(["/bin/umount", self.mountdir]) - if rc == 0: - self.mounted = False + def get_device(self): + return self._device + def set_device(self, path): + self._device = path + device = property(get_device, set_device) - if self.rmdir and not self.mounted: - try: - os.rmdir(self.mountdir) - except OSError, e: - pass - self.rmdir = False + def get_size(self): + return self._size + size = property(get_size) + + +class RawDisk(Disk): + def __init__(self, size, device): + Disk.__init__(self, size, device) + + def fixed(self): + return True - def lounsetup(self): - if self.losetup: - rc = subprocess.call(["/sbin/losetup", "-d", self.loopdev]) - self.losetup = False - self.loopdev = None + def exists(self): + return True - def loopsetup(self): - if self.losetup: +class LoopbackDisk(Disk): + def __init__(self, lofile, size): + Disk.__init__(self, size) + self.lofile = lofile + + def fixed(self): + return False + + def exists(self): + return os.path.exists(self.lofile) + + def create(self): + if self.device is not None: return losetupProc = subprocess.Popen(["/sbin/losetup", "-f"], @@ -132,40 +141,27 @@ class LoopbackMount: raise MountError("Failed to allocate loop device for '%s'" % self.lofile) - self.loopdev = losetupOutput.split()[0] + device = losetupOutput.split()[0] - rc = subprocess.call(["/sbin/losetup", self.loopdev, self.lofile]) + print "Losetup add %s %s" % (device, self.lofile) + rc = subprocess.call(["/sbin/losetup", device, self.lofile]) if rc != 0: raise MountError("Failed to allocate loop device for '%s'" % self.lofile) + self.device = device - self.losetup = True - - def mount(self): - if self.mounted: + def cleanup(self): + if self.device is None: return + print "Losetup remove %s" % self.device + rc = subprocess.call(["/sbin/losetup", "-d", self.device]) + self.device = None - self.loopsetup() - - if not os.path.isdir(self.mountdir): - os.makedirs(self.mountdir) - self.rmdir = True - - args = [ "/bin/mount", self.loopdev, self.mountdir ] - if self.fstype: - args.extend(["-t", self.fstype]) - - rc = subprocess.call(args) - if rc != 0: - raise MountError("Failed to mount '%s' to '%s'" % - (self.loopdev, self.mountdir)) - self.mounted = True -class SparseLoopbackMount(LoopbackMount): - def __init__(self, lofile, mountdir, size, fstype = None): - LoopbackMount.__init__(self, lofile, mountdir, fstype) - self.size = size +class SparseLoopbackDisk(LoopbackDisk): + def __init__(self, lofile, size): + LoopbackDisk.__init__(self, lofile, size) def expand(self, create = False, size = None): flags = os.O_WRONLY @@ -191,10 +187,81 @@ class SparseLoopbackMount(LoopbackMount): def create(self): self.expand(create = True) + LoopbackDisk.create(self) -class SparseExtLoopbackMount(SparseLoopbackMount): - def __init__(self, lofile, mountdir, size, fstype, blocksize, fslabel): - SparseLoopbackMount.__init__(self, lofile, mountdir, size, fstype) +class Mount: + def __init__(self, mountdir): + self.mountdir = mountdir + + def cleanup(self): + self.unmount() + + def mount(self): + pass + + def unmount(self): + pass + +class DiskMount(Mount): + def __init__(self, disk, mountdir, fstype = None, rmmountdir = True): + Mount.__init__(self, mountdir) + + self.disk = disk + self.fstype = fstype + self.rmmountdir = rmmountdir + + self.mounted = False + self.rmdir = False + + def cleanup(self): + Mount.cleanup(self) + self.disk.cleanup() + + def unmount(self): + if self.mounted: + rc = subprocess.call(["/bin/umount", self.mountdir]) + if rc == 0: + self.mounted = False + + if self.rmdir and not self.mounted: + try: + os.rmdir(self.mountdir) + except OSError, e: + pass + self.rmdir = False + + + def __create(self): + print "Disk create %s" % str(self) + self.disk.create() + + + def mount(self): + if self.mounted: + return + + print "Disk mount" + if not os.path.isdir(self.mountdir): + os.makedirs(self.mountdir) + self.rmdir = self.rmmountdir + + self.__create() + + print "Do mount %s " % self.disk.device + args = [ "/bin/mount", self.disk.device, self.mountdir ] + if self.fstype: + args.extend(["-t", self.fstype]) + + rc = subprocess.call(args) + if rc != 0: + raise MountError("Failed to mount '%s' to '%s'" % + (self.disk.device, self.mountdir)) + + self.mounted = True + +class ExtDiskMount(DiskMount): + def __init__(self, disk, mountdir, fstype, blocksize, fslabel, rmmountdir=True): + DiskMount.__init__(self, disk, mountdir, fstype, rmmountdir) self.blocksize = blocksize self.fslabel = fslabel @@ -202,19 +269,15 @@ class SparseExtLoopbackMount(SparseLoopbackMount): rc = subprocess.call(["/sbin/mkfs." + self.fstype, "-F", "-L", self.fslabel, "-m", "1", "-b", str(self.blocksize), - self.lofile, - str(self.size / self.blocksize)]) + self.disk.device]) + # str(self.disk.size / self.blocksize)]) if rc != 0: raise MountError("Error creating %s filesystem" % (self.fstype,)) subprocess.call(["/sbin/tune2fs", "-c0", "-i0", "-Odir_index", - "-ouser_xattr,acl", self.lofile]) - - def create(self): - SparseLoopbackMount.create(self) - self.__format_filesystem() + "-ouser_xattr,acl", self.disk.device]) - def resize(self, size = None): - current_size = os.stat(self.lofile)[stat.ST_SIZE] + def __resize_filesystem(self, size = None): + current_size = os.stat(self.disk.lofile)[stat.ST_SIZE] if size is None: size = self.size @@ -227,21 +290,30 @@ class SparseExtLoopbackMount(SparseLoopbackMount): self.__fsck() - resize2fs(self.lofile, size) - - if size < current_size: - self.truncate(size) + resize2fs(self.disk.lofile, size) return size - def mount(self): - if not os.path.isfile(self.lofile): - self.create() + def __create(self): + resize = False + if not self.disk.fixed() and self.disk.exists(): + resize = True + + #DiskMount.__create(self) + self.disk.create() + + if resize: + print "Fs resie" + self.__resize_filesystem() else: - self.resize() - return SparseLoopbackMount.mount(self) + print "FS format" + self.__format_filesystem() + + def mount(self): + self.__create() + DiskMount.mount(self) def __fsck(self): - subprocess.call(["/sbin/e2fsck", "-f", "-y", self.lofile]) + subprocess.call(["/sbin/e2fsck", "-f", "-y", self.disk.lofile]) def __get_size_from_filesystem(self): def parse_field(output, field): @@ -253,7 +325,7 @@ class SparseExtLoopbackMount(SparseLoopbackMount): dev_null = os.open("/dev/null", os.O_WRONLY) try: - out = subprocess.Popen(['/sbin/dumpe2fs', '-h', self.lofile], + out = subprocess.Popen(['/sbin/dumpe2fs', '-h', self.disk.lofile], stdout = subprocess.PIPE, stderr = dev_null).communicate()[0] finally: @@ -273,7 +345,7 @@ class SparseExtLoopbackMount(SparseLoopbackMount): while top != (bot + 1): t = bot + ((top - bot) / 2) - if not resize2fs(self.lofile, t): + if not resize2fs(self.disk.lofile, t): top = t else: bot = t @@ -281,12 +353,243 @@ class SparseExtLoopbackMount(SparseLoopbackMount): def resparse(self, size = None): self.cleanup() - minsize = self.__resize_to_minimal() + self.disk.truncate(minsize) + return minsize + +class PartitionedMount(Mount): + def __init__(self, disks, devnames, mountdir): + Mount.__init__(self, mountdir) + self.disks = {} + for i in range(len(disks)): + d = disks[i] + devname = devnames[i] + print "Assigned disk %s" % devname + self.disks[devname] = { 'disk': d, # Disk object + 'mapped': False, # True if kpartx mapping exists + 'numpart': 0, # Number of allocate partitions + 'partitions': [], # indexes to self.partitions + 'extended': 0, # Size of extended partition + 'offset': 0 } # Offset of next partition + self.partitions = [] + self.mapped = False + self.mountOrder = [] + self.unmountOrder = [] + + def add_partition(self, size, disk, mountpoint, fstype = None): + self.partitions.append({'size': size, + 'mountpoint': mountpoint, # Mount relative to chroot + 'fstype': fstype, + 'disk': disk, # physical disk name holding partition + 'device': None, # kpartx device node for partition + 'mount': None, # Mount object + 'num': None}) # Partition number + + def __format_disks(self): + print "Assigning disks" + for dev in self.disks.keys(): + d = self.disks[dev] + print "Initializing partition table for %s" % (d['disk'].device) + rc = subprocess.call(["/sbin/parted", "-s", d['disk'].device, "mklabel", "msdos"]) + if rc != 0: + raise MountError("Error writing partition table on %s" % d.device) + + print "Assigning partitions" + for n in range(len(self.partitions)): + p = self.partitions[n] + + if not self.disks.has_key(p['disk']): + raise MountError("No disk %s for partition %s" % (p['disk'], p['mountpoint'])) + + d = self.disks[p['disk']] + d['numpart'] += 1 + if d['numpart'] > 3: + # Increase allocation of extended partition to hold this partition + d['extended'] += p['size'] + p['type'] = 'logical' + p['num'] = d['numpart'] + 1 + else: + p['type'] = 'primary' + p['num'] = d['numpart'] + + p['start'] = d['offset'] + d['offset'] += p['size'] + d['partitions'].append(n) + print "Assigned %s to %s%d at %d at size %d" % (p['mountpoint'], p['disk'], p['num'], p['start'], p['size']) + + # XXX we should probably work in cylinder units to keep fdisk happier.. + start = 0 + print "Creating partitions" + for p in self.partitions: + d = self.disks[p['disk']] + if p['num'] == 5: + print "Added extended part at %d of size %d" % (p['start'], d['extended']) + rc = subprocess.call(["/sbin/parted", "-s", d['disk'].device, "mkpart", "extended", + "%dM" % p['start'], "%dM" % (p['start'] + d['extended'])]) + + print "Add %s part at %d of size %d" % (p['type'], p['start'], p['size']) + rc = subprocess.call(["/sbin/parted", "-s", d['disk'].device, "mkpart", + p['type'], "%dM" % p['start'], "%dM" % (p['start']+p['size'])]) + + # XXX disabled return code check because parted always fails to + # reload part table with loop devices. Annoying because we can't + # distinguish this failure from real partition failures :-( + if rc != 0 and 1 == 0: + raise MountError("Error creating partition on %s" % d['disk'].device) + + def __map_partitions(self): + for dev in self.disks.keys(): + d = self.disks[dev] + if d['mapped']: + continue + + kpartx = subprocess.Popen(["/sbin/kpartx", "-l", d['disk'].device], + stdout=subprocess.PIPE) + + kpartxOutput = kpartx.communicate()[0].split("\n") + # Strip trailing blank + kpartxOutput = kpartxOutput[0:len(kpartxOutput)-1] + print "Partx planned mapping %s" % str(kpartxOutput) + if kpartx.returncode: + raise MountError("Failed to query partition mapping for '%s'" % + d.device) + + # Quick sanity check that the number of partitions matches + # our expectation. If it doesn't, someone broke the code + # further up + if len(kpartxOutput) != d['numpart']: + raise MountError("Unexpected number of partitions from kpartx: %d != %d" % + (len(kpartxOutput), d['numpart'])) + + for i in range(len(kpartxOutput)): + print " Got %s" % (kpartxOutput[i]) + line = kpartxOutput[i] + newdev = line.split()[0] + mapperdev = "/dev/mapper/" + newdev + loopdev = d['disk'].device + newdev[-1] + + print "Dev %s: %s -> %s" % (newdev, loopdev, mapperdev) + pnum = d['partitions'][i] + self.partitions[pnum]['device'] = loopdev + + # grub's install wants partitions to be named + # to match their parent device + partition num + # kpartx doesn't work like this, so we add compat + # symlinks to point to /dev/mapper + os.symlink(mapperdev, loopdev) + + print "Adding partx mapping for %s" % d['disk'].device + rc = subprocess.call(["/sbin/kpartx", "-a", d['disk'].device]) + if rc != 0: + raise MountError("Failed to map partitions for '%s'" % + d['disk'].device) + d['mapped'] = True + + + def __unmap_partitions(self): + for dev in self.disks.keys(): + d = self.disks[dev] + if not d['mapped']: + continue + + print "Removing compat symlinks" + for pnum in d['partitions']: + if self.partitions[pnum]['device'] != None: + os.unlink(self.partitions[pnum]['device']) + self.partitions[pnum]['device'] = None + + print "Unmapping" + rc = subprocess.call(["/sbin/kpartx", "-d", d['disk'].device]) + if rc != 0: + raise MountError("Failed to unmap partitions for '%s'" % + d['disk'].device) + + d['mapped'] = False + + + def __calculate_mountorder(self): + for p in self.partitions: + self.mountOrder.append(p['mountpoint']) + self.unmountOrder.append(p['mountpoint']) + + self.mountOrder.sort() + self.unmountOrder.sort() + self.unmountOrder.reverse() + print str(self.mountOrder) - self.truncate(minsize) + def cleanup(self): + print "Doing cleanup" + import time + #time.sleep(20) + Mount.cleanup(self) + print "Unmap parts" + self.__unmap_partitions() + print "Cleanup disks" + for dev in self.disks.keys(): + d = self.disks[dev] + try: + print "Cleanup disk %s -> %s" % (d['disk'].lofile, d['disk'].device) + d['disk'].cleanup() + except: + pass + + def unmount(self): + print "Do unmount" + for mp in self.unmountOrder: + if mp == 'swap': + continue + p = None + for p1 in self.partitions: + if p1['mountpoint'] == mp: + p = p1 + break + + if p['mount'] != None: + print "Clenaup %s " % p['mountpoint'] + try: + p['mount'].cleanup() + except: + pass + p['mount'] = None + print "Done unmount" + + def mount(self): + for dev in self.disks.keys(): + d = self.disks[dev] + d['disk'].create() + + self.__format_disks() + self.__map_partitions() + self.__calculate_mountorder() + + for mp in self.mountOrder: + p = None + for p1 in self.partitions: + if p1['mountpoint'] == mp: + p = p1 + break + + if mp == 'swap': + subprocess.call(["/sbin/mkswap", p['device']]) + continue + + print "Submount %s " % p['mountpoint'] + rmmountdir = False + if p['mountpoint'] == "/": + rmmountdir = True + pdisk = ExtDiskMount(RawDisk(p['size'] * 1024 * 1024, p['device']), + self.mountdir + p['mountpoint'], + p['fstype'], + 4096, + p['mountpoint'], + rmmountdir) + pdisk.mount() + p['mount'] = pdisk + + def resparse(self, size = None): + # Can't re-sparse a disk image - too hard + pass - return self.resize(size) class DeviceMapperSnapshot(object): def __init__(self, imgloop, cowloop): @@ -306,8 +609,8 @@ class DeviceMapperSnapshot(object): if self.__created: return - self.imgloop.loopsetup() - self.cowloop.loopsetup() + self.imgloop.create() + self.cowloop.create() self.__name = "imgcreate-%d-%d" % (os.getpid(), random.randint(0, 2**16)) @@ -315,8 +618,8 @@ class DeviceMapperSnapshot(object): size = os.stat(self.imgloop.lofile)[stat.ST_SIZE] table = "0 %d snapshot %s %s p 8" % (size / 512, - self.imgloop.loopdev, - self.cowloop.loopdev) + self.imgloop.device, + self.cowloop.device) args = ["/sbin/dmsetup", "create", self.__name, "--table", table] if subprocess.call(args) != 0: @@ -382,15 +685,14 @@ class DeviceMapperSnapshot(object): # 8) Create a squashfs of the COW # def create_image_minimizer(path, image, minimal_size): - imgloop = LoopbackMount(image, "None") + imgloop = LoopbackDisk(image, None) # Passing bogus size - doesn't matter - cowloop = SparseLoopbackMount(os.path.join(os.path.dirname(path), "osmin"), - None, 64L * 1024L * 1024L) + cowloop = SparseLoopbackDisk(os.path.join(os.path.dirname(path), "osmin"), + 64L * 1024L * 1024L) snapshot = DeviceMapperSnapshot(imgloop, cowloop) try: - cowloop.create() snapshot.create() resize2fs(snapshot.path, minimal_size) diff --git a/imgcreate/kickstart.py b/imgcreate/kickstart.py index a7e0723..a92f877 100644 --- a/imgcreate/kickstart.py +++ b/imgcreate/kickstart.py @@ -468,6 +468,9 @@ def get_groups(ks, required = []): def get_excluded(ks, required = []): return ks.handler.packages.excludedList + required +def get_partitions(ks, required = []): + return ks.handler.partition.partitions + def ignore_missing(ks): return ks.handler.packages.handleMissing == ksconstants.KS_MISSING_IGNORE diff --git a/imgcreate/live.py b/imgcreate/live.py index bbb17ef..dc2aea9 100644 --- a/imgcreate/live.py +++ b/imgcreate/live.py @@ -130,7 +130,7 @@ class LiveImageCreatorBase(LoopImageCreator): # def __base_on_iso(self, base_on): """helper function to extract ext3 file system from a live CD ISO""" - isoloop = LoopbackMount(base_on, self._mkdtemp()) + isoloop = Mount(LoopbackDisk(base_on), self._mkdtemp()) try: isoloop.mount() @@ -144,10 +144,10 @@ class LiveImageCreatorBase(LoopImageCreator): else: squashimg = isoloop.mountdir + "/LiveOS/squashfs.img" - squashloop = LoopbackMount(squashimg, self._mkdtemp(), "squashfs") + squashloop = Mount(LoopbackDisk(squashimg), self._mkdtemp(), "squashfs") try: - if not os.path.exists(squashloop.lofile): + if not squashloop.disk.exists(): raise CreatorError("'%s' is not a valid live CD ISO : " "squashfs.img doesn't exist" % base_on) diff --git a/livecd-tools.spec b/livecd-tools.spec index a789460..d9118ba 100644 --- a/livecd-tools.spec +++ b/livecd-tools.spec @@ -58,6 +58,7 @@ rm -rf $RPM_BUILD_ROOT %dir %{_datadir}/livecd-tools %{_datadir}/livecd-tools/* %{_bindir}/image-creator +%{_bindir}/disk-creator %dir %{python_sitelib}/imgcreate %{python_sitelib}/imgcreate/*.py %{python_sitelib}/imgcreate/*.pyo diff --git a/tools/disk-creator b/tools/disk-creator index 52584f6..acd6bd7 100755 --- a/tools/disk-creator +++ b/tools/disk-creator @@ -25,10 +25,16 @@ import optparse import imgcreate def parse_options(args): - parser = optparse.OptionParser(usage = "%prog [--name=] ") + parser = optparse.OptionParser(usage = "%prog [--disk=] [--size=] ") parser.add_option("-n", "--name", type="string", dest="name", - help="Disk name") + help="Appliance name") + parser.add_option("-d", "--disk", type="string", dest="disk", + action="append", help="Disk file name") + parser.add_option("-s", "--size", type="float", dest="size", + action="append", help="Disk size in MB") + parser.add_option("-f", "--format", type="string", dest="format", + help="Disk format (raw, qcow2, vmdk, ...)") (options, args) = parser.parse_args() @@ -51,12 +57,45 @@ def main(): print >> sys.stderr, "Error loading kickstart file '%s' : %s" % (kscfg, e) return 1 + disks = [] + + print str(options.disk) + print str(options.size) + if options.disk is None: + name = imgcreate.build_name(kscfg) + size = 4096L * 1024L * 1024L + if options.size is not None: + if type(options.size) != float: + print >> sys.stderr, "Error too many disk sizes provided" + return 1 + size = options.size * 1024L * 1024L + + disks.append({ 'name': name, 'size': size }) + elif type(options.disk) == list: + if type(options.size) != list or len(options.size) != len(options.disk): + print >> sys.stderr, "Error must provide a size for each disk" + return 1 + + for i in range(len(options.disk)): + disks.append({ 'name': options.disk[i], 'size': options.size[i] * 1024L * 1024L }) + else: + size = 4096L * 1024L * 1024L + if options.size is not None: + if type(options.size) != float: + print >> sys.stderr, "Error too many disk sizes provided" + return 1 + size = options.size * 1024L * 1024L + + disks.append({ 'name': options.disk, 'size': size }) + + name = imgcreate.build_name(kscfg) if options.name: name = options.name - else: - name = imgcreate.build_name(kscfg) - creator = imgcreate.DiskImageCreator(ks, name) + format = "raw" + if options.format: + format = options.format + creator = imgcreate.DiskImageCreator(ks, name, disks, format) try: creator.create()