Change JobStorage to work with ids not filenames
[ganeti-local] / lib / bdev.py
index d73d63f..7ba3470 100644 (file)
@@ -26,9 +26,9 @@ import time
 import errno
 import pyparsing as pyp
 import os
+import logging
 
 from ganeti import utils
-from ganeti import logger
 from ganeti import errors
 from ganeti import constants
 
@@ -82,6 +82,7 @@ class BlockDev(object):
     self.unique_id = unique_id
     self.major = None
     self.minor = None
+    self.attached = False
 
   def Assemble(self):
     """Assemble the device from its components.
@@ -255,6 +256,16 @@ class BlockDev(object):
     for child in self._children:
       child.SetInfo(text)
 
+  def Grow(self, amount):
+    """Grow the block device.
+
+    Arguments:
+      amount: the amount (in mebibytes) to grow with
+
+    Returns: None
+
+    """
+    raise NotImplementedError
 
   def __repr__(self):
     return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
@@ -277,6 +288,8 @@ class LogicalVolume(BlockDev):
       raise ValueError("Invalid configuration data %s" % str(unique_id))
     self._vg_name, self._lv_name = unique_id
     self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
+    self._degraded = True
+    self.major = self.minor = None
     self.Attach()
 
   @classmethod
@@ -325,14 +338,14 @@ class LogicalVolume(BlockDev):
                "--separator=:"]
     result = utils.RunCmd(command)
     if result.failed:
-      logger.Error("Can't get the PV information: %s - %s" %
-                   (result.fail_reason, result.output))
+      logging.error("Can't get the PV information: %s - %s",
+                    result.fail_reason, result.output)
       return None
     data = []
     for line in result.stdout.splitlines():
       fields = line.strip().split(':')
       if len(fields) != 4:
-        logger.Error("Can't parse pvs output: line '%s'" % line)
+        logging.error("Can't parse pvs output: line '%s'", line)
         return None
       # skip over pvs from another vg or ones which are not allocatable
       if fields[1] != vg_name or fields[3][0] != 'a':
@@ -351,8 +364,8 @@ class LogicalVolume(BlockDev):
     result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
                            (self._vg_name, self._lv_name)])
     if result.failed:
-      logger.Error("Can't lvremove: %s - %s" %
-                   (result.fail_reason, result.output))
+      logging.error("Can't lvremove: %s - %s",
+                    result.fail_reason, result.output)
 
     return not result.failed
 
@@ -382,19 +395,37 @@ class LogicalVolume(BlockDev):
     recorded.
 
     """
-    result = utils.RunCmd(["lvdisplay", self.dev_path])
+    self.attached = False
+    result = utils.RunCmd(["lvs", "--noheadings", "--separator=,",
+                           "-olv_attr,lv_kernel_major,lv_kernel_minor",
+                           self.dev_path])
     if result.failed:
-      logger.Error("Can't find LV %s: %s, %s" %
-                   (self.dev_path, result.fail_reason, result.output))
+      logging.error("Can't find LV %s: %s, %s",
+                    self.dev_path, result.fail_reason, result.output)
       return False
-    match = re.compile("^ *Block device *([0-9]+):([0-9]+).*$")
-    for line in result.stdout.splitlines():
-      match_result = match.match(line)
-      if match_result:
-        self.major = int(match_result.group(1))
-        self.minor = int(match_result.group(2))
-        return True
-    return False
+    out = result.stdout.strip().rstrip(',')
+    out = out.split(",")
+    if len(out) != 3:
+      logging.error("Can't parse LVS output, len(%s) != 3", str(out))
+      return False
+
+    status, major, minor = out[:3]
+    if len(status) != 6:
+      logging.error("lvs lv_attr is not 6 characters (%s)", status)
+      return False
+
+    try:
+      major = int(major)
+      minor = int(minor)
+    except ValueError, err:
+      logging.error("lvs major/minor cannot be parsed: %s", str(err))
+
+    self.major = major
+    self.minor = minor
+    self._degraded = status[0] == 'v' # virtual volume, i.e. doesn't backing
+                                      # storage
+    self.attached = True
+    return True
 
   def Assemble(self):
     """Assemble the device.
@@ -406,8 +437,9 @@ class LogicalVolume(BlockDev):
     """
     result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
     if result.failed:
-      logger.Error("Can't activate lv %s: %s" % (self.dev_path, result.output))
-    return not result.failed
+      logging.error("Can't activate lv %s: %s", self.dev_path, result.output)
+      return False
+    return self.Attach()
 
   def Shutdown(self):
     """Shutdown the device.
@@ -438,20 +470,10 @@ class LogicalVolume(BlockDev):
     physical disk failure and subsequent 'vgreduce --removemissing' on
     the volume group.
 
+    The status was already read in Attach, so we just return it.
+
     """
-    result = utils.RunCmd(["lvs", "--noheadings", "-olv_attr", self.dev_path])
-    if result.failed:
-      logger.Error("Can't display lv: %s - %s" %
-                   (result.fail_reason, result.output))
-      return None, None, True, True
-    out = result.stdout.strip()
-    # format: type/permissions/alloc/fixed_minor/state/open
-    if len(out) != 6:
-      logger.Debug("Error in lvs output: attrs=%s, len != 6" % out)
-      return None, None, True, True
-    ldisk = out[0] == 'v' # virtual volume, i.e. doesn't have
-                          # backing storage
-    return None, None, ldisk, ldisk
+    return None, None, self._degraded, self._degraded
 
   def Open(self, force=False):
     """Make the device ready for I/O.
@@ -518,6 +540,71 @@ class LogicalVolume(BlockDev):
       raise errors.BlockDeviceError("Command: %s error: %s - %s" %
                                     (result.cmd, result.fail_reason,
                                      result.output))
+  def Grow(self, amount):
+    """Grow the logical volume.
+
+    """
+    # we try multiple algorithms since the 'best' ones might not have
+    # space available in the right place, but later ones might (since
+    # they have less constraints); also note that only recent LVM
+    # supports 'cling'
+    for alloc_policy in "contiguous", "cling", "normal":
+      result = utils.RunCmd(["lvextend", "--alloc", alloc_policy,
+                             "-L", "+%dm" % amount, self.dev_path])
+      if not result.failed:
+        return
+    raise errors.BlockDeviceError("Can't grow LV %s: %s" %
+                                  (self.dev_path, result.output))
+
+
+class DRBD8Status(object):
+  """A DRBD status representation class.
+
+  Note that this doesn't support unconfigured devices (cs:Unconfigured).
+
+  """
+  LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+st:([^/]+)/(\S+)"
+                       "\s+ds:([^/]+)/(\S+)\s+.*$")
+  SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
+                       "\sfinish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
+
+  def __init__(self, procline):
+    m = self.LINE_RE.match(procline)
+    if not m:
+      raise errors.BlockDeviceError("Can't parse input data '%s'" % procline)
+    self.cstatus = m.group(1)
+    self.lrole = m.group(2)
+    self.rrole = m.group(3)
+    self.ldisk = m.group(4)
+    self.rdisk = m.group(5)
+
+    self.is_standalone = self.cstatus == "StandAlone"
+    self.is_wfconn = self.cstatus == "WFConnection"
+    self.is_connected = self.cstatus == "Connected"
+    self.is_primary = self.lrole == "Primary"
+    self.is_secondary = self.lrole == "Secondary"
+    self.peer_primary = self.rrole == "Primary"
+    self.peer_secondary = self.rrole == "Secondary"
+    self.both_primary = self.is_primary and self.peer_primary
+    self.both_secondary = self.is_secondary and self.peer_secondary
+
+    self.is_diskless = self.ldisk == "Diskless"
+    self.is_disk_uptodate = self.ldisk == "UpToDate"
+
+    m = self.SYNC_RE.match(procline)
+    if m:
+      self.sync_percent = float(m.group(1))
+      hours = int(m.group(2))
+      minutes = int(m.group(3))
+      seconds = int(m.group(4))
+      self.est_time = hours * 3600 + minutes * 60 + seconds
+    else:
+      self.sync_percent = None
+      self.est_time = None
+
+    self.is_sync_target = self.peer_sync_source = self.cstatus == "SyncTarget"
+    self.peer_sync_target = self.is_sync_source = self.cstatus == "SyncSource"
+    self.is_resync = self.is_sync_target or self.is_sync_source
 
 
 class BaseDRBD(BlockDev):
@@ -535,18 +622,20 @@ class BaseDRBD(BlockDev):
   _ST_WFCONNECTION = "WFConnection"
   _ST_CONNECTED = "Connected"
 
+  _STATUS_FILE = "/proc/drbd"
+
   @staticmethod
-  def _GetProcData():
+  def _GetProcData(filename=_STATUS_FILE):
     """Return data from /proc/drbd.
 
     """
-    stat = open("/proc/drbd", "r")
+    stat = open(filename, "r")
     try:
       data = stat.read().splitlines()
     finally:
       stat.close()
     if not data:
-      raise errors.BlockDeviceError("Can't read any data from /proc/drbd")
+      raise errors.BlockDeviceError("Can't read any data from %s" % filename)
     return data
 
   @staticmethod
@@ -643,9 +732,11 @@ class BaseDRBD(BlockDev):
     """
     if minor is None:
       self.minor = self.dev_path = None
+      self.attached = False
     else:
       self.minor = minor
       self.dev_path = self._DevPath(minor)
+      self.attached = True
 
   @staticmethod
   def _CheckMetaSize(meta_device):
@@ -657,20 +748,20 @@ class BaseDRBD(BlockDev):
     """
     result = utils.RunCmd(["blockdev", "--getsize", meta_device])
     if result.failed:
-      logger.Error("Failed to get device size: %s - %s" %
-                   (result.fail_reason, result.output))
+      logging.error("Failed to get device size: %s - %s",
+                    result.fail_reason, result.output)
       return False
     try:
       sectors = int(result.stdout)
     except ValueError:
-      logger.Error("Invalid output from blockdev: '%s'" % result.stdout)
+      logging.error("Invalid output from blockdev: '%s'", result.stdout)
       return False
     bytes = sectors * 512
     if bytes < 128 * 1024 * 1024: # less than 128MiB
-      logger.Error("Meta device too small (%.2fMib)" % (bytes / 1024 / 1024))
+      logging.error("Meta device too small (%.2fMib)", (bytes / 1024 / 1024))
       return False
     if bytes > (128 + 32) * 1024 * 1024: # account for an extra (big) PE on LVM
-      logger.Error("Meta device too big (%.2fMiB)" % (bytes / 1024 / 1024))
+      logging.error("Meta device too big (%.2fMiB)", (bytes / 1024 / 1024))
       return False
     return True
 
@@ -755,7 +846,7 @@ class DRBD8(BaseDRBD):
     if highest is None: # there are no minors in use at all
       return 0
     if highest >= cls._MAX_MINORS:
-      logger.Error("Error: no free drbd minors!")
+      logging.error("Error: no free drbd minors!")
       raise errors.BlockDeviceError("Can't find a free DRBD minor")
     return highest + 1
 
@@ -770,7 +861,7 @@ class DRBD8(BaseDRBD):
                            "v08", meta_device, "0",
                            "dstate"])
     if result.failed:
-      logger.Error("Invalid meta device %s: %s" % (meta_device, result.output))
+      logging.error("Invalid meta device %s: %s", meta_device, result.output)
       return False
     return True
 
@@ -831,8 +922,8 @@ class DRBD8(BaseDRBD):
     """
     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
     if result.failed:
-      logger.Error("Can't display the drbd config: %s - %s" %
-                   (result.fail_reason, result.output))
+      logging.error("Can't display the drbd config: %s - %s",
+                    result.fail_reason, result.output)
       return None
     return result.stdout
 
@@ -945,7 +1036,7 @@ class DRBD8(BaseDRBD):
             backend, meta, "0", "-e", "detach", "--create-device"]
     result = utils.RunCmd(args)
     if result.failed:
-      logger.Error("Can't attach local disk: %s" % result.output)
+      logging.error("Can't attach local disk: %s", result.output)
     return not result.failed
 
   @classmethod
@@ -972,8 +1063,8 @@ class DRBD8(BaseDRBD):
       args.extend(["-a", hmac, "-x", secret])
     result = utils.RunCmd(args)
     if result.failed:
-      logger.Error("Can't setup network for dbrd device: %s - %s" %
-                   (result.fail_reason, result.output))
+      logging.error("Can't setup network for dbrd device: %s - %s",
+                    result.fail_reason, result.output)
       return False
 
     timeout = time.time() + 10
@@ -990,7 +1081,7 @@ class DRBD8(BaseDRBD):
       ok = True
       break
     if not ok:
-      logger.Error("Timeout while configuring network")
+      logging.error("Timeout while configuring network")
       return False
     return True
 
@@ -1035,7 +1126,7 @@ class DRBD8(BaseDRBD):
       raise errors.BlockDeviceError("We don't have two children: %s" %
                                     self._children)
     if self._children.count(None) == 2: # we don't actually have children :)
-      logger.Error("Requested detach while detached")
+      logging.error("Requested detach while detached")
       return
     if len(devices) != 2:
       raise errors.BlockDeviceError("We need two children in RemoveChildren")
@@ -1055,15 +1146,27 @@ class DRBD8(BaseDRBD):
     """
     children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
     if self.minor is None:
-      logger.Info("Instance not attached to a device")
+      logging.info("Instance not attached to a device")
       return False
     result = utils.RunCmd(["drbdsetup", self.dev_path, "syncer", "-r", "%d" %
                            kbytes])
     if result.failed:
-      logger.Error("Can't change syncer rate: %s - %s" %
-                   (result.fail_reason, result.output))
+      logging.error("Can't change syncer rate: %s - %s",
+                    result.fail_reason, result.output)
     return not result.failed and children_result
 
+  def GetProcStatus(self):
+    """Return device data from /proc.
+
+    """
+    if self.minor is None:
+      raise errors.BlockDeviceError("GetStats() called while not attached")
+    proc_info = self._MassageProcData(self._GetProcData())
+    if self.minor not in proc_info:
+      raise errors.BlockDeviceError("Can't find myself in /proc (minor %d)" %
+                                    self.minor)
+    return DRBD8Status(proc_info[self.minor])
+
   def GetSyncStatus(self):
     """Returns the sync status of the device.
 
@@ -1084,31 +1187,10 @@ class DRBD8(BaseDRBD):
     """
     if self.minor is None and not self.Attach():
       raise errors.BlockDeviceError("Can't attach to device in GetSyncStatus")
-    proc_info = self._MassageProcData(self._GetProcData())
-    if self.minor not in proc_info:
-      raise errors.BlockDeviceError("Can't find myself in /proc (minor %d)" %
-                                    self.minor)
-    line = proc_info[self.minor]
-    match = re.match("^.*sync'ed: *([0-9.]+)%.*"
-                     " finish: ([0-9]+):([0-9]+):([0-9]+) .*$", line)
-    if match:
-      sync_percent = float(match.group(1))
-      hours = int(match.group(2))
-      minutes = int(match.group(3))
-      seconds = int(match.group(4))
-      est_time = hours * 3600 + minutes * 60 + seconds
-    else:
-      sync_percent = None
-      est_time = None
-    match = re.match("^ *\d+: cs:(\w+).*ds:(\w+)/(\w+).*$", line)
-    if not match:
-      raise errors.BlockDeviceError("Can't find my data in /proc (minor %d)" %
-                                    self.minor)
-    client_state = match.group(1)
-    local_disk_state = match.group(2)
-    ldisk = local_disk_state != "UpToDate"
-    is_degraded = client_state != "Connected"
-    return sync_percent, est_time, is_degraded or ldisk, ldisk
+    stats = self.GetProcStatus()
+    ldisk = not stats.is_disk_uptodate
+    is_degraded = not stats.is_connected
+    return stats.sync_percent, stats.est_time, is_degraded or ldisk, ldisk
 
   def Open(self, force=False):
     """Make the local state primary.
@@ -1120,7 +1202,7 @@ class DRBD8(BaseDRBD):
 
     """
     if self.minor is None and not self.Attach():
-      logger.Error("DRBD cannot attach to a device during open")
+      logging.error("DRBD cannot attach to a device during open")
       return False
     cmd = ["drbdsetup", self.dev_path, "primary"]
     if force:
@@ -1128,7 +1210,7 @@ class DRBD8(BaseDRBD):
     result = utils.RunCmd(cmd)
     if result.failed:
       msg = ("Can't make drbd device primary: %s" % result.output)
-      logger.Error(msg)
+      logging.error(msg)
       raise errors.BlockDeviceError(msg)
 
   def Close(self):
@@ -1138,13 +1220,13 @@ class DRBD8(BaseDRBD):
 
     """
     if self.minor is None and not self.Attach():
-      logger.Info("Instance not attached to a device")
+      logging.info("Instance not attached to a device")
       raise errors.BlockDeviceError("Can't find device")
     result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
     if result.failed:
       msg = ("Can't switch drbd device to"
              " secondary: %s" % result.output)
-      logger.Error(msg)
+      logging.error(msg)
       raise errors.BlockDeviceError(msg)
 
   def Attach(self):
@@ -1224,7 +1306,7 @@ class DRBD8(BaseDRBD):
     """
     self.Attach()
     if self.minor is not None:
-      logger.Info("Already assembled")
+      logging.info("Already assembled")
       return True
 
     result = super(DRBD8, self).Assemble()
@@ -1247,7 +1329,7 @@ class DRBD8(BaseDRBD):
       if not result:
         if need_localdev_teardown:
           # we will ignore failures from this
-          logger.Error("net setup failed, tearing down local device")
+          logging.error("net setup failed, tearing down local device")
           self._ShutdownAll(minor)
         return False
     self._SetFromMinor(minor)
@@ -1263,7 +1345,7 @@ class DRBD8(BaseDRBD):
     """
     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
     if result.failed:
-      logger.Error("Can't detach local device: %s" % result.output)
+      logging.error("Can't detach local device: %s", result.output)
     return not result.failed
 
   @classmethod
@@ -1275,7 +1357,7 @@ class DRBD8(BaseDRBD):
     """
     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
     if result.failed:
-      logger.Error("Can't shutdown network: %s" % result.output)
+      logging.error("Can't shutdown network: %s", result.output)
     return not result.failed
 
   @classmethod
@@ -1287,7 +1369,7 @@ class DRBD8(BaseDRBD):
     """
     result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
     if result.failed:
-      logger.Error("Can't shutdown drbd device: %s" % result.output)
+      logging.error("Can't shutdown drbd device: %s", result.output)
     return not result.failed
 
   def Shutdown(self):
@@ -1295,7 +1377,7 @@ class DRBD8(BaseDRBD):
 
     """
     if self.minor is None and not self.Attach():
-      logger.Info("DRBD device not attached to a device during Shutdown")
+      logging.info("DRBD device not attached to a device during Shutdown")
       return True
     if not self._ShutdownAll(self.minor):
       return False
@@ -1330,6 +1412,21 @@ class DRBD8(BaseDRBD):
       raise errors.BlockDeviceError("Cannot initalize meta device")
     return cls(unique_id, children)
 
+  def Grow(self, amount):
+    """Resize the DRBD device and its backing storage.
+
+    """
+    if self.minor is None:
+      raise errors.ProgrammerError("drbd8: Grow called while not attached")
+    if len(self._children) != 2 or None in self._children:
+      raise errors.BlockDeviceError("Cannot grow diskless DRBD8 device")
+    self._children[0].Grow(amount)
+    result = utils.RunCmd(["drbdsetup", self.dev_path, "resize"])
+    if result.failed:
+      raise errors.BlockDeviceError("resize failed for %s: %s" %
+                                    (self.dev_path, result.output))
+    return
+
 
 class FileStorage(BlockDev):
   """File device.
@@ -1400,8 +1497,7 @@ class FileStorage(BlockDev):
       os.remove(self.dev_path)
       return True
     except OSError, err:
-      logger.Error("Can't remove file '%s': %s"
-                   % (self.dev_path, err))
+      logging.error("Can't remove file '%s': %s", self.dev_path, err)
       return False
 
   def Attach(self):
@@ -1460,7 +1556,7 @@ def FindDevice(dev_type, unique_id, children):
   if dev_type not in DEV_MAP:
     raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
   device = DEV_MAP[dev_type](unique_id, children)
-  if not device.Attach():
+  if not device.attached:
     return None
   return  device
 
@@ -1475,9 +1571,9 @@ def AttachOrAssemble(dev_type, unique_id, children):
   if dev_type not in DEV_MAP:
     raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
   device = DEV_MAP[dev_type](unique_id, children)
-  if not device.Attach():
+  if not device.attached:
     device.Assemble()
-    if not device.Attach():
+    if not device.attached:
       raise errors.BlockDeviceError("Can't find a valid block device for"
                                     " %s/%s/%s" %
                                     (dev_type, unique_id, children))