#!/usr/bin/python
#
-# Copyright (C) 2006, 2007 Google Inc.
+# Copyright (C) 2006, 2007, 2011 Google Inc.
#
# 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
usually not get these errors, but if we do it's most probably a system
error, so they should be handled and the user instructed to report
them.
+
"""
import sys
import optparse
import time
+import errno
+import re
from ganeti.utils import RunCmd, ReadFile
from ganeti import constants
from ganeti import cli
+from ganeti import compat
USAGE = ("\tlvmstrap diskinfo\n"
- "\tlvmstrap [--vgname=NAME] [--allow-removable]"
- " { --alldisks | --disks DISKLIST }"
+ "\tlvmstrap [--vg-name=NAME] [--allow-removable]"
+ " { --alldisks | --disks DISKLIST } [--use-sfdisk]"
" create")
verbose_flag = False
+#: Supported disk types (as prefixes)
+SUPPORTED_TYPES = [
+ "hd",
+ "sd",
+ "md",
+ "ubd",
+ ]
+
+#: Excluded filesystem types
+EXCLUDED_FS = frozenset([
+ "nfs",
+ "nfs4",
+ "autofs",
+ "tmpfs",
+ "proc",
+ "sysfs",
+ "usbfs",
+ "devpts",
+ ])
+
+#: A regular expression that matches partitions (must be kept in sync
+# with L{SUPPORTED_TYPES}
+PART_RE = re.compile("^((?:h|s|m|ub)d[a-z]{1,2})[0-9]+$")
+
+#: Minimum partition size to be considered (1 GB)
+PART_MINSIZE = 1024 * 1024 * 1024
+MBR_MAX_SIZE = 2 * (10 ** 12)
+
class Error(Exception):
"""Generic exception"""
This should catch sysfs tree changes, or otherwise incorrect
assumptions about the contents of the /sys/block/... directories.
+
"""
pass
This should usually mean that the installation of the Xen node
failed in some steps.
+
"""
pass
This should usually mean that the build steps for the Xen node were
not followed correctly.
+
"""
pass
"""Exception denoting actual errors.
Errors during the bootstrapping are signaled using this exception.
+
"""
pass
Wrong disks given as parameters will be signaled using this
exception.
+
"""
pass
def Usage():
- """Shows program usage information and exits the program."""
+ """Shows program usage information and exits the program.
+ """
print >> sys.stderr, "Usage:"
print >> sys.stderr, USAGE
sys.exit(2)
In case of command line errors, it will show the usage and exit the
program.
- Returns:
- (options, args), as returned by OptionParser.parse_args
+ @rtype: tuple
+ @return: a tuple of (options, args), as returned by
+ OptionParser.parse_args
+
"""
- global verbose_flag # pylint: disable-msg=W0603
+ global verbose_flag # pylint: disable=W0603
parser = optparse.OptionParser(usage="\n%s" % USAGE,
version="%%prog (ganeti) %s" %
parser.add_option("-g", "--vg-name", type="string",
dest="vgname", default="xenvg", metavar="NAME",
help="the volume group to be created [default: xenvg]")
-
+ parser.add_option("--use-sfdisk", dest="use_sfdisk",
+ action="store_true", default=False,
+ help="use sfdisk instead of parted")
options, args = parser.parse_args()
if len(args) != 1:
return options, args
+def IsPartitioned(disk):
+ """Returns whether a given disk should be used partitioned or as-is.
+
+ Currently only md devices are used as is.
+
+ """
+ return not (disk.startswith("md") or PART_RE.match(disk))
+
+
+def DeviceName(disk):
+ """Returns the appropriate device name for a disk.
+
+ For non-partitioned devices, it returns the name as is, otherwise it
+ returns the first partition.
+
+ """
+ if IsPartitioned(disk):
+ device = "/dev/%s1" % disk
+ else:
+ device = "/dev/%s" % disk
+ return device
+
+
+def SysfsName(disk):
+ """Returns the sysfs name for a disk or partition.
+
+ """
+ match = PART_RE.match(disk)
+ if match:
+ # this is a partition, which resides in /sys/block under a different name
+ disk = "%s/%s" % (match.group(1), disk)
+ return "/sys/block/%s" % disk
+
+
def ExecCommand(command):
"""Executes a command.
difference that if the command line argument -v has been given, it
will print the command line and the command output on stdout.
- Args:
- the command line
- Returns:
- (status, output) where status is the exit status and output the
- stdout and stderr of the command together
- """
+ @param command: the command line to be executed
+ @rtype: tuple
+ @return: a tuple of (status, output) where status is the exit status
+ and output the stdout and stderr of the command together
+ """
if verbose_flag:
print command
result = RunCmd(command)
It check that it runs on Linux 2.6, and that /sys is mounted and the
fact that /sys/block is a directory.
- """
+ """
if os.getuid() != 0:
raise PrereqError("This tool runs as root only. Really.")
- osname, nodename, release, version, arch = os.uname()
- if osname != 'Linux':
+ osname, _, release, _, _ = os.uname()
+ if osname != "Linux":
raise PrereqError("This tool only runs on Linux"
" (detected OS: %s)." % osname)
- if not release.startswith("2.6."):
+ if not (release.startswith("2.6.") or release.startswith("3.")):
raise PrereqError("Wrong major kernel version (detected %s, needs"
- " 2.6.*)" % release)
+ " 2.6.* or 3.*)" % release)
if not os.path.ismount("/sys"):
raise PrereqError("Can't find a filesystem mounted at /sys."
def CheckVGExists(vgname):
"""Checks to see if a volume group exists.
- Args:
- vgname: the volume group name
+ @param vgname: the volume group name
- Returns:
- a four-tuple (exists, lv_count, vg_size, vg_free), where:
- exists: True if the volume exists, otherwise False; if False,
+ @return: a four-tuple (exists, lv_count, vg_size, vg_free), where:
+ - exists: True if the volume exists, otherwise False; if False,
all other members of the tuple are None
- lv_count: The number of logical volumes in the volume group
- vg_size: The total size of the volume group (in gibibytes)
- vg_free: The available space in the volume group
- """
+ - lv_count: The number of logical volumes in the volume group
+ - vg_size: The total size of the volume group (in gibibytes)
+ - vg_free: The available space in the volume group
+ """
result = ExecCommand("vgs --nohead -o lv_count,vg_size,vg_free"
" --nosuffix --units g"
" --ignorelockingfailure %s" % vgname)
some retries here. Since we only do a stat, we can afford to do many
short retries.
- Args:
- name: the device name, e.g. 'sda'
- devnum: the device number, e.g. 0x803 (2051 in decimal) for sda3
+ @param name: the device name, e.g. 'sda'
+ @param devnum: the device number, e.g. 0x803 (2051 in decimal) for sda3
+ @raises L{SysconfigError}: in case of failure of the check
- Returns:
- None; failure of the check is signaled by raising a
- SysconfigError exception
"""
-
path = "/dev/%s" % name
- for retries in range(40):
+ for _ in range(40):
if os.path.exists(path):
break
time.sleep(0.250)
function reads that file and converts the major:minor pair to a dev
number.
- Args:
- syspath: the path to a block device dir in sysfs, e.g. /sys/block/sda
+ @type syspath: string
+ @param syspath: the path to a block device dir in sysfs,
+ e.g. C{/sys/block/sda}
- Returns:
- the device number
- """
+ @return: the device number
+ """
if not os.path.exists("%s/dev" % syspath):
raise ProgrammingError("Invalid path passed to ReadDev: %s" % syspath)
f = open("%s/dev" % syspath)
function reads that file and converts the number in sectors to the
size in bytes.
- Args:
- syspath: the path to a block device dir in sysfs, e.g. /sys/block/sda
+ @type syspath: string
+ @param syspath: the path to a block device dir in sysfs,
+ e.g. C{/sys/block/sda}
+
+ @rtype: int
+ @return: the device size in bytes
- Returns:
- the device size in bytes
"""
if not os.path.exists("%s/size" % syspath):
This function tries to see if a block device is a physical volume.
- Args:
- dev: the device name (e.g. sda)
- Returns:
- The name of the volume group to which this PV belongs, or
- "" if this PV is not in use, or
- None if this is not a PV
- """
+ @type name: string
+ @param name: the device name (e.g. sda)
+
+ @return: the name of the volume group to which this PV belongs, or
+ "" if this PV is not in use, or None if this is not a PV
+ """
result = ExecCommand("pvdisplay -c /dev/%s" % name)
if result.failed:
return None
This function examines the /sys/block tree and using information
therein, computes the status of the block device.
- Returns:
- [(name, size, dev, partitions, inuse), ...]
- where:
- name is the block device name (e.g. sda)
- size the size in bytes
- dev the device number (e.g. 8704 for hdg)
- partitions is [(name, size, dev), ...] mirroring the disk list data
- inuse is a boolean showing the in-use status of the disk, computed as the
- possibility of re-reading the partition table (the meaning of the
- operation varies with the kernel version, but is usually accurate;
- a mounted disk/partition or swap-area or PV with active LVs on it
- is busy)
- """
+ @return: a list like [(name, size, dev, partitions, inuse), ...], where:
+ - name is the block device name (e.g. sda)
+ - size the size in bytes
+ - dev is the device number (e.g. 8704 for hdg)
+ - partitions is [(name, size, dev), ...] mirroring the disk list
+ data inuse is a boolean showing the in-use status of the disk,
+ computed as the possibility of re-reading the partition table
+ (the meaning of the operation varies with the kernel version,
+ but is usually accurate; a mounted disk/partition or swap-area
+ or PV with active LVs on it is busy)
+ """
dlist = []
for name in os.listdir("/sys/block"):
- if (not name.startswith("hd") and
- not name.startswith("sd") and
- not name.startswith("ubd")):
+ if not compat.any([name.startswith(pfx) for pfx in SUPPORTED_TYPES]):
continue
- size = ReadSize("/sys/block/%s" % name)
+ disksysfsname = "/sys/block/%s" % name
+ size = ReadSize(disksysfsname)
f = open("/sys/block/%s/removable" % name)
removable = int(f.read().strip())
if removable and not opts.removable_ok:
continue
- dev = ReadDev("/sys/block/%s" % name)
+ dev = ReadDev(disksysfsname)
CheckSysDev(name, dev)
- inuse = not CheckReread(name)
+ inuse = InUse(name)
# Enumerate partitions of the block device
partitions = []
- for partname in os.listdir("/sys/block/%s" % name):
+ for partname in os.listdir(disksysfsname):
if not partname.startswith(name):
continue
- partdev = ReadDev("/sys/block/%s/%s" % (name, partname))
- partsize = ReadSize("/sys/block/%s/%s" % (name, partname))
- CheckSysDev(partname, partdev)
- partitions.append((partname, partsize, partdev))
+ partsysfsname = "%s/%s" % (disksysfsname, partname)
+ partdev = ReadDev(partsysfsname)
+ partsize = ReadSize(partsysfsname)
+ if partsize >= PART_MINSIZE:
+ CheckSysDev(partname, partdev)
+ partinuse = InUse(partname)
+ partitions.append((partname, partsize, partdev, partinuse))
partitions.sort()
dlist.append((name, size, dev, partitions, inuse))
dlist.sort()
of the results is memorised for later matching against the
/sys/block devices.
- Returns:
- a mountpoint: device number dictionary
- """
+ @rtype: dict
+ @return: a {mountpoint: device number} dictionary
+ """
mountlines = ReadFile("/proc/mounts").splitlines()
mounts = {}
for line in mountlines:
- device, mountpoint, fstype, rest = line.split(None, 3)
+ _, mountpoint, fstype, _ = line.split(None, 3)
# fs type blacklist
- if fstype in ["nfs", "nfs4", "autofs", "tmpfs", "proc", "sysfs"]:
+ if fstype in EXCLUDED_FS:
continue
try:
dev = os.stat(mountpoint).st_dev
return mounts
+def GetSwapInfo():
+ """Reads /proc/swaps and returns the list of swap backing stores.
+
+ """
+ swaplines = ReadFile("/proc/swaps").splitlines()[1:]
+ return [line.split(None, 1)[0] for line in swaplines]
+
+
def DevInfo(name, dev, mountinfo):
"""Computes miscellaneous information about a block device.
- Args:
- name: the device name, e.g. sda
+ @type name: string
+ @param name: the device name, e.g. sda
- Returns:
- (mpath, whatvg, fileinfo), where
- mpath is the mount path where this device is mounted or None
- whatvg is the result of the ReadPV function
- fileinfo is the output of file -bs on the device
- """
+ @return: a tuple (mpath, whatvg, fileinfo), where:
+ - mpath is the mount path where this device is mounted or None
+ - whatvg is the result of the ReadPV function
+ - fileinfo is the output of file -bs on the device
+ """
if dev in mountinfo:
mpath = mountinfo[dev]
else:
choice about which disks should be allocated to our volume group.
"""
+ def _inuse(inuse):
+ if inuse:
+ return "yes"
+ else:
+ return "no"
+
mounts = GetMountInfo()
dlist = GetDiskList(opts)
flatlist = []
# Flatten the [(disk, [partition,...]), ...] list
for name, size, dev, parts, inuse in dlist:
- if inuse:
- str_inuse = "yes"
- else:
- str_inuse = "no"
- flatlist.append((name, size, dev, str_inuse))
- for partname, partsize, partdev in parts:
- flatlist.append((partname, partsize, partdev, ""))
+ flatlist.append((name, size, dev, _inuse(inuse)))
+ for partname, partsize, partdev, partinuse in parts:
+ flatlist.append((partname, partsize, partdev, _inuse(partinuse)))
strlist = []
for name, size, dev, in_use in flatlist:
print line
+def CheckSysfsHolders(name):
+ """Check to see if a device is 'hold' at sysfs level.
+
+ This is usually the case for Physical Volumes under LVM.
+
+ @rtype: boolean
+ @return: true if the device is available according to sysfs
+
+ """
+ try:
+ contents = os.listdir("%s/holders/" % SysfsName(name))
+ except OSError, err:
+ if err.errno == errno.ENOENT:
+ contents = []
+ else:
+ raise
+ return not bool(contents)
+
+
def CheckReread(name):
"""Check to see if a block device is in use.
- Uses blockdev to reread the partition table of a block device, and
- thus compute the in-use status. See the discussion in GetDiskList
- about the meaning of 'in use'.
+ Uses blockdev to reread the partition table of a block device (or
+ fuser if the device is not partitionable), and thus compute the
+ in-use status. See the discussion in GetDiskList about the meaning
+ of 'in use'.
+
+ @rtype: boolean
+ @return: the in-use status of the device
- Returns:
- boolean, the in-use status of the device
"""
+ use_blockdev = IsPartitioned(name)
+ if use_blockdev:
+ cmd = "blockdev --rereadpt /dev/%s" % name
+ else:
+ cmd = "fuser -vam /dev/%s" % name
- for retries in range(3):
- result = ExecCommand("blockdev --rereadpt /dev/%s" % name)
- if not result.failed:
+ for _ in range(3):
+ result = ExecCommand(cmd)
+ if not use_blockdev and result.failed:
+ break
+ elif use_blockdev and not result.failed:
break
time.sleep(2)
- return not result.failed
+ if use_blockdev:
+ return not result.failed
+ else:
+ return result.failed
+
+
+def CheckMounted(name):
+ """Check to see if a block device is a mountpoint.
+
+ In recent distros/kernels, this is reported directly via fuser, but
+ on older ones not, so we do an additional check here (manually).
+
+ """
+ minfo = GetMountInfo()
+ dev = ReadDev(SysfsName(name))
+ return dev not in minfo
+
+
+def CheckSwap(name):
+ """Check to see if a block device is being used as swap.
+
+ """
+ name = "/dev/%s" % name
+ return name not in GetSwapInfo()
+
+
+def InUse(name):
+ """Returns if a disk is in use or not.
+
+ """
+ return not (CheckSysfsHolders(name) and CheckReread(name) and
+ CheckMounted(name) and CheckSwap(name))
def WipeDisk(name):
partition table. If not successful, it writes back the old partition
data, and leaves the cleanup to the user.
- Args:
- the device name (e.g. sda)
+ @param name: the device name (e.g. sda)
+
"""
- if not CheckReread(name):
+ if InUse(name):
raise OperationalError("CRITICAL: disk %s you selected seems to be in"
" use. ABORTING!" % name)
" %d. I don't know how to cleanup. Sorry." %
(name, bytes_written))
- if not CheckReread(name):
+ if InUse(name):
+ # try to restore the data
fd = os.open("/dev/%s" % name, os.O_RDWR | os.O_SYNC)
os.write(fd, olddata)
os.close(fd)
name)
-def PartitionDisk(name):
+def PartitionDisk(name, use_sfdisk):
"""Partitions a disk.
This function creates a single partition spanning the entire disk,
by means of fdisk.
- Args:
- the device name, e.g. sda
+ @param name: the device name, e.g. sda
+
"""
- result = ExecCommand(
- 'echo ,,8e, | sfdisk /dev/%s' % name)
+
+ # Check that parted exists
+ result = ExecCommand("parted --help")
if result.failed:
- raise OperationalError("CRITICAL: disk %s which I have just partitioned"
- " cannot reread its partition table, or there"
- " is some other sfdisk error. Likely, it is in"
- " use. You have to clean this yourself. Error"
- " message from sfdisk: %s" %
- (name, result.output))
+ use_sfdisk = True
+ print >> sys.stderr, ("Unable to execute \"parted --help\","
+ " falling back to sfdisk.")
+
+ # Check disk size - over 2TB means we need to use GPT
+ size = ReadSize("/sys/block/%s" % name)
+ if size > MBR_MAX_SIZE:
+ label_type = "gpt"
+ if use_sfdisk:
+ raise OperationalError("Critical: Disk larger than 2TB detected, but"
+ " parted is either not installed or --use-sfdisk"
+ " has been specified")
+ else:
+ label_type = "msdos"
+
+ if use_sfdisk:
+ result = ExecCommand(
+ "echo ,,8e, | sfdisk /dev/%s" % name)
+ if result.failed:
+ raise OperationalError("CRITICAL: disk %s which I have just partitioned"
+ " cannot reread its partition table, or there"
+ " is some other sfdisk error. Likely, it is in"
+ " use. You have to clean this yourself. Error"
+ " message from sfdisk: %s" %
+ (name, result.output))
+
+ else:
+ result = ExecCommand("parted -s /dev/%s mklabel %s" % (name, label_type))
+ if result.failed:
+ raise OperationalError("Critical: failed to create %s label on %s" %
+ (label_type, name))
+ result = ExecCommand("parted -s /dev/%s mkpart pri ext2 1 100%%" % name)
+ if result.failed:
+ raise OperationalError("Critical: failed to create partition on %s" %
+ name)
+ result = ExecCommand("parted -s /dev/%s set 1 lvm on" % name)
+ if result.failed:
+ raise OperationalError("Critical: failed to set partition on %s to LVM" %
+ name)
def CreatePVOnDisk(name):
This function creates a physical volume on a block device, overriding
all warnings. So it can wipe existing PVs and PVs which are in a VG.
- Args:
- the device name, e.g. sda
+ @param name: the device name, e.g. sda
"""
- result = ExecCommand("pvcreate -yff /dev/%s1 " % name)
+ device = DeviceName(name)
+ result = ExecCommand("pvcreate -yff %s" % device)
if result.failed:
raise OperationalError("I cannot create a physical volume on"
- " partition /dev/%s1. Error message: %s."
+ " %s. Error message: %s."
" Please clean up yourself." %
- (name, result.output))
+ (device, result.output))
def CreateVG(vgname, disks):
This function creates a volume group named `vgname` on the disks
given as parameters. The physical extent size is set to 64MB.
- Args:
- disks: a list of disk names, e.g. ['sda','sdb']
+ @param disks: a list of disk names, e.g. ['sda','sdb']
"""
- pnames = ["'/dev/%s1'" % disk for disk in disks]
+ pnames = [DeviceName(d) for d in disks]
result = ExecCommand("vgcreate -s 64MB '%s' %s" % (vgname, " ".join(pnames)))
if result.failed:
raise OperationalError("I cannot create the volume group %s from"
using the --disks option) such that all given disks are present and
not in use.
- Args:
- the options returned from OptParser.parse_options
+ @param options: the options returned from OptParser.parse_options
- Returns:
- a list of disk names, e.g. ['sda', 'sdb']
- """
+ @return: a list of disk names, e.g. ['sda', 'sdb']
+ """
sysdisks = GetDiskList(options)
if not sysdisks:
raise PrereqError("no disks found (I looked for"
" non-removable block devices).")
sysd_free = []
sysd_used = []
- for name, size, dev, part, used in sysdisks:
+ for name, _, _, parts, used in sysdisks:
if used:
sysd_used.append(name)
+ for partname, _, _, partused in parts:
+ if partused:
+ sysd_used.append(partname)
+ else:
+ sysd_free.append(partname)
else:
sysd_free.append(name)
def BootStrap():
- """Actual main routine."""
+ """Actual main routine.
+ """
CheckPrereq()
options, args = ParseOptions()
" LV count: %s, size: %s, free: %s." %
(vgname, lv_count, vg_size, vg_free))
-
disklist = ValidateDiskList(options)
for disk in disklist:
WipeDisk(disk)
- PartitionDisk(disk)
+ if IsPartitioned(disk):
+ PartitionDisk(disk, options.use_sfdisk)
for disk in disklist:
CreatePVOnDisk(disk)
CreateVG(vgname, disklist)
- status, lv_count, size, free = CheckVGExists(vgname)
+ status, lv_count, size, _ = CheckVGExists(vgname)
if status:
print "Done! %s: size %s GiB, disks: %s" % (vgname, size,
",".join(disklist))
def main():
- """application entry point.
+ """Application entry point.
This is just a wrapper over BootStrap, to handle our own exceptions.
- """
+ """
try:
BootStrap()
except PrereqError, err:
print >> sys.stderr, str(err)
sys.exit(1)
except ProgrammingError, err:
- print >> sys.stderr, ("Internal application error. Please signal this"
- " to xencluster-team.")
+ print >> sys.stderr, ("Internal application error. Please report this"
+ " to the Ganeti developer list.")
print >> sys.stderr, "Error description: %s" % str(err)
sys.exit(1)
except Error, err: