#!/usr/bin/env python # domUloader.py """Loader for kernel and (optional) ramdisk from domU filesystem Uses bootentry = [dev:]kernel[,initrd] to get a kernel [and initrd] from a domU filesystem to boot it for Xen. dev is the disk as seen by domU, filenames are relative to that filesystem. The script uses the disk settings from the config file to find the domU filesystems. The bootentry is passed to the script using --entry= Optionally, dev: can be omitted; the script then looks at the root filesystem, parses /etc/fstab to resolve the path to the kernel [and the initrd]. Obviously, the paths relative to the domU root filesystem needs to be specified for the kernel and initrd filenames. The root FS is passed using --root=, the filesystem setup in --disks=. The disks list is a python list [[uname, dev, mode, backend], [uname, dev, mode, backend], ...] passed as a string. The script writes an sxpr specifying the locations of the copied kernel and initrd into the file specified by --output (default is stdout). Limitations: - It is assumed both kernel and initrd are on the same filesystem. - domUs might use LVM; the script currently does not have support for setting up LVM mappings for domUs; it's not trivial and we might risk namespace conflicts. If you want to use LVM inside domUs, set up a small non-LVM boot partition and specify it in bootentry. The script uses kpartx (multipath-tools) to create mappings for devices that are exported as whole disk devices that are partitioned. (c) 01/2006 Novell Inc License: GNU GPL Author: Kurt Garloff """ import os, sys, getopt from xen.xend import sxp import tempfile # Global options quiet = False verbose = False tmpdir = '/var/lib/xen/tmp' # List of partitions # It's created by setting up the all the devices from the xen disk # config; every entry creates on Wholedisk object, which does necessary # preparatory steps such as losetup and kpartx -a; then a Partition # object is setup for every partition (which may be one or several per # Wholedisk); it references the Wholedisk if needed; python reference # counting will take care of the cleanup. partitions = [] # Helper functions def traildigits(strg): "Return the trailing digits, used to split the partition number off" idx = len(strg)-1 while strg[idx].isdigit(): if len == 0: return strg idx -= 1 return strg[idx+1:] def isWholedisk(domUname): "Determines whether dev is a wholedisk dev" return not domUname[-1:].isdigit() def freeLoopDev(): "Finds a free loop device; racy!" loops = [] fd = os.popen("losetup -a") for ln in fd.readlines(): loops.append(ln.split(':')[0]) for nr in range(0,256): if "/dev/loop%i" % nr not in loops: return "/dev/loop%i" % nr return None def findPart(dev): "Find device dev in list of partitions" if len(dev) > 5 and dev[:5] == "/dev/": dev = dev[5:] for part in partitions: if dev == part.domname: return part return None class Wholedisk: "Class representing a whole disk that has partitions" def __init__(self, domname, physdev, loopfile = None): "c'tor: set up" self.domname = domname self.physdev = physdev self.loopfile = loopfile self.mapped = 0 self.pcount = self.scanpartitions() def loopsetup(self): "Setup the loop mapping" if self.loopfile and not self.physdev: ldev = freeLoopDev() if not ldev: raise RuntimeError("domUloader: No free loop device found") if verbose: print "domUloader: losetup %s %s" % (ldev, self.loopfile) fd = os.popen("losetup %s %s" % (ldev, self.loopfile)) if fd.close(): raise RuntimeError("domUloader: Failure setting up loop dev") self.physdev = ldev def loopclean(self): "Delete the loop mapping" if self.loopfile and self.physdev: if verbose: print "domUloader: losetup -d %s" % self.physdev fd = os.popen("losetup -d %s" % self.physdev) self.physdev = None return fd.close() def scanpartitions(self): """Scan device for partitions (kpartx -l) and set up data structures, Returns number of partitions found.""" self.loopsetup() if not self.physdev: raise RuntimeError("domUloader: No physical device? %s" % self.__repr__()) # TODO: We could use fdisk -l instead and look at the type of # partitions; this way we could also detect LVM and support it. fd = os.popen("kpartx -l %s" % self.physdev) pcount = 0 for line in fd.readlines(): line = line.strip() (pname, params) = line.split(':') pno = int(traildigits(pname.strip())) #if pname.rfind('/') != -1: # pname = pname[pname.rfind('/')+1:] #pname = self.physdev[:self.physdev.rfind('/')] + '/' + pname pname = "/dev/mapper/" + pname partitions.append(Partition(self, self.domname + '%i' % pno, pname)) pcount += 1 fd.close() if not pcount: if self.loopfile: ref = self else: ref = None partitions.append(Partition(ref, self.domname, self.physdev)) self.loopclean() return pcount def activatepartitions(self): "Set up loop mapping and device-mapper mappings" if not self.mapped: self.loopsetup() if self.pcount: if verbose: print "domUloader: kpartx -a %s" % self.physdev fd = os.popen("kpartx -a %s" % self.physdev) fd.close() self.mapped += 1 def deactivatepartitions(self): "Remove device-mapper mappings and loop mapping" if not self.mapped: return self.mapped -= 1 if not self.mapped: if self.pcount: if verbose: print "domUloader: kpartx -d %s" % self.physdev fd = os.popen("kpartx -d %s" % self.physdev) fd.close() self.loopclean() def __del__(self): "d'tor: clean up" self.deactivatepartitions() self.loopclean() def __repr__(self): "string representation for debugging" strg = "[" + self.domname + "," + self.physdev + "," if self.loopfile: strg += self.loopfile strg += "," + str(self.pcount) + ",mapped %ix]" % self.mapped return strg class Partition: """Class representing a domU filesystem (partition) that can be mounted in dom0""" def __init__(self, whole = None, domname = None, physdev = None): "c'tor: setup" self.wholedisk = whole self.domname = domname self.physdev = physdev self.mountpoint = None def __del__(self): "d'tor: cleanup" if self.mountpoint: self.umount() # Not needed: Refcounting will take care of it. #if self.wholedisk: # self.wholedisk.deactivatepartitions() def __repr__(self): "string representation for debugging" strg = "[" + self.domname + "," + self.physdev + "," if self.mountpoint: strg += "mounted on " + self.mountpoint + "," else: strg += "not mounted," if self.wholedisk: return strg + self.wholedisk.__repr__() + "]" else: return strg + "]" def mount(self, fstype = None, options = "ro"): "mount filesystem, sets self.mountpoint" if self.mountpoint: return if self.wholedisk: self.wholedisk.activatepartitions() mtpt = tempfile.mkdtemp(prefix = "%s." % self.domname, dir = tmpdir) mopts = "" if fstype: mopts += " -t %s" % fstype mopts += " -o %s" % options if verbose: print "domUloader: mount %s %s %s" % (mopts, self.physdev, mtpt) fd = os.popen("mount %s %s %s" % (mopts, self.physdev, mtpt)) err = fd.close() if err: raise RuntimeError("domUloader: Error %i from mount %s %s on %s" % \ (err, mopts, self.physdev, mtpt)) self.mountpoint = mtpt def umount(self): "umount filesystem at self.mountpoint" if not self.mountpoint: return if verbose: print "domUloader: umount %s" % self.mountpoint fd = os.popen("umount %s" % self.mountpoint) err = fd.close() os.rmdir(self.mountpoint) if err: raise RuntimeError("Error %i from umount %s" % \ (err, self.mountpoint)) self.mountpoint = None if self.wholedisk: self.wholedisk.deactivatepartitions() def setupOneDisk(cfg): """Sets up one exported disk (incl. partitions if existing) @param cfg: 4-tuple (uname, dev, mode, backend)""" from xen.util.blkif import blkdev_uname_to_file (type, dev) = cfg[0].split(':') (loopfile, physdev) = (None, None) if type == "file": loopfile = dev elif type == "phy": physdev = blkdev_uname_to_file(cfg[0]) wdisk = Wholedisk(cfg[1], physdev, loopfile) def setupDisks(vbds): """Create a list of all disks from the disk config: @param vbds: The disk config as list of 4-tuples (uname, dev, mode, backend)""" disks = eval(eval(vbds)) for disk in disks: setupOneDisk(disk) if verbose: print "Partitions: " + str(partitions) class Fstab: "Class representing an fstab" class FstabEntry: "Class representing one fstab line" def __init__(self, line): "c'tor: parses one line" spline = line.split() self.dev, self.mtpt, self.fstype, self.opts = \ spline[0], spline[1], spline[2], spline[3] if len(self.mtpt) > 1: self.mtpt = self.mtpt.rstrip('/') def __init__(self, filename): "c'tor: parses fstab" self.entries = [] fd = open(filename) for line in fd.readlines(): line = line.strip() if len(line) == 0 or line[0] == '#': continue self.entries.append(Fstab.FstabEntry(line)) def find(self, fname): "Looks for matching filesystem in fstab" matchlen = 0 match = None fnmlst = fname.split('/') for fs in self.entries: entlst = fs.mtpt.split('/') # '/' needs special treatment :-( if entlst == ['','']: entlst = [''] entln = len(entlst) if len(fnmlst) >= entln and fnmlst[:entln] == entlst \ and entln > matchlen: match = fs matchlen = entln if not match: return (None, None) return (match.dev, match.mtpt) def fsFromFstab(kernel, initrd, root): """Investigate rootFS fstab, check for filesystem that contains the kernel and return it; also returns adapted kernel and initrd path. """ part = findPart(root) if not part: raise RuntimeError("domUloader: Root fs %s not exported?" % root) part.mount() if not os.access(part.mountpoint + '/etc/fstab', os.R_OK): part.umount() raise RuntimeError("domUloader: /etc/fstab not found on %s" % root) fstab = Fstab(part.mountpoint + '/etc/fstab') (dev, fs) = fstab.find(kernel) if not fs: raise RuntimeError("domUloader: no matching filesystem for image %s found in fstab" % kernel) #return (None, kernel, initrd) if fs == '/': ln = 0 # this avoids the stupid /dev/root problem dev = root else: ln = len(fs) kernel = kernel[ln:] if initrd: initrd = initrd[ln:] if verbose: print "fsFromFstab: %s %s -- %s,%s" % (dev, fs, kernel, initrd) return (kernel, initrd, dev) def parseEntry(entry): "disects bootentry and returns kernel, initrd, filesys" fs = None initrd = None fsspl = entry.split(':') if len(fsspl) > 1: fs = fsspl[0] entry = fsspl[1] enspl = entry.split(',') # Prepend '/' if missing kernel = enspl[0] if kernel[0] != '/': kernel = '/' + kernel if len(enspl) > 1: initrd = enspl[1] if initrd[0] != '/': initrd = '/' + initrd return kernel, initrd, fs def copyFile(src, dst): "Wrapper for shutil.filecopy" import shutil if verbose: print "domUloader: cp %s %s" % (src, dst) stat = os.stat(src) if stat.st_size > 16*1024*1024: raise RuntimeError("Too large file %s (%s larger than 16MB)" \ % (src, stat.st_size)) try: shutil.copyfile(src, dst) except: os.unlink(dst) raise() def copyKernelAndInitrd(fs, kernel, initrd): """Finds fs in list of partitions, mounts the partition, copies kernel [and initrd] off to dom0 files, umounts the parition again, and returns sxpr pointing to these copies.""" import shutil part = findPart(fs) if not part: raise RuntimeError("domUloader: Filesystem %s not exported\n" % fs) part.mount() try: (fd, knm) = tempfile.mkstemp(prefix = "vmlinuz.", dir = tmpdir) os.close(fd) copyFile(part.mountpoint + kernel, knm) except: os.unlink(knm) raise if not quiet: print "Copy kernel %s from %s to %s for booting" % \ (kernel, fs, knm) sxpr = "linux (kernel %s)" % knm if (initrd): try: (fd, inm) = tempfile.mkstemp(prefix = "initrd.", dir = tmpdir) os.close(fd) copyFile(part.mountpoint + initrd, inm) except: os.unlink(knm) os.unlink(inm) raise sxpr += "(ramdisk %s)" % inm part.umount() return sxpr def main(argv): "Main routine: Parses options etc." global quiet, verbose, tmpdir def usage(): "Help output (usage info)" global verbose, quiet print >> sys.stderr, "domUloader usage: domUloader --disks=disklist [--root=rootFS]\n" \ + " --entry=kernel[,initrd] [--output=fd] [--quiet] [--verbose] [--help]\n" print >> sys.stderr, __doc__ #print "domUloader " + str(argv) try: (optlist, args) = getopt.gnu_getopt(argv, 'qvh', \ ('disks=', 'root=', 'entry=', 'output=', 'tmpdir=', 'help', 'quiet', 'verbose')) except: usage() sys.exit(1) entry = None output = None root = None disks = None for (opt, oarg) in optlist: if opt in ('-h', '--help'): usage() sys.exit(0) elif opt in ('-q', '--quiet'): quiet = True elif opt in ('-v', '--verbose'): verbose = True elif opt == '--root': root = oarg elif opt == '--output': output = oarg elif opt == '--disks': disks = oarg elif opt == '--entry': entry = oarg elif opt == '--tmpdir': tmpdir = oarg if not entry or not disks: usage() sys.exit(1) if output is None or output == "-": fd = sys.stdout.fileno() else: fd = os.open(output, os.O_WRONLY) if not os.access(tmpdir, os.X_OK): os.mkdir(tmpdir) os.chmod(tmpdir, 0750) # We assume kernel and initrd are on the same FS, # so only one fs kernel, initrd, fs = parseEntry(entry) setupDisks(disks) if not fs: if not root: usage() raise RuntimeError("domUloader: No root= to parse fstab and no disk in bootentry") sys.exit(1) kernel, initrd, fs = fsFromFstab(kernel, initrd, root) sxpr = copyKernelAndInitrd(fs, kernel, initrd) sys.stdout.flush() os.write(fd, sxpr) # Call main if called (and not imported) if __name__ == "__main__": main(sys.argv)