Move a function from backend to ssconf
[ganeti-local] / lib / bdev.py
index 666c44c..d6c1a66 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2006, 2007, 2010, 2011 Google Inc.
+# Copyright (C) 2006, 2007, 2010, 2011, 2012 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
@@ -338,7 +338,7 @@ class BlockDev(object):
     for child in self._children:
       child.SetInfo(text)
 
-  def Grow(self, amount, dryrun):
+  def Grow(self, amount, dryrun, backingstore):
     """Grow the block device.
 
     @type amount: integer
@@ -346,6 +346,9 @@ class BlockDev(object):
     @type dryrun: boolean
     @param dryrun: whether to execute the operation in simulation mode
         only, without actually increasing the size
+    @param backingstore: whether to execute the operation on backing storage
+        only, or on "logical" storage only; e.g. DRBD is logical storage,
+        whereas LVM, file, RBD are backing storage
 
     """
     raise NotImplementedError
@@ -618,8 +621,8 @@ class LogicalVolume(BlockDev):
       return False
 
     status, major, minor, pe_size, stripes = out
-    if len(status) != 6:
-      logging.error("lvs lv_attr is not 6 characters (%s)", status)
+    if len(status) < 6:
+      logging.error("lvs lv_attr is not at least 6 characters (%s)", status)
       return False
 
     try:
@@ -768,10 +771,12 @@ class LogicalVolume(BlockDev):
       _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason,
                   result.output)
 
-  def Grow(self, amount, dryrun):
+  def Grow(self, amount, dryrun, backingstore):
     """Grow the logical volume.
 
     """
+    if not backingstore:
+      return
     if self.pe_size is None or self.stripe_count is None:
       if not self.Attach():
         _ThrowError("Can't attach to LV during Grow()")
@@ -985,12 +990,13 @@ class BaseDRBD(BlockDev): # pylint: disable=W0223
                                     first_line)
 
     values = version.groups()
-    retval = {"k_major": int(values[0]),
-              "k_minor": int(values[1]),
-              "k_point": int(values[2]),
-              "api": int(values[3]),
-              "proto": int(values[4]),
-             }
+    retval = {
+      "k_major": int(values[0]),
+      "k_minor": int(values[1]),
+      "k_point": int(values[2]),
+      "api": int(values[3]),
+      "proto": int(values[4]),
+      }
     if values[5] is not None:
       retval["proto2"] = values[5]
 
@@ -1058,7 +1064,7 @@ class BaseDRBD(BlockDev): # pylint: disable=W0223
   def _CheckMetaSize(meta_device):
     """Check if the given meta device looks like a valid one.
 
-    This currently only check the size, which must be around
+    This currently only checks the size, which must be around
     128MiB.
 
     """
@@ -1098,10 +1104,10 @@ class DRBD8(BaseDRBD):
   doesn't do anything to the supposed peer. If you need a fully
   connected DRBD pair, you need to use this class on both hosts.
 
-  The unique_id for the drbd device is the (local_ip, local_port,
-  remote_ip, remote_port) tuple, and it must have two children: the
-  data device and the meta_device. The meta device is checked for
-  valid size and is zeroed on create.
+  The unique_id for the drbd device is a (local_ip, local_port,
+  remote_ip, remote_port, local_minor, secret) tuple, and it must have
+  two children: the data device and the meta_device. The meta device
+  is checked for valid size and is zeroed on create.
 
   """
   _MAX_MINORS = 255
@@ -1197,7 +1203,7 @@ class DRBD8(BaseDRBD):
   def _GetShowParser(cls):
     """Return a parser for `drbd show` output.
 
-    This will either create or return an already-create parser for the
+    This will either create or return an already-created parser for the
     output of the command `drbd show`.
 
     """
@@ -1218,10 +1224,10 @@ class DRBD8(BaseDRBD):
     defa = pyp.Literal("_is_default").suppress()
     dbl_quote = pyp.Literal('"').suppress()
 
-    keyword = pyp.Word(pyp.alphanums + '-')
+    keyword = pyp.Word(pyp.alphanums + "-")
 
     # value types
-    value = pyp.Word(pyp.alphanums + '_-/.:')
+    value = pyp.Word(pyp.alphanums + "_-/.:")
     quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
     ipv4_addr = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
                  pyp.Word(pyp.nums + ".") + colon + number)
@@ -1388,7 +1394,7 @@ class DRBD8(BaseDRBD):
 
   @classmethod
   def _ComputeDiskBarrierArgs(cls, vmaj, vmin, vrel, disabled_barriers,
-      disable_meta_flush):
+                              disable_meta_flush):
     """Compute the DRBD command line parameters for disk barriers
 
     Returns a list of the disk barrier parameters as requested via the
@@ -1622,7 +1628,7 @@ class DRBD8(BaseDRBD):
                    "--c-delay-target", params[constants.LDP_DELAY_TARGET],
                    "--c-max-rate", params[constants.LDP_MAX_RATE],
                    "--c-min-rate", params[constants.LDP_MIN_RATE],
-                  ])
+                   ])
 
     else:
       args.extend(["-r", "%d" % params[constants.LDP_RESYNC_RATE]])
@@ -1633,7 +1639,7 @@ class DRBD8(BaseDRBD):
       msg = ("Can't change syncer rate: %s - %s" %
              (result.fail_reason, result.output))
       logging.error(msg)
-      return msg
+      return [msg]
 
     return []
 
@@ -2070,7 +2076,7 @@ class DRBD8(BaseDRBD):
     cls._InitMeta(aminor, meta.dev_path)
     return cls(unique_id, children, size, params)
 
-  def Grow(self, amount, dryrun):
+  def Grow(self, amount, dryrun, backingstore):
     """Resize the DRBD device and its backing storage.
 
     """
@@ -2078,9 +2084,10 @@ class DRBD8(BaseDRBD):
       _ThrowError("drbd%d: Grow called while not attached", self._aminor)
     if len(self._children) != 2 or None in self._children:
       _ThrowError("drbd%d: cannot grow diskless device", self.minor)
-    self._children[0].Grow(amount, dryrun)
-    if dryrun:
-      # DRBD does not support dry-run mode, so we'll return here
+    self._children[0].Grow(amount, dryrun, backingstore)
+    if dryrun or backingstore:
+      # DRBD does not support dry-run mode and is not backing storage,
+      # so we'll return here
       return
     result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
                            "%dm" % (self.size + amount)])
@@ -2163,12 +2170,14 @@ class FileStorage(BlockDev):
     # TODO: implement rename for file-based storage
     _ThrowError("Rename is not supported for file-based storage")
 
-  def Grow(self, amount, dryrun):
+  def Grow(self, amount, dryrun, backingstore):
     """Grow the file
 
     @param amount: the amount (in mebibytes) to grow with
 
     """
+    if not backingstore:
+      return
     # Check that the file exists
     self.Assemble()
     current_size = self.GetActualSize()
@@ -2339,17 +2348,311 @@ class PersistentBlockDevice(BlockDev):
     """
     pass
 
-  def Grow(self, amount, dryrun):
+  def Grow(self, amount, dryrun, backingstore):
     """Grow the logical volume.
 
     """
     _ThrowError("Grow is not supported for PersistentBlockDev storage")
 
 
+class RADOSBlockDevice(BlockDev):
+  """A RADOS Block Device (rbd).
+
+  This class implements the RADOS Block Device for the backend. You need
+  the rbd kernel driver, the RADOS Tools and a working RADOS cluster for
+  this to be functional.
+
+  """
+  def __init__(self, unique_id, children, size, params):
+    """Attaches to an rbd device.
+
+    """
+    super(RADOSBlockDevice, self).__init__(unique_id, children, size, params)
+    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
+      raise ValueError("Invalid configuration data %s" % str(unique_id))
+
+    self.driver, self.rbd_name = unique_id
+
+    self.major = self.minor = None
+    self.Attach()
+
+  @classmethod
+  def Create(cls, unique_id, children, size, params):
+    """Create a new rbd device.
+
+    Provision a new rbd volume inside a RADOS pool.
+
+    """
+    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
+      raise errors.ProgrammerError("Invalid configuration data %s" %
+                                   str(unique_id))
+    rbd_pool = params[constants.LDP_POOL]
+    rbd_name = unique_id[1]
+
+    # Provision a new rbd volume (Image) inside the RADOS cluster.
+    cmd = [constants.RBD_CMD, "create", "-p", rbd_pool,
+           rbd_name, "--size", "%s" % size]
+    result = utils.RunCmd(cmd)
+    if result.failed:
+      _ThrowError("rbd creation failed (%s): %s",
+                  result.fail_reason, result.output)
+
+    return RADOSBlockDevice(unique_id, children, size, params)
+
+  def Remove(self):
+    """Remove the rbd device.
+
+    """
+    rbd_pool = self.params[constants.LDP_POOL]
+    rbd_name = self.unique_id[1]
+
+    if not self.minor and not self.Attach():
+      # The rbd device doesn't exist.
+      return
+
+    # First shutdown the device (remove mappings).
+    self.Shutdown()
+
+    # Remove the actual Volume (Image) from the RADOS cluster.
+    cmd = [constants.RBD_CMD, "rm", "-p", rbd_pool, rbd_name]
+    result = utils.RunCmd(cmd)
+    if result.failed:
+      _ThrowError("Can't remove Volume from cluster with rbd rm: %s - %s",
+                  result.fail_reason, result.output)
+
+  def Rename(self, new_id):
+    """Rename this device.
+
+    """
+    pass
+
+  def Attach(self):
+    """Attach to an existing rbd device.
+
+    This method maps the rbd volume that matches our name with
+    an rbd device and then attaches to this device.
+
+    """
+    self.attached = False
+
+    # Map the rbd volume to a block device under /dev
+    self.dev_path = self._MapVolumeToBlockdev(self.unique_id)
+
+    try:
+      st = os.stat(self.dev_path)
+    except OSError, err:
+      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
+      return False
+
+    if not stat.S_ISBLK(st.st_mode):
+      logging.error("%s is not a block device", self.dev_path)
+      return False
+
+    self.major = os.major(st.st_rdev)
+    self.minor = os.minor(st.st_rdev)
+    self.attached = True
+
+    return True
+
+  def _MapVolumeToBlockdev(self, unique_id):
+    """Maps existing rbd volumes to block devices.
+
+    This method should be idempotent if the mapping already exists.
+
+    @rtype: string
+    @return: the block device path that corresponds to the volume
+
+    """
+    pool = self.params[constants.LDP_POOL]
+    name = unique_id[1]
+
+    # Check if the mapping already exists.
+    showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
+    result = utils.RunCmd(showmap_cmd)
+    if result.failed:
+      _ThrowError("rbd showmapped failed (%s): %s",
+                  result.fail_reason, result.output)
+
+    rbd_dev = self._ParseRbdShowmappedOutput(result.output, name)
+
+    if rbd_dev:
+      # The mapping exists. Return it.
+      return rbd_dev
+
+    # The mapping doesn't exist. Create it.
+    map_cmd = [constants.RBD_CMD, "map", "-p", pool, name]
+    result = utils.RunCmd(map_cmd)
+    if result.failed:
+      _ThrowError("rbd map failed (%s): %s",
+                  result.fail_reason, result.output)
+
+    # Find the corresponding rbd device.
+    showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
+    result = utils.RunCmd(showmap_cmd)
+    if result.failed:
+      _ThrowError("rbd map succeeded, but showmapped failed (%s): %s",
+                  result.fail_reason, result.output)
+
+    rbd_dev = self._ParseRbdShowmappedOutput(result.output, name)
+
+    if not rbd_dev:
+      _ThrowError("rbd map succeeded, but could not find the rbd block"
+                  " device in output of showmapped, for volume: %s", name)
+
+    # The device was successfully mapped. Return it.
+    return rbd_dev
+
+  @staticmethod
+  def _ParseRbdShowmappedOutput(output, volume_name):
+    """Parse the output of `rbd showmapped'.
+
+    This method parses the output of `rbd showmapped' and returns
+    the rbd block device path (e.g. /dev/rbd0) that matches the
+    given rbd volume.
+
+    @type output: string
+    @param output: the whole output of `rbd showmapped'
+    @type volume_name: string
+    @param volume_name: the name of the volume whose device we search for
+    @rtype: string or None
+    @return: block device path if the volume is mapped, else None
+
+    """
+    allfields = 5
+    volumefield = 2
+    devicefield = 4
+
+    field_sep = "\t"
+
+    lines = output.splitlines()
+    splitted_lines = map(lambda l: l.split(field_sep), lines)
+
+    # Check empty output.
+    if not splitted_lines:
+      _ThrowError("rbd showmapped returned empty output")
+
+    # Check showmapped header line, to determine number of fields.
+    field_cnt = len(splitted_lines[0])
+    if field_cnt != allfields:
+      _ThrowError("Cannot parse rbd showmapped output because its format"
+                  " seems to have changed; expected %s fields, found %s",
+                  allfields, field_cnt)
+
+    matched_lines = \
+      filter(lambda l: len(l) == allfields and l[volumefield] == volume_name,
+             splitted_lines)
+
+    if len(matched_lines) > 1:
+      _ThrowError("The rbd volume %s is mapped more than once."
+                  " This shouldn't happen, try to unmap the extra"
+                  " devices manually.", volume_name)
+
+    if matched_lines:
+      # rbd block device found. Return it.
+      rbd_dev = matched_lines[0][devicefield]
+      return rbd_dev
+
+    # The given volume is not mapped.
+    return None
+
+  def Assemble(self):
+    """Assemble the device.
+
+    """
+    pass
+
+  def Shutdown(self):
+    """Shutdown the device.
+
+    """
+    if not self.minor and not self.Attach():
+      # The rbd device doesn't exist.
+      return
+
+    # Unmap the block device from the Volume.
+    self._UnmapVolumeFromBlockdev(self.unique_id)
+
+    self.minor = None
+    self.dev_path = None
+
+  def _UnmapVolumeFromBlockdev(self, unique_id):
+    """Unmaps the rbd device from the Volume it is mapped.
+
+    Unmaps the rbd device from the Volume it was previously mapped to.
+    This method should be idempotent if the Volume isn't mapped.
+
+    """
+    pool = self.params[constants.LDP_POOL]
+    name = unique_id[1]
+
+    # Check if the mapping already exists.
+    showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
+    result = utils.RunCmd(showmap_cmd)
+    if result.failed:
+      _ThrowError("rbd showmapped failed [during unmap](%s): %s",
+                  result.fail_reason, result.output)
+
+    rbd_dev = self._ParseRbdShowmappedOutput(result.output, name)
+
+    if rbd_dev:
+      # The mapping exists. Unmap the rbd device.
+      unmap_cmd = [constants.RBD_CMD, "unmap", "%s" % rbd_dev]
+      result = utils.RunCmd(unmap_cmd)
+      if result.failed:
+        _ThrowError("rbd unmap failed (%s): %s",
+                    result.fail_reason, result.output)
+
+  def Open(self, force=False):
+    """Make the device ready for I/O.
+
+    """
+    pass
+
+  def Close(self):
+    """Notifies that the device will no longer be used for I/O.
+
+    """
+    pass
+
+  def Grow(self, amount, dryrun, backingstore):
+    """Grow the Volume.
+
+    @type amount: integer
+    @param amount: the amount (in mebibytes) to grow with
+    @type dryrun: boolean
+    @param dryrun: whether to execute the operation in simulation mode
+        only, without actually increasing the size
+
+    """
+    if not backingstore:
+      return
+    if not self.Attach():
+      _ThrowError("Can't attach to rbd device during Grow()")
+
+    if dryrun:
+      # the rbd tool does not support dry runs of resize operations.
+      # Since rbd volumes are thinly provisioned, we assume
+      # there is always enough free space for the operation.
+      return
+
+    rbd_pool = self.params[constants.LDP_POOL]
+    rbd_name = self.unique_id[1]
+    new_size = self.size + amount
+
+    # Resize the rbd volume (Image) inside the RADOS cluster.
+    cmd = [constants.RBD_CMD, "resize", "-p", rbd_pool,
+           rbd_name, "--size", "%s" % new_size]
+    result = utils.RunCmd(cmd)
+    if result.failed:
+      _ThrowError("rbd resize failed (%s): %s",
+                  result.fail_reason, result.output)
+
+
 DEV_MAP = {
   constants.LD_LV: LogicalVolume,
   constants.LD_DRBD8: DRBD8,
   constants.LD_BLOCKDEV: PersistentBlockDevice,
+  constants.LD_RBD: RADOSBlockDevice,
   }
 
 if constants.ENABLE_FILE_STORAGE or constants.ENABLE_SHARED_FILE_STORAGE:
@@ -2361,6 +2664,16 @@ def _VerifyDiskType(dev_type):
     raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
 
 
+def _VerifyDiskParams(disk):
+  """Verifies if all disk parameters are set.
+
+  """
+  missing = set(constants.DISK_LD_DEFAULTS[disk.dev_type]) - set(disk.params)
+  if missing:
+    raise errors.ProgrammerError("Block device is missing disk parameters: %s" %
+                                 missing)
+
+
 def FindDevice(disk, children):
   """Search for an existing, assembled device.
 
@@ -2375,10 +2688,8 @@ def FindDevice(disk, children):
 
   """
   _VerifyDiskType(disk.dev_type)
-  dev_params = objects.FillDict(constants.DISK_LD_DEFAULTS[disk.dev_type],
-                                disk.params)
   device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
-                                  dev_params)
+                                  disk.params)
   if not device.attached:
     return None
   return device
@@ -2398,10 +2709,9 @@ def Assemble(disk, children):
 
   """
   _VerifyDiskType(disk.dev_type)
-  dev_params = objects.FillDict(constants.DISK_LD_DEFAULTS[disk.dev_type],
-                                disk.params)
+  _VerifyDiskParams(disk)
   device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
-                                  dev_params)
+                                  disk.params)
   device.Assemble()
   return device
 
@@ -2417,8 +2727,7 @@ def Create(disk, children):
 
   """
   _VerifyDiskType(disk.dev_type)
-  dev_params = objects.FillDict(constants.DISK_LD_DEFAULTS[disk.dev_type],
-                                disk.params)
+  _VerifyDiskParams(disk)
   device = DEV_MAP[disk.dev_type].Create(disk.physical_id, children, disk.size,
-                                         dev_params)
+                                         disk.params)
   return device