Get rid of constants.HT_HVM_DEFAULT_BOOT_ORDER
[ganeti-local] / lib / bdev.py
index f29da8f..834d7b3 100644 (file)
@@ -87,36 +87,16 @@ class BlockDev(object):
   def Assemble(self):
     """Assemble the device from its components.
 
-    If this is a plain block device (e.g. LVM) than assemble does
-    nothing, as the LVM has no children and we don't put logical
-    volumes offline.
-
-    One guarantee is that after the device has been assembled, it
-    knows its major/minor numbers. This allows other devices (usually
-    parents) to probe correctly for their children.
+    Implementations of this method by child classes must ensure that:
+      - after the device has been assembled, it knows its major/minor
+        numbers; this allows other devices (usually parents) to probe
+        correctly for their children
+      - calling this method on an existing, in-use device is safe
+      - if the device is already configured (and in an OK state),
+        this method is idempotent
 
     """
-    status = True
-    for child in self._children:
-      if not isinstance(child, BlockDev):
-        raise TypeError("Invalid child passed of type '%s'" % type(child))
-      if not status:
-        break
-      status = status and child.Assemble()
-      if not status:
-        break
-
-      try:
-        child.Open()
-      except errors.BlockDeviceError:
-        for child in self._children:
-          child.Shutdown()
-        raise
-
-    if not status:
-      for child in self._children:
-        child.Shutdown()
-    return status
+    return True
 
   def Attach(self):
     """Find a device which matches our config and attach to it.
@@ -295,7 +275,8 @@ class LogicalVolume(BlockDev):
 
     """
     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
-      raise ValueError("Invalid configuration data %s" % str(unique_id))
+      raise errors.ProgrammerError("Invalid configuration data %s" %
+                                   str(unique_id))
     vg_name, lv_name = unique_id
     pvs_info = cls.GetPVInfo(vg_name)
     if not pvs_info:
@@ -315,8 +296,8 @@ class LogicalVolume(BlockDev):
     result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-n%s" % lv_name,
                            vg_name] + pvlist)
     if result.failed:
-      raise errors.BlockDeviceError("%s - %s" % (result.fail_reason,
-                                                result.output))
+      raise errors.BlockDeviceError("LV create failed (%s): %s" %
+                                    (result.fail_reason, result.output))
     return LogicalVolume(unique_id, children)
 
   @staticmethod
@@ -559,20 +540,28 @@ class DRBD8Status(object):
   Note that this doesn't support unconfigured devices (cs:Unconfigured).
 
   """
+  UNCONF_RE = re.compile(r"\s*[0-9]+:\s*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)
+    u = self.UNCONF_RE.match(procline)
+    if u:
+      self.cstatus = "Unconfigured"
+      self.lrole = self.rrole = self.ldisk = self.rdisk = None
+    else:
+      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)
+
+    # end reading of data from the LINE_RE or UNCONF_RE
 
     self.is_standalone = self.cstatus == "StandAlone"
     self.is_wfconn = self.cstatus == "WFConnection"
@@ -587,6 +576,9 @@ class DRBD8Status(object):
     self.is_diskless = self.ldisk == "Diskless"
     self.is_disk_uptodate = self.ldisk == "UpToDate"
 
+    self.is_in_resync = self.cstatus in ("SyncSource", "SyncTarget")
+    self.is_in_use = self.cstatus != "Unconfigured"
+
     m = self.SYNC_RE.match(procline)
     if m:
       self.sync_percent = float(m.group(1))
@@ -700,7 +692,7 @@ class BaseDRBD(BlockDev):
     return "/dev/drbd%d" % minor
 
   @classmethod
-  def _GetUsedDevs(cls):
+  def GetUsedDevs(cls):
     """Compute the list of used DRBD devices.
 
     """
@@ -786,6 +778,9 @@ class DRBD8(BaseDRBD):
   _MAX_MINORS = 255
   _PARSE_SHOW = None
 
+  # timeout constants
+  _NET_RECONFIG_TIMEOUT = 60
+
   def __init__(self, unique_id, children):
     if children and children.count(None) > 0:
       children = []
@@ -853,21 +848,6 @@ class DRBD8(BaseDRBD):
     return highest + 1
 
   @classmethod
-  def _IsValidMeta(cls, meta_device):
-    """Check if the given meta device looks like a valid one.
-
-    """
-    minor = cls._FindUnusedMinor()
-    minor_path = cls._DevPath(minor)
-    result = utils.RunCmd(["drbdmeta", minor_path,
-                           "v08", meta_device, "0",
-                           "dstate"])
-    if result.failed:
-      logging.error("Invalid meta device %s: %s", meta_device, result.output)
-      return False
-    return True
-
-  @classmethod
   def _GetShowParser(cls):
     """Return a parser for `drbd show` output.
 
@@ -1028,12 +1008,7 @@ class DRBD8(BaseDRBD):
   def _AssembleLocal(cls, minor, backend, meta):
     """Configure the local part of a DRBD device.
 
-    This is the first thing that must be done on an unconfigured DRBD
-    device. And it must be done only once.
-
     """
-    if not cls._IsValidMeta(meta):
-      return False
     args = ["drbdsetup", cls._DevPath(minor), "disk",
             backend, meta, "0", "-e", "detach", "--create-device"]
     result = utils.RunCmd(args)
@@ -1053,6 +1028,14 @@ class DRBD8(BaseDRBD):
       # sure its shutdown
       return cls._ShutdownNet(minor)
 
+    # Workaround for a race condition. When DRBD is doing its dance to
+    # establish a connection with its peer, it also sends the
+    # synchronization speed over the wire. In some cases setting the
+    # sync speed only after setting up both sides can race with DRBD
+    # connecting, hence we set it here before telling DRBD anything
+    # about its peer.
+    cls._SetMinorSyncSpeed(minor, constants.SYNC_SPEED)
+
     args = ["drbdsetup", cls._DevPath(minor), "net",
             "%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport), protocol,
             "-A", "discard-zero-changes",
@@ -1106,8 +1089,6 @@ class DRBD8(BaseDRBD):
     if not self._CheckMetaSize(meta.dev_path):
       raise errors.BlockDeviceError("Invalid meta device size")
     self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
-    if not self._IsValidMeta(meta.dev_path):
-      raise errors.BlockDeviceError("Cannot initalize meta device")
 
     if not self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path):
       raise errors.BlockDeviceError("Can't attach to local storage")
@@ -1142,20 +1123,41 @@ class DRBD8(BaseDRBD):
       raise errors.BlockDeviceError("Can't detach from local storage")
     self._children = []
 
-  def SetSyncSpeed(self, kbytes):
+  @classmethod
+  def _SetMinorSyncSpeed(cls, minor, kbytes):
     """Set the speed of the DRBD syncer.
 
+    This is the low-level implementation.
+
+    @type minor: int
+    @param minor: the drbd minor whose settings we change
+    @type kbytes: int
+    @param kbytes: the speed in kbytes/second
+    @rtype: boolean
+    @return: the success of the operation
+
     """
-    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
-    if self.minor is None:
-      logging.info("Instance not attached to a device")
-      return False
-    result = utils.RunCmd(["drbdsetup", self.dev_path, "syncer", "-r", "%d" %
-                           kbytes])
+    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
+                           "-r", "%d" % kbytes, "--create-device"])
     if result.failed:
       logging.error("Can't change syncer rate: %s - %s",
                     result.fail_reason, result.output)
-    return not result.failed and children_result
+    return not result.failed
+
+  def SetSyncSpeed(self, kbytes):
+    """Set the speed of the DRBD syncer.
+
+    @type kbytes: int
+    @param kbytes: the speed in kbytes/second
+    @rtype: boolean
+    @return: the success of the operation
+
+    """
+    if self.minor is None:
+      logging.info("Not attached during SetSyncSpeed")
+      return False
+    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
+    return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
 
   def GetProcStatus(self):
     """Return device data from /proc.
@@ -1232,8 +1234,126 @@ class DRBD8(BaseDRBD):
       logging.error(msg)
       raise errors.BlockDeviceError(msg)
 
+  def DisconnectNet(self):
+    """Removes network configuration.
+
+    This method shutdowns the network side of the device.
+
+    The method will wait up to a hardcoded timeout for the device to
+    go into standalone after the 'disconnect' command before
+    re-configuring it, as sometimes it takes a while for the
+    disconnect to actually propagate and thus we might issue a 'net'
+    command while the device is still connected. If the device will
+    still be attached to the network and we time out, we raise an
+    exception.
+
+    """
+    if self.minor is None:
+      raise errors.BlockDeviceError("DRBD disk not attached in re-attach net")
+
+    if None in (self._lhost, self._lport, self._rhost, self._rport):
+      raise errors.BlockDeviceError("DRBD disk missing network info in"
+                                    " DisconnectNet()")
+
+    ever_disconnected = self._ShutdownNet(self.minor)
+    timeout_limit = time.time() + self._NET_RECONFIG_TIMEOUT
+    sleep_time = 0.100 # we start the retry time at 100 miliseconds
+    while time.time() < timeout_limit:
+      status = self.GetProcStatus()
+      if status.is_standalone:
+        break
+      # retry the disconnect, it seems possible that due to a
+      # well-time disconnect on the peer, my disconnect command might
+      # be ingored and forgotten
+      ever_disconnected = self._ShutdownNet(self.minor) or ever_disconnected
+      time.sleep(sleep_time)
+      sleep_time = min(2, sleep_time * 1.5)
+
+    if not status.is_standalone:
+      if ever_disconnected:
+        msg = ("Device did not react to the"
+               " 'disconnect' command in a timely manner")
+      else:
+        msg = ("Can't shutdown network, even after multiple retries")
+      raise errors.BlockDeviceError(msg)
+
+    reconfig_time = time.time() - timeout_limit + self._NET_RECONFIG_TIMEOUT
+    if reconfig_time > 15: # hardcoded alert limit
+      logging.debug("DRBD8.DisconnectNet: detach took %.3f seconds",
+                    reconfig_time)
+
+  def AttachNet(self, multimaster):
+    """Reconnects the network.
+
+    This method connects the network side of the device with a
+    specified multi-master flag. The device needs to be 'Standalone'
+    but have valid network configuration data.
+
+    Args:
+      - multimaster: init the network in dual-primary mode
+
+    """
+    if self.minor is None:
+      raise errors.BlockDeviceError("DRBD disk not attached in AttachNet")
+
+    if None in (self._lhost, self._lport, self._rhost, self._rport):
+      raise errors.BlockDeviceError("DRBD disk missing network info in"
+                                    " AttachNet()")
+
+    status = self.GetProcStatus()
+
+    if not status.is_standalone:
+      raise errors.BlockDeviceError("Device is not standalone in AttachNet")
+
+    return self._AssembleNet(self.minor,
+                             (self._lhost, self._lport,
+                              self._rhost, self._rport),
+                             "C", dual_pri=multimaster)
+
   def Attach(self):
-    """Find a DRBD device which matches our config and attach to it.
+    """Check if our minor is configured.
+
+    This doesn't do any device configurations - it only checks if the
+    minor is in a state different from Unconfigured.
+
+    Note that this function will not change the state of the system in
+    any way (except in case of side-effects caused by reading from
+    /proc).
+
+    """
+    used_devs = self.GetUsedDevs()
+    if self._aminor in used_devs:
+      minor = self._aminor
+    else:
+      minor = None
+
+    self._SetFromMinor(minor)
+    return minor is not None
+
+  def Assemble(self):
+    """Assemble the drbd.
+
+    Method:
+      - if we have a configured device, we try to ensure that it matches
+        our config
+      - if not, we create it from zero
+
+    """
+    result = super(DRBD8, self).Assemble()
+    if not result:
+      return result
+
+    self.Attach()
+    if self.minor is None:
+      # local device completely unconfigured
+      return self._FastAssemble()
+    else:
+      # we have to recheck the local and network status and try to fix
+      # the device
+      return self._SlowAssemble()
+
+  def _SlowAssemble(self):
+    """Assembles the DRBD device from a (partially) configured device.
 
     In case of partially attached (local device matches but no network
     setup), we perform the network attach. If successful, we re-test
@@ -1294,34 +1414,12 @@ class DRBD8(BaseDRBD):
     self._SetFromMinor(minor)
     return minor is not None
 
-  def Assemble(self):
-    """Assemble the drbd.
+  def _FastAssemble(self):
+    """Assemble the drbd device from zero.
 
-    Method:
-      - if we have a local backing device, we bind to it by:
-        - checking the list of used drbd devices
-        - check if the local minor use of any of them is our own device
-        - if yes, abort?
-        - if not, bind
-      - if we have a local/remote net info:
-        - redo the local backing device step for the remote device
-        - check if any drbd device is using the local port,
-          if yes abort
-        - check if any remote drbd device is using the remote
-          port, if yes abort (for now)
-        - bind our net port
-        - bind the remote net port
+    This is run when in Assemble we detect our minor is unused.
 
     """
-    self.Attach()
-    if self.minor is not None:
-      logging.info("Already assembled")
-      return True
-
-    result = super(DRBD8, self).Assemble()
-    if not result:
-      return result
-
     # TODO: maybe completely tear-down the minor (drbdsetup ... down)
     # before attaching our own?
     minor = self._aminor
@@ -1414,15 +1512,24 @@ class DRBD8(BaseDRBD):
     """
     if len(children) != 2:
       raise errors.ProgrammerError("Invalid setup for the drbd device")
+    # check that the minor is unused
+    aminor = unique_id[4]
+    proc_info = cls._MassageProcData(cls._GetProcData())
+    if aminor in proc_info:
+      status = DRBD8Status(proc_info[aminor])
+      in_use = status.is_in_use
+    else:
+      in_use = False
+    if in_use:
+      raise errors.BlockDeviceError("DRBD minor %d already in use at"
+                                    " Create() time" % aminor)
     meta = children[1]
     meta.Assemble()
     if not meta.Attach():
       raise errors.BlockDeviceError("Can't attach to meta device")
     if not cls._CheckMetaSize(meta.dev_path):
       raise errors.BlockDeviceError("Invalid meta device size")
-    cls._InitMeta(cls._FindUnusedMinor(), meta.dev_path)
-    if not cls._IsValidMeta(meta.dev_path):
-      raise errors.BlockDeviceError("Cannot initalize meta device")
+    cls._InitMeta(aminor, meta.dev_path)
     return cls(unique_id, children)
 
   def Grow(self, amount):
@@ -1536,16 +1643,17 @@ class FileStorage(BlockDev):
     @return: an instance of FileStorage
 
     """
+    # TODO: decide whether we should check for existing files and
+    # abort or not
     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
       raise ValueError("Invalid configuration data %s" % str(unique_id))
     dev_path = unique_id[1]
     try:
       f = open(dev_path, 'w')
-    except IOError, err:
-      raise errors.BlockDeviceError("Could not create '%'" % err)
-    else:
       f.truncate(size * 1024 * 1024)
       f.close()
+    except IOError, err:
+      raise errors.BlockDeviceError("Error in file creation: %" % str(err))
 
     return FileStorage(unique_id, children)
 
@@ -1572,22 +1680,20 @@ def FindDevice(dev_type, unique_id, children):
   return device
 
 
-def AttachOrAssemble(dev_type, unique_id, children):
+def Assemble(dev_type, unique_id, children):
   """Try to attach or assemble an existing device.
 
-  This will attach to an existing assembled device or will assemble
-  the device, as needed, to bring it fully up.
+  This will attach to assemble the device, as needed, to bring it
+  fully up. It must be safe to run on already-assembled devices.
 
   """
   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.attached:
-    device.Assemble()
-    if not device.attached:
-      raise errors.BlockDeviceError("Can't find a valid block device for"
-                                    " %s/%s/%s" %
-                                    (dev_type, unique_id, children))
+  if not device.Assemble():
+    raise errors.BlockDeviceError("Can't find a valid block device for"
+                                  " %s/%s/%s" %
+                                  (dev_type, unique_id, children))
   return device