#!/usr/bin/python # Copyright (c) 2009 - ALBAR (Toulouse, FRANCE). # mailto:barthe@albar.fr # # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; either version 2 of the License, or (at your option) any later # version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details # # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # Use epydoc (http://epydoc.sourceforge.net) to produce the documentation. """ NAME ==== vbdmount.py - Mounts or unmounts virtual block devices SYNOPSIS ======== Syntax:: vbdmount.py [-v] [-o mount-opts] { -f config-file | -b block-device }[:path-to-mount] mount-point vbdmount.py [-v] -u mount-point path-to-mount only the partition that contains this path is mounted mount-point path onto the file systems are mounted Options:: -h, --help show this help message and exit -o MOUNTOPTS, --mount-options=MOUNTOPTS Options to be passed to the mount command -f CONFIGFILE, --domu-config-file=CONFIGFILE A domU configuration file -b VBD, --block-device=VBD A virtual block device path (file or LV or raw device) -u, --umount Unmounts the mounted virtual block device -v, --verbose Prints a verbose output USAGE ===== This script mounts file systems found in a virtual block device. Once mounted, the file systems are rooted at the I{mount-point}. Then you may chroot in this directory to do useful things such as installing packages or configuring the network. This script must not be used for virtual partitions as they are directly mountable. DESCRIPTION =========== This script mounts one or more virtual block devices, expressed - directly as a B{file} path (raw disk image for file backed VBDs), a B{logical volume} path or a B{physical device} path, - as a B{Xen} domU configuration file, supposed to contain a valid python code which defines the variable I{disk} as a list. This path may be followed by a path to be mounted separated by a ':'. The following actions are performed: 1. the script examines the partition table of the VBD(s), partitions that are not reported to be of type 'Linux' or 'Linux LVM' by C{fdisk} are skipped. 2. it runs C{losetup} if the device is a file, then C{kpartx} for the partitions to be accessible 3. it looks for a C{/etc/fstab} file partition by partition, and raises an error if none is found. 4. finally it mounts file systems according to the I{fstab}. The unmount variant, after having unmounted the file systems, tries to deactive logical volumes and to remove associated virtual partitions with C{kpartx -d} and eventually loop devices with C{losetup -d}. CAUTIONS ======== B{WARNING}: this script does not check if the VBD is currently used by a running virtual machine. B{MOUNTING A VIRTUAL BLOCK DEVICE WHILE IT IS USED BY A VIRTUAL MACHINE WILL IRREMEDIABLY DAMAGE IT.} The file systems are mounted systematically read-only, unless the option C{rw} is present in the I{mount-options}. This script must be executed as C{root}. It runs the UNIX commands: C{fdisk, kpartx, losetup, lvscan, mount, umount, e2label}. Those commands are expected to be in the user's C{PATH}. EXAMPLES ======== Mounts read-write and verbosely the file systems of the domU I{devcentos5} on C{/tmp/mnt}:: # python vbdmount.py -v -o rw -f /etc/xen/devcentos5 /tmp/mnt Getting disks from config file /etc/xen/devcentos5 Getting partition table... fdisk -l /dev/vgdevcentos5/lvdevcentos5-a fdisk -l /dev/vgdevcentos5/lvdevcentos5-b Setting partitions... kpartx -a -p "p" /dev/vgdevcentos5/lvdevcentos5-a kpartx -a -p "p" /dev/vgdevcentos5/lvdevcentos5-b Looking for /etc/fstab mount -o ro /dev/mapper/lvdevcentos5-ap2 /tmp/tmptNx2zj Found it in /dev/mapper/lvdevcentos5-ap2 Getting labels... e2label /dev/mapper/lvdevcentos5-ap2 e2label /dev/mapper/lvdevcentos5-bp2 e2label /dev/mapper/lvdevcentos5-ap1 e2label /dev/mapper/lvdevcentos5-bp3 mount -o rw /dev/mapper/lvdevcentos5-ap2 /tmp/mnt/ mount -o rw /dev/mapper/lvdevcentos5-bp3 /tmp/mnt/var mount -o rw /dev/mapper/lvdevcentos5-bp2 /tmp/mnt/tmp mount -o rw /dev/mapper/lvdevcentos5-ap1 /tmp/mnt/boot 4 file systems mounted. Unmount it:: # python vbdmount.py -uv /tmp/mnt umount /tmp/mnt/boot umount /tmp/mnt/tmp umount /tmp/mnt/var umount /tmp/mnt Getting logical volumes with 'lvscan'... kpartx -d -p "p" /dev/vgdevcentos5/lvdevcentos5-a kpartx -d -p "p" /dev/vgdevcentos5/lvdevcentos5-b 4 file systems unmounted. Mounts a file backed VBD, only the C{/boot} partition:: # python vbdmount.py -b /mnt/vdisks/filebackedvbd.img:/boot /tmp/mnt 1 file systems mounted. USING THIS SCRIPT AS A PYTHON MODULE ==================================== You may import this script as a python module and use the procedures L{vbdmount}, L{vbdumount} and L{get_diskspecs_from_config}. >>> import vbdmount >>> disks = vbdmount.get_diskspecs_from_config('/etc/xen/devcentos5') >>> disks ['phy:/dev/vgdevcentos5/lvdevcentos5-a,xvda,w', 'phy:/dev/vgdevcentos5/lvdevcentos5-b,xvdb,w'] >>> vbdmount.vbdmount(disks, '/tmp/mnt') 4 >>> vbdmount.vbdumount('/tmp/mnt') 4 >>> vbdmount.vbdmount(['/mnt/vdisks/filebackedvbd.img'], '/tmp/mnt', path2mount='/boot', mountopts='rw') 1 >>> vbdmount.vbdumount('/tmp/mnt') 1 @version: 1.0 @status: stable @author: A. Barthe @copyright: 2009 - ALBAR (Toulouse, FRANCE) @contact: mailto:barthe@albar.fr """ import os, sys, re from tempfile import mkdtemp from optparse import OptionParser #: Partition separator for use with C{kpartx} PART_SEPARATOR = 'XX' #: Verbose flag VERBOSE = False #----------------------------------------------------------------------- def _get_dev_from_diskspec(diskspec): "Interprets the disk description expressed with the xen syntax" if diskspec.count(',') == 2: frontdev, backdev, perm = diskspec.split(',') if frontdev.count(':') > 0: dev = frontdev.split(':')[-1] mode = frontdev.startswith('phy') and 'phy' or 'file' else: raise ValueError("Invalide VBD spec: " + dev) if not backdev.startswith('/dev/'): backdev = '/dev/' + backdev else: dev, backdev, mode = diskspec, None, None return (dev, backdev, mode) #----------------------------------------------------------------------- def _get_lvs(): "Returns the list of LVs found with C{lvscan}" _verbose("Getting logical volumes with 'lvscan'...") return [ x.split()[1][1:-1] for x in os.popen('lvscan').readlines() ] #----------------------------------------------------------------------- def _losetup(path): "Set up a loop device for the file I{path}" device = _loseek(path) if device is not None: return device _verbose("Setting loop device...") _verbose('losetup -f ' + path) status = os.system('losetup -f ' + path) if status != 0: raise OSError("The command 'losetup -f %s' failed" % path) device = _loseek(path) if device is None: raise OSError("losetup failed with %s" % path) return device #----------------------------------------------------------------------- def _loseek(path): "Returns the loop device associated to the file I{path}" for line in os.popen("losetup -a"): m = re.match('([^:]+): .*%s' % path, line) if m is not None: return m.group(1) return None #----------------------------------------------------------------------- def _get_partition_table(devices): "Returns the consolidated partition table (list of dicts) for all I{devices}." result = [] _verbose("Getting partition table...") for device, backdev in devices: _verbose("fdisk -l %s" % device) fdisk = "LANG=en fdisk -l %s 2>/dev/null" % device if re.match('.*\d$', device): # ends with a number part_prefix = device + 'p' else: part_prefix = device re_pattern = '%s(\d+)\s+\*?\s*\d+\s+\d+\s+[\d\+]+\s+[\da-f]{2}\s+(\w+.*)$' % part_prefix for line in os.popen(fdisk): m = re.match(re_pattern, line) if m is not None and m.group(2) in ('Linux', 'Linux LVM'): part = {} part['device'] = device part['partnumber'] = m.group(1) part['kpxpath'] = '/dev/mapper/' + os.path.basename(device) + PART_SEPARATOR + part['partnumber'] part['type'] = m.group(2) if part['type'] == 'Linux LVM': _verbose("Found LVM partition.") part['toBmounted'] = None else: part['toBmounted'] = part['kpxpath'] if backdev is not None: part['backdev'] = backdev + part['partnumber'] else: part['backdev'] = None result.append(part) #print result return result #----------------------------------------------------------------------- def _kpartx_add(part_table): "Adds partitions for I{devices}, check them regarding I{part_table}." _verbose("Setting partitions...") done = [] for part in part_table: if part['device'] not in done: _verbose('kpartx -a -p "%s" %s' % (PART_SEPARATOR, part['device'])) os.system('kpartx -a -p "%s" %s' % (PART_SEPARATOR, part['device'])) if not os.path.exists(part['kpxpath']): raise OSError("kpartx failed: no such device: " + part['device']) done.append(part['device']) #----------------------------------------------------------------------- def _look4fstab(part_table): "Returns the content of the first fstab found in partitions of I{part_table}." tmpdir = mkdtemp() fstab_content = None fstab_path = os.path.join(tmpdir, 'etc', 'fstab') _verbose("Looking for /etc/fstab") for part in part_table: _verbose('mount -o ro %s %s' % (part['toBmounted'], tmpdir)) os.system('mount -o ro %s %s' % (part['toBmounted'], tmpdir)) if os.path.isfile(fstab_path): fstab_content = open(fstab_path).readlines() os.system('umount ' + tmpdir) _verbose("Found it in " + part['toBmounted']) break os.system('umount ' + tmpdir) os.rmdir(tmpdir) return fstab_content #----------------------------------------------------------------------- def _get_labels(part_table): "Returns a dict label:path for each partition of I{part_table}." labels = {} _verbose("Getting labels...") for part in part_table: _verbose('e2label ' + part['kpxpath']) labels[ os.popen('e2label %s 2>/dev/null' % part['kpxpath']).readline().rstrip() ] = part['kpxpath'] return labels #----------------------------------------------------------------------- def _get_mnt_table(fstab, part_table, mountpoint, path2mount=None): "Returns the mount table as a list of tuples (device, mount-point)." mnt_table = [] labels = None for line in fstab: device, mounton = line.split()[0:2] if not mounton.startswith('/'): continue mountto = mountpoint + mounton if device.startswith('LABEL='): if labels is None: labels = _get_labels(part_table) label = device.replace('LABEL=', '') try: device = labels[label] except: continue elif device.startswith('/dev/'): for part in part_table: if part['toBmounted'] == device: break if part['backdev'] == device or device.endswith(part['partnumber']): device = part['toBmounted'] break else: raise OSError("Partition not found: %s" % device) else: continue if path2mount is not None: if path2mount.startswith(mounton): mnt_table = [(device, mountto), ] else: mnt_table.append((device, mountto),) return mnt_table #----------------------------------------------------------------------- def _unset_devices(device): "Removes partitions and loop device for I{device}" if not os.path.exists(device): return _verbose('kpartx -d -p "%s" %s' % (PART_SEPARATOR, device)) os.system('kpartx -d -p "%s" %s' % (PART_SEPARATOR, device)) if device.startswith('/dev/loop'): _verbose('losetup -d ' + device) os.system('losetup -d ' + device) #----------------------------------------------------------------------- def _setup_lvs(part_table): "Adds the field B{toBmounted} to part_table for LVM." _verbose("Setting up LVM...") pvs = [ (x.split()[0], x.split()[1]) for x in os.popen('pvs').readlines() ] lvs = _get_lvs() path2Bmounted = {} for pv, vg in pvs: if pv in [ x['kpxpath'] for x in part_table ]: for lv in lvs: if os.path.basename(os.path.dirname(lv)) == vg: path2Bmounted[pv] = lv _verbose('lvchange -ay ' + lv) os.system('lvchange -ay ' + lv) break for part in part_table: if part['kpxpath'] in path2Bmounted.keys(): part['toBmounted'] = path2Bmounted[part['kpxpath']] return part_table #----------------------------------------------------------------------- def _do_mount(mnt_table, mountopts): "Performs the actual mount following I{mnt_table} and using I{mountopts}." for device, mountto in mnt_table: mount_cmd = 'mount -o %s %s %s' % (mountopts, device, mountto) if not os.path.exists(mountto): os.makedirs(mountto) _verbose(mount_cmd) os.system(mount_cmd) ######################################################################## # PUBLIC FUNCTIONS ######################################################################## def get_diskspecs_from_config(conffile): """Returns the variable I{disk} found in I{conffile}. Does not trap exceptions (file not found, python syntax error or I{disk} variable not found). @param conffile: a xen domU configuration file @return: a list of disk specification (xen config syntax) """ class container: execfile(conffile) return container.disk ######################################################################## def vbdmount(vbds, mountpoint, path2mount=None, mountopts='ro'): """Mounts virtual block devices I{vbds}. @param vbds: list of virtual block devices, either as a regular path or as the xen configuration variable B{disk}. @param mountpoint: the directory to mount on. Must exist. @param path2mount: the path onto the file systems are mounted @param mountopts: mount options, C{ro} by default. @return: the number of mounted file systems @raise OSError: when something goes wrong in executed commands. @warning: may destroy file systems if they are used elsewhere. """ if not os.path.isdir(mountpoint): raise OSError("No such directory: %s " % mountpoint) devices = [] for vbd in vbds: dev, backdev, mode = _get_dev_from_diskspec(vbd) if os.path.isfile(dev): devices.append((_losetup(dev), backdev)) elif os.path.exists(dev): devices.append((dev, backdev)) else: raise OSError("No such virtual block device: %s" % dev) part_table = _get_partition_table(devices) if len(part_table) == 0: raise OSError("No partition found.") _kpartx_add(part_table) if 'Linux LVM' in [ x['type'] for x in part_table ]: part_table = _setup_lvs(part_table) fstab = _look4fstab(part_table) if fstab is None: for device in devices: _unset_devices(device[0]) raise OSError("Cannot find any fstab.") mnt_table = _get_mnt_table(fstab, part_table, mountpoint, path2mount) _do_mount(mnt_table, mountopts) return len(mnt_table) ######################################################################## def vbdumount(mountpoint): """Unmounts virtual block devices mounted at I{mountpoint}. @param mountpoint: the directory to unmount. @return: the number of unmounted file systems @raise OSError: when something goes wrong in executed commands. """ umounts = [] devices = [] for line in os.popen('mount'): m = re.match('([^\s]+) on (%s.*) type' % mountpoint, line) if m is not None: mm = re.match('([^\s]+)%s\d+$' % PART_SEPARATOR, m.group(1)) if mm is not None: device = mm.group(1) else: # LVM inside device = m.group(1) if device not in devices: devices.append(device) umounts.append(m.group(2)) if len(umounts) == 0: return 0 for d in reversed(umounts): _verbose('umount ' + d) os.system('umount ' + d) lvs = None for device in devices: if not device.startswith('/dev/mapper/'): raise OSError("Not a mapped device: " + device) guess = device.replace('/mapper', '') if os.path.exists(guess): # loops or raw device device = guess else: # guess LV if lvs is None: lvs = _get_lvs() try: device = [ lv for lv in lvs if device.endswith(os.path.basename(lv)) ][0] _verbose('lvchange -an ' + device) os.system('lvchange -an ' + device) except IndexError: continue _unset_devices(device) return len(umounts) def _verbose(msg): global VERBOSE if VERBOSE: print msg ######################################################################## #: Usage for I{optparse} USAGE = """Usages: %prog [-v] [-o mount-opts] { -f config-file | -b block-device }[:path-to-mount] mount-point' %prog [-v] -u mount-point """ #: Description for I{optparse} DESCRIPTION = """Mounts one or more virtual block devices, expressed either as a string (flag -b) or as a domU configuration file (flag -f).""" ######################################################################## def _main(arguments=sys.argv[1:]): "The main procedure called when I run as a script." global VERBOSE parser = OptionParser(USAGE, description=DESCRIPTION) parser.add_option("-o", "--mount-options", dest="mountopts", help="Options to be passed to the mount command") parser.add_option("-f", "--domu-config-file", dest="configfile", help="A domU configuration file") parser.add_option("-b", "--block-device", dest="vbd", help="A virtual block device path (file or LV or raw device)") parser.add_option("-u", "--umount", dest="umount", action="store_true", default=False, help="Unmounts the mounted virtual block device") parser.add_option("-v", "--verbose", dest="verbose", action="store_true", default=False, help="Prints a verbose output") (opts, args) = parser.parse_args(arguments) if opts.configfile and opts.vbd: parser.error("Flags 'b' and 'f' are mutually exclusive.") if not (opts.configfile or opts.vbd) and not opts.umount: parser.error("One of the flags 'b' and 'f' is mandatory.") if opts.umount and (opts.configfile or opts.vbd or opts.mountopts): parser.error("Incorrect use of 'u' flag") if len(args) != 1: parser.error("The argument 'mount-point' is mandatory") mountpoint = args[0] VERBOSE = opts.verbose if opts.umount: mntnb = vbdumount(mountpoint) print("%d file systems unmounted." % mntnb) sys.exit(0) if opts.mountopts: mountopts = opts.mountopts.count('rw') == 0 and opts.mountopts + ',ro' or opts.mountopts else: mountopts = 'ro' target = opts.configfile or opts.vbd if target.count(':/') != 0: target, path2mount = target.split(':') else: path2mount = None if opts.configfile: _verbose("Getting disks from config file " + opts.configfile) vbds = get_diskspecs_from_config(target) elif opts.vbd: vbds = [target,] else: # should never occur parser.error("Strange error occured...") mntnb = vbdmount(vbds, mountpoint, path2mount, mountopts) print("%d file systems mounted." % mntnb) ######################################################################## if __name__ == '__main__': try: _main() except SystemExit: pass except Exception, errobj: print >> sys.stderr, "Error: " + str(errobj) sys.exit(1)