Cluster verify checks server.pem permissions
[ganeti-local] / lib / bdev.py
index 8201c26..b1e1ef9 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2006, 2007, 2010, 2011, 2012 Google Inc.
+# Copyright (C) 2006, 2007, 2010, 2011, 2012, 2013 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
@@ -29,6 +29,7 @@ import stat
 import pyparsing as pyp
 import os
 import logging
+import math
 
 from ganeti import utils
 from ganeti import errors
@@ -37,12 +38,20 @@ from ganeti import objects
 from ganeti import compat
 from ganeti import netutils
 from ganeti import pathutils
+from ganeti import serializer
 
 
 # Size of reads in _CanReadDevice
 _DEVICE_READ_SIZE = 128 * 1024
 
 
+class RbdShowmappedJsonError(Exception):
+  """`rbd showmmapped' JSON formatting error Exception class.
+
+  """
+  pass
+
+
 def _IgnoreError(fn, *args, **kwargs):
   """Executes the given function, ignoring BlockDeviceErrors.
 
@@ -175,7 +184,9 @@ def _CheckFileStoragePath(path, allowed):
       break
   else:
     raise errors.FileStoragePathError("Path '%s' is not acceptable for file"
-                                      " storage" % path)
+                                      " storage. A possible fix might be to add"
+                                      " it to /etc/ganeti/file-storage-paths"
+                                      " on all nodes." % path)
 
 
 def _LoadAllowedFileStoragePaths(filename):
@@ -246,7 +257,7 @@ class BlockDev(object):
       an attached instance (lvcreate)
     - attaching of a python instance to an existing (real) device
 
-  The second point, the attachement to a device, is different
+  The second point, the attachment to a device, is different
   depending on whether the device is assembled or not. At init() time,
   we search for a device with the same unique_id as us. If found,
   good. It also means that the device is already assembled. If not,
@@ -290,7 +301,7 @@ class BlockDev(object):
     raise NotImplementedError
 
   @classmethod
-  def Create(cls, unique_id, children, size, params):
+  def Create(cls, unique_id, children, size, params, excl_stor):
     """Create the device.
 
     If the device cannot be created, it will return None
@@ -523,8 +534,45 @@ class LogicalVolume(BlockDev):
     self.major = self.minor = self.pe_size = self.stripe_count = None
     self.Attach()
 
+  @staticmethod
+  def _GetStdPvSize(pvs_info):
+    """Return the the standard PV size (used with exclusive storage).
+
+    @param pvs_info: list of objects.LvmPvInfo, cannot be empty
+    @rtype: float
+    @return: size in MiB
+
+    """
+    assert len(pvs_info) > 0
+    smallest = min([pv.size for pv in pvs_info])
+    return smallest / (1 + constants.PART_MARGIN + constants.PART_RESERVED)
+
+  @staticmethod
+  def _ComputeNumPvs(size, pvs_info):
+    """Compute the number of PVs needed for an LV (with exclusive storage).
+
+    @type size: float
+    @param size: LV size in MiB
+    @param pvs_info: list of objects.LvmPvInfo, cannot be empty
+    @rtype: integer
+    @return: number of PVs needed
+    """
+    assert len(pvs_info) > 0
+    pv_size = float(LogicalVolume._GetStdPvSize(pvs_info))
+    return int(math.ceil(float(size) / pv_size))
+
+  @staticmethod
+  def _GetEmptyPvNames(pvs_info, max_pvs=None):
+    """Return a list of empty PVs, by name.
+
+    """
+    empty_pvs = filter(objects.LvmPvInfo.IsEmpty, pvs_info)
+    if max_pvs is not None:
+      empty_pvs = empty_pvs[:max_pvs]
+    return map((lambda pv: pv.name), empty_pvs)
+
   @classmethod
-  def Create(cls, unique_id, children, size, params):
+  def Create(cls, unique_id, children, size, params, excl_stor):
     """Create a new logical volume.
 
     """
@@ -536,7 +584,11 @@ class LogicalVolume(BlockDev):
     cls._ValidateName(lv_name)
     pvs_info = cls.GetPVInfo([vg_name])
     if not pvs_info:
-      _ThrowError("Can't compute PV info for vg %s", vg_name)
+      if excl_stor:
+        msg = "No (empty) PVs found"
+      else:
+        msg = "Can't compute PV info for vg %s" % vg_name
+      _ThrowError(msg)
     pvs_info.sort(key=(lambda pv: pv.free), reverse=True)
 
     pvlist = [pv.name for pv in pvs_info]
@@ -544,24 +596,43 @@ class LogicalVolume(BlockDev):
       _ThrowError("Some of your PVs have the invalid character ':' in their"
                   " name, this is not supported - please filter them out"
                   " in lvm.conf using either 'filter' or 'preferred_names'")
-    free_size = sum([pv.free for pv in pvs_info])
+
     current_pvs = len(pvlist)
     desired_stripes = params[constants.LDP_STRIPES]
     stripes = min(current_pvs, desired_stripes)
-    if stripes < desired_stripes:
-      logging.warning("Could not use %d stripes for VG %s, as only %d PVs are"
-                      " available.", desired_stripes, vg_name, current_pvs)
 
-    # The size constraint should have been checked from the master before
-    # calling the create function.
-    if free_size < size:
-      _ThrowError("Not enough free space: required %s,"
-                  " available %s", size, free_size)
-    cmd = ["lvcreate", "-L%dm" % size, "-n%s" % lv_name]
+    if excl_stor:
+      (err_msgs, _) = utils.LvmExclusiveCheckNodePvs(pvs_info)
+      if err_msgs:
+        for m in err_msgs:
+          logging.warning(m)
+      req_pvs = cls._ComputeNumPvs(size, pvs_info)
+      pvlist = cls._GetEmptyPvNames(pvs_info, req_pvs)
+      current_pvs = len(pvlist)
+      if current_pvs < req_pvs:
+        _ThrowError("Not enough empty PVs to create a disk of %d MB:"
+                    " %d available, %d needed", size, current_pvs, req_pvs)
+      assert current_pvs == len(pvlist)
+      if stripes > current_pvs:
+        # No warning issued for this, as it's no surprise
+        stripes = current_pvs
+
+    else:
+      if stripes < desired_stripes:
+        logging.warning("Could not use %d stripes for VG %s, as only %d PVs are"
+                        " available.", desired_stripes, vg_name, current_pvs)
+      free_size = sum([pv.free for pv in pvs_info])
+      # The size constraint should have been checked from the master before
+      # calling the create function.
+      if free_size < size:
+        _ThrowError("Not enough free space: required %s,"
+                    " available %s", size, free_size)
+
     # If the free space is not well distributed, we won't be able to
     # create an optimally-striped volume; in that case, we want to try
     # with N, N-1, ..., 2, and finally 1 (non-stripped) number of
     # stripes
+    cmd = ["lvcreate", "-L%dm" % size, "-n%s" % lv_name]
     for stripes_arg in range(stripes, 0, -1):
       result = utils.RunCmd(cmd + ["-i%d" % stripes_arg] + [vg_name] + pvlist)
       if not result.failed:
@@ -573,7 +644,7 @@ class LogicalVolume(BlockDev):
 
   @staticmethod
   def _GetVolumeInfo(lvm_cmd, fields):
-    """Returns LVM Volumen infos using lvm_cmd
+    """Returns LVM Volume infos using lvm_cmd
 
     @param lvm_cmd: Should be one of "pvs", "vgs" or "lvs"
     @param fields: Fields to return
@@ -605,43 +676,84 @@ class LogicalVolume(BlockDev):
     return data
 
   @classmethod
-  def GetPVInfo(cls, vg_names, filter_allocatable=True):
+  def GetPVInfo(cls, vg_names, filter_allocatable=True, include_lvs=False):
     """Get the free space info for PVs in a volume group.
 
     @param vg_names: list of volume group names, if empty all will be returned
     @param filter_allocatable: whether to skip over unallocatable PVs
+    @param include_lvs: whether to include a list of LVs hosted on each PV
 
     @rtype: list
     @return: list of objects.LvmPvInfo objects
 
     """
+    # We request "lv_name" field only if we care about LVs, so we don't get
+    # a long list of entries with many duplicates unless we really have to.
+    # The duplicate "pv_name" field will be ignored.
+    if include_lvs:
+      lvfield = "lv_name"
+    else:
+      lvfield = "pv_name"
     try:
       info = cls._GetVolumeInfo("pvs", ["pv_name", "vg_name", "pv_free",
-                                        "pv_attr", "pv_size"])
+                                        "pv_attr", "pv_size", lvfield])
     except errors.GenericError, err:
       logging.error("Can't get PV information: %s", err)
       return None
 
+    # When asked for LVs, "pvs" may return multiple entries for the same PV-LV
+    # pair. We sort entries by PV name and then LV name, so it's easy to weed
+    # out duplicates.
+    if include_lvs:
+      info.sort(key=(lambda i: (i[0], i[5])))
     data = []
-    for (pv_name, vg_name, pv_free, pv_attr, pv_size) in info:
+    lastpvi = None
+    for (pv_name, vg_name, pv_free, pv_attr, pv_size, lv_name) in info:
       # (possibly) skip over pvs which are not allocatable
       if filter_allocatable and pv_attr[0] != "a":
         continue
       # (possibly) skip over pvs which are not in the right volume group(s)
       if vg_names and vg_name not in vg_names:
         continue
-      pvi = objects.LvmPvInfo(name=pv_name, vg_name=vg_name,
-                              size=float(pv_size), free=float(pv_free),
-                              attributes=pv_attr)
-      data.append(pvi)
+      # Beware of duplicates (check before inserting)
+      if lastpvi and lastpvi.name == pv_name:
+        if include_lvs and lv_name:
+          if not lastpvi.lv_list or lastpvi.lv_list[-1] != lv_name:
+            lastpvi.lv_list.append(lv_name)
+      else:
+        if include_lvs and lv_name:
+          lvl = [lv_name]
+        else:
+          lvl = []
+        lastpvi = objects.LvmPvInfo(name=pv_name, vg_name=vg_name,
+                                    size=float(pv_size), free=float(pv_free),
+                                    attributes=pv_attr, lv_list=lvl)
+        data.append(lastpvi)
 
     return data
 
   @classmethod
-  def GetVGInfo(cls, vg_names, filter_readonly=True):
+  def _GetExclusiveStorageVgFree(cls, vg_name):
+    """Return the free disk space in the given VG, in exclusive storage mode.
+
+    @type vg_name: string
+    @param vg_name: VG name
+    @rtype: float
+    @return: free space in MiB
+    """
+    pvs_info = cls.GetPVInfo([vg_name])
+    if not pvs_info:
+      return 0.0
+    pv_size = cls._GetStdPvSize(pvs_info)
+    num_pvs = len(cls._GetEmptyPvNames(pvs_info))
+    return pv_size * num_pvs
+
+  @classmethod
+  def GetVGInfo(cls, vg_names, excl_stor, filter_readonly=True):
     """Get the free space info for specific VGs.
 
     @param vg_names: list of volume group names, if empty all will be returned
+    @param excl_stor: whether exclusive_storage is enabled
     @param filter_readonly: whether to skip over readonly VGs
 
     @rtype: list
@@ -664,6 +776,11 @@ class LogicalVolume(BlockDev):
       # (possibly) skip over vgs which are not in the right volume group(s)
       if vg_names and vg_name not in vg_names:
         continue
+      # Exclusive storage needs a different concept of free space
+      if excl_stor:
+        es_free = cls._GetExclusiveStorageVgFree(vg_name)
+        assert es_free <= vg_free
+        vg_free = es_free
       data.append((float(vg_free), float(vg_size), vg_name))
 
     return data
@@ -721,7 +838,7 @@ class LogicalVolume(BlockDev):
     """
     self.attached = False
     result = utils.RunCmd(["lvs", "--noheadings", "--separator=,",
-                           "--units=m", "--nosuffix",
+                           "--units=k", "--nosuffix",
                            "-olv_attr,lv_kernel_major,lv_kernel_minor,"
                            "vg_extent_size,stripes", self.dev_path])
     if result.failed:
@@ -860,7 +977,7 @@ class LogicalVolume(BlockDev):
     snap = LogicalVolume((self._vg_name, snap_name), None, size, self.params)
     _IgnoreError(snap.Remove)
 
-    vg_info = self.GetVGInfo([self._vg_name])
+    vg_info = self.GetVGInfo([self._vg_name], False)
     if not vg_info:
       _ThrowError("Can't compute VG info for vg %s", self._vg_name)
     free_size, _, _ = vg_info[0]
@@ -914,10 +1031,12 @@ class LogicalVolume(BlockDev):
       if not self.Attach():
         _ThrowError("Can't attach to LV during Grow()")
     full_stripe_size = self.pe_size * self.stripe_count
+    # pe_size is in KB
+    amount *= 1024
     rest = amount % full_stripe_size
     if rest != 0:
       amount += full_stripe_size - rest
-    cmd = ["lvextend", "-L", "+%dm" % amount]
+    cmd = ["lvextend", "-L", "+%dk" % amount]
     if dryrun:
       cmd.append("--test")
     # we try multiple algorithms since the 'best' ones might not have
@@ -2181,7 +2300,7 @@ class DRBD8(BaseDRBD):
     self.Shutdown()
 
   @classmethod
-  def Create(cls, unique_id, children, size, params):
+  def Create(cls, unique_id, children, size, params, excl_stor):
     """Create a new DRBD8 device.
 
     Since DRBD devices are not created per se, just assembled, this
@@ -2190,6 +2309,9 @@ class DRBD8(BaseDRBD):
     """
     if len(children) != 2:
       raise errors.ProgrammerError("Invalid setup for the drbd device")
+    if excl_stor:
+      raise errors.ProgrammerError("DRBD device requested with"
+                                   " exclusive_storage")
     # check that the minor is unused
     aminor = unique_id[4]
     proc_info = cls._MassageProcData(cls._GetProcData())
@@ -2355,7 +2477,7 @@ class FileStorage(BlockDev):
       _ThrowError("Can't stat %s: %s", self.dev_path, err)
 
   @classmethod
-  def Create(cls, unique_id, children, size, params):
+  def Create(cls, unique_id, children, size, params, excl_stor):
     """Create a new file.
 
     @param size: the size of file in MiB
@@ -2364,6 +2486,9 @@ class FileStorage(BlockDev):
     @return: an instance of FileStorage
 
     """
+    if excl_stor:
+      raise errors.ProgrammerError("FileStorage device requested with"
+                                   " exclusive_storage")
     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
       raise ValueError("Invalid configuration data %s" % str(unique_id))
 
@@ -2420,12 +2545,15 @@ class PersistentBlockDevice(BlockDev):
     self.Attach()
 
   @classmethod
-  def Create(cls, unique_id, children, size, params):
+  def Create(cls, unique_id, children, size, params, excl_stor):
     """Create a new device
 
     This is a noop, we only return a PersistentBlockDevice instance
 
     """
+    if excl_stor:
+      raise errors.ProgrammerError("Persistent block device requested with"
+                                   " exclusive_storage")
     return PersistentBlockDevice(unique_id, children, 0, params)
 
   def Remove(self):
@@ -2517,7 +2645,7 @@ class RADOSBlockDevice(BlockDev):
     self.Attach()
 
   @classmethod
-  def Create(cls, unique_id, children, size, params):
+  def Create(cls, unique_id, children, size, params, excl_stor):
     """Create a new rbd device.
 
     Provision a new rbd volume inside a RADOS pool.
@@ -2526,6 +2654,9 @@ class RADOSBlockDevice(BlockDev):
     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
       raise errors.ProgrammerError("Invalid configuration data %s" %
                                    str(unique_id))
+    if excl_stor:
+      raise errors.ProgrammerError("RBD device requested with"
+                                   " exclusive_storage")
     rbd_pool = params[constants.LDP_POOL]
     rbd_name = unique_id[1]
 
@@ -2607,14 +2738,7 @@ class RADOSBlockDevice(BlockDev):
     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)
-
+    rbd_dev = self._VolumeToBlockdev(pool, name)
     if rbd_dev:
       # The mapping exists. Return it.
       return rbd_dev
@@ -2627,14 +2751,7 @@ class RADOSBlockDevice(BlockDev):
                   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)
-
+    rbd_dev = self._VolumeToBlockdev(pool, 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)
@@ -2642,16 +2759,93 @@ class RADOSBlockDevice(BlockDev):
     # The device was successfully mapped. Return it.
     return rbd_dev
 
+  @classmethod
+  def _VolumeToBlockdev(cls, pool, volume_name):
+    """Do the 'volume name'-to-'rbd block device' resolving.
+
+    @type pool: string
+    @param pool: RADOS pool to use
+    @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
+
+    """
+    try:
+      # Newer versions of the rbd tool support json output formatting. Use it
+      # if available.
+      showmap_cmd = [
+        constants.RBD_CMD,
+        "showmapped",
+        "-p",
+        pool,
+        "--format",
+        "json"
+        ]
+      result = utils.RunCmd(showmap_cmd)
+      if result.failed:
+        logging.error("rbd JSON output formatting returned error (%s): %s,"
+                      "falling back to plain output parsing",
+                      result.fail_reason, result.output)
+        raise RbdShowmappedJsonError
+
+      return cls._ParseRbdShowmappedJson(result.output, volume_name)
+    except RbdShowmappedJsonError:
+      # For older versions of rbd, we have to parse the plain / text output
+      # manually.
+      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)
+
+      return cls._ParseRbdShowmappedPlain(result.output, volume_name)
+
+  @staticmethod
+  def _ParseRbdShowmappedJson(output, volume_name):
+    """Parse the json output of `rbd showmapped'.
+
+    This method parses the json 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 json 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
+
+    """
+    try:
+      devices = serializer.LoadJson(output)
+    except ValueError, err:
+      _ThrowError("Unable to parse JSON data: %s" % err)
+
+    rbd_dev = None
+    for d in devices.values(): # pylint: disable=E1103
+      try:
+        name = d["name"]
+      except KeyError:
+        _ThrowError("'name' key missing from json object %s", devices)
+
+      if name == volume_name:
+        if rbd_dev is not None:
+          _ThrowError("rbd volume %s is mapped more than once", volume_name)
+
+        rbd_dev = d["device"]
+
+    return rbd_dev
+
   @staticmethod
-  def _ParseRbdShowmappedOutput(output, volume_name):
-    """Parse the output of `rbd showmapped'.
+  def _ParseRbdShowmappedPlain(output, volume_name):
+    """Parse the (plain / text) 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'
+    @param output: the plain text 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
@@ -2662,30 +2856,31 @@ class RADOSBlockDevice(BlockDev):
     volumefield = 2
     devicefield = 4
 
-    field_sep = "\t"
-
     lines = output.splitlines()
-    splitted_lines = map(lambda l: l.split(field_sep), lines)
 
-    # Check empty output.
+    # Try parsing the new output format (ceph >= 0.55).
+    splitted_lines = map(lambda l: l.split(), lines)
+
+    # Check for empty output.
     if not splitted_lines:
-      _ThrowError("rbd showmapped returned empty output")
+      return None
 
-    # Check showmapped header line, to determine number of fields.
+    # Check showmapped output, 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)
+      # Parsing the new format failed. Fallback to parsing the old output
+      # format (< 0.55).
+      splitted_lines = map(lambda l: l.split("\t"), lines)
+      if field_cnt != allfields:
+        _ThrowError("Cannot parse rbd showmapped output 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)
+      _ThrowError("rbd volume %s mapped more than once", volume_name)
 
     if matched_lines:
       # rbd block device found. Return it.
@@ -2726,13 +2921,7 @@ class RADOSBlockDevice(BlockDev):
     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)
+    rbd_dev = self._VolumeToBlockdev(pool, name)
 
     if rbd_dev:
       # The mapping exists. Unmap the rbd device.
@@ -2810,7 +2999,7 @@ class ExtStorageDevice(BlockDev):
     self.Attach()
 
   @classmethod
-  def Create(cls, unique_id, children, size, params):
+  def Create(cls, unique_id, children, size, params, excl_stor):
     """Create a new extstorage device.
 
     Provision a new volume using an extstorage provider, which will
@@ -2820,6 +3009,9 @@ class ExtStorageDevice(BlockDev):
     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
       raise errors.ProgrammerError("Invalid configuration data %s" %
                                    str(unique_id))
+    if excl_stor:
+      raise errors.ProgrammerError("extstorage device requested with"
+                                   " exclusive_storage")
 
     # Call the External Storage's create script,
     # to provision a new Volume inside the External Storage
@@ -3235,7 +3427,7 @@ def Assemble(disk, children):
   return device
 
 
-def Create(disk, children):
+def Create(disk, children, excl_stor):
   """Create a device.
 
   @type disk: L{objects.Disk}
@@ -3243,10 +3435,12 @@ def Create(disk, children):
   @type children: list of L{bdev.BlockDev}
   @param children: the list of block devices that are children of the device
                   represented by the disk parameter
+  @type excl_stor: boolean
+  @param excl_stor: Whether exclusive_storage is active
 
   """
   _VerifyDiskType(disk.dev_type)
   _VerifyDiskParams(disk)
   device = DEV_MAP[disk.dev_type].Create(disk.physical_id, children, disk.size,
-                                         disk.params)
+                                         disk.params, excl_stor)
   return device