Rename OpDiagnoseOS and LUDiagnoseOS
[ganeti-local] / tools / lvmstrap
index a10296e..654548d 100755 (executable)
@@ -1,7 +1,7 @@
 #!/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
@@ -36,6 +36,7 @@ OSError. The idea behind this is, since we run as root, we should
 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.
+
 """
 
 
@@ -43,10 +44,12 @@ import os
 import sys
 import optparse
 import time
+import errno
 
-from ganeti.utils import RunCmd
+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]"
@@ -55,6 +58,14 @@ USAGE = ("\tlvmstrap diskinfo\n"
 
 verbose_flag = False
 
+#: Supported disk types (as prefixes)
+SUPPORTED_TYPES = [
+  "hd",
+  "sd",
+  "md",
+  "ubd",
+  ]
+
 
 class Error(Exception):
   """Generic exception"""
@@ -66,6 +77,7 @@ class ProgrammingError(Error):
 
   This should catch sysfs tree changes, or otherwise incorrect
   assumptions about the contents of the /sys/block/... directories.
+
   """
   pass
 
@@ -79,6 +91,7 @@ class SysconfigError(Error):
 
   This should usually mean that the installation of the Xen node
   failed in some steps.
+
   """
   pass
 
@@ -92,6 +105,7 @@ class PrereqError(Error):
 
   This should usually mean that the build steps for the Xen node were
   not followed correctly.
+
   """
   pass
 
@@ -100,6 +114,7 @@ class OperationalError(Error):
   """Exception denoting actual errors.
 
   Errors during the bootstrapping are signaled using this exception.
+
   """
   pass
 
@@ -109,13 +124,15 @@ class ParameterError(Error):
 
   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)
@@ -127,10 +144,12 @@ def ParseOptions():
   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
+  global verbose_flag # pylint: disable-msg=W0603
 
   parser = optparse.OptionParser(usage="\n%s" % USAGE,
                                  version="%%prog (ganeti) %s" %
@@ -142,9 +161,7 @@ def ParseOptions():
   parser.add_option("-d", "--disks", dest="disks",
                     help="Choose disks (e.g. hda,hdg)",
                     metavar="DISKLIST")
-  parser.add_option("-v", "--verbose",
-                    action="store_true", dest="verbose", default=False,
-                    help="print command execution messages to stdout")
+  parser.add_option(cli.VERBOSE_OPT)
   parser.add_option("-r", "--allow-removable",
                     action="store_true", dest="removable_ok", default=False,
                     help="allow and use removable devices too")
@@ -162,6 +179,29 @@ def ParseOptions():
   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')
+
+
+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 ExecCommand(command):
   """Executes a command.
 
@@ -169,13 +209,12 @@ def ExecCommand(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)
@@ -189,12 +228,12 @@ def CheckPrereq():
 
   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()
+  osname, _, release, _, _ = os.uname()
   if osname != 'Linux':
     raise PrereqError("This tool only runs on Linux"
                       " (detected OS: %s)." % osname)
@@ -222,18 +261,16 @@ def CheckPrereq():
 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)
@@ -263,17 +300,13 @@ def CheckSysDev(name, devnum):
   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)
@@ -295,13 +328,13 @@ def ReadDev(syspath):
   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)
@@ -322,11 +355,13 @@ def ReadSize(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):
@@ -343,14 +378,13 @@ def ReadPV(name):
 
   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
@@ -364,25 +398,21 @@ def GetDiskList(opts):
   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)
@@ -396,7 +426,7 @@ def GetDiskList(opts):
 
     dev = ReadDev("/sys/block/%s" % name)
     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):
@@ -421,16 +451,14 @@ def GetMountInfo():
   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
 
-  f = open("/proc/mounts", "r")
-  mountlines = f.readlines()
-  f.close()
+  """
+  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"]:
       continue
@@ -452,16 +480,15 @@ def GetMountInfo():
 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:
@@ -535,24 +562,62 @@ def ShowDiskInfo(opts):
     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("/sys/block/%s/holders/" % 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 not result.failed:
       break
     time.sleep(2)
 
-  return not result.failed
+  if use_blockdev:
+    return not result.failed
+  else:
+    return result.failed
+
+
+def InUse(name):
+  """Returns if a disk is in use or not.
+
+  """
+  return not (CheckSysfsHolders(name) and CheckReread(name))
 
 
 def WipeDisk(name):
@@ -562,11 +627,11 @@ 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)
 
@@ -586,7 +651,8 @@ def WipeDisk(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)
@@ -604,8 +670,8 @@ def PartitionDisk(name):
   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)
@@ -624,16 +690,16 @@ 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):
@@ -642,11 +708,10 @@ 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"
@@ -663,20 +728,18 @@ def ValidateDiskList(options):
   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, _, _, _, used in sysdisks:
     if used:
       sysd_used.append(name)
     else:
@@ -701,8 +764,9 @@ def ValidateDiskList(options):
 
 
 def BootStrap():
-  """Actual main routine."""
+  """Actual main routine.
 
+  """
   CheckPrereq()
 
   options, args = ParseOptions()
@@ -725,12 +789,13 @@ def BootStrap():
 
   for disk in disklist:
     WipeDisk(disk)
-    PartitionDisk(disk)
+    if IsPartitioned(disk):
+      PartitionDisk(disk)
   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))
@@ -740,11 +805,11 @@ def BootStrap():
 
 
 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: