Exemple #1
0
    def setUp(self):
        """Set up test data"""
        testutils.GanetiTestCase.setUp(self)

        self.volume_name = "31225655-5775-4356-c212-e8b1e137550a.disk0"
        self.test_unique_id = ("ganeti", self.volume_name)
        self.test_params = {constants.LDP_STRIPES: 1}
        self.pv_info_return = [
            objects.LvmPvInfo(name="/dev/sda5",
                              vg_name="xenvg",
                              size=3500000.00,
                              free=5000000.00,
                              attributes="wz--n-",
                              lv_list=[])
        ]
        self.pv_info_invalid = [
            objects.LvmPvInfo(name="/dev/s:da5",
                              vg_name="xenvg",
                              size=3500000.00,
                              free=5000000.00,
                              attributes="wz--n-",
                              lv_list=[])
        ]
        self.pv_info_no_space = [
            objects.LvmPvInfo(name="/dev/sda5",
                              vg_name="xenvg",
                              size=3500000.00,
                              free=0.00,
                              attributes="wz--n-",
                              lv_list=[])
        ]
Exemple #2
0
  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", lvfield])
    except errors.GenericError as 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 = []
    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
      # 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
 def _GenerateRandomPvInfo(rnd, name, vg):
   # Granularity is .01 MiB
   size = rnd.randint(1024 * 100, 10 * 1024 * 1024 * 100)
   if rnd.choice([False, True]):
     free = float(rnd.randint(0, size)) / 100.0
   else:
     free = float(size) / 100.0
   size = float(size) / 100.0
   attr = "a-"
   return objects.LvmPvInfo(name=name, vg_name=vg, size=size, free=free,
                            attributes=attr)
Exemple #4
0
class LogicalVolume(base.BlockDev):
  """Logical Volume block device.

  """
  _VALID_NAME_RE = re.compile("^[a-zA-Z0-9+_.-]*$")
  _PARSE_PV_DEV_RE = re.compile(r"^([^ ()]+)\([0-9]+\)$")
  _INVALID_NAMES = compat.UniqueFrozenset([".", "..", "snapshot", "pvmove"])
  _INVALID_SUBSTRINGS = compat.UniqueFrozenset(["_mlog", "_mimage"])

  def __init__(self, unique_id, children, size, params, dyn_params, *args):
    """Attaches to a LV device.

    The unique_id is a tuple (vg_name, lv_name)

    """
    super(LogicalVolume, self).__init__(unique_id, children, size, params,
                                        dyn_params, *args)
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
      raise ValueError("Invalid configuration data %s" % str(unique_id))
    self._vg_name, self._lv_name = unique_id
    self._ValidateName(self._vg_name)
    self._ValidateName(self._lv_name)
    self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
    self._degraded = True
    self.major = self.minor = self.pe_size = self.stripe_count = None
    self.pv_names = 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, spindles, params, excl_stor,
             dyn_params, *args):
    """Create a new logical volume.

    """
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
      raise errors.ProgrammerError("Invalid configuration data %s" %
                                   str(unique_id))
    vg_name, lv_name = unique_id
    cls._ValidateName(vg_name)
    cls._ValidateName(lv_name)
    pvs_info = cls.GetPVInfo([vg_name])
    if not pvs_info:
      if excl_stor:
        msg = "No (empty) PVs found"
      else:
        msg = "Can't compute PV info for vg %s" % vg_name
      base.ThrowError(msg)
    pvs_info.sort(key=(lambda pv: pv.free), reverse=True)

    pvlist = [pv.name for pv in pvs_info]
    if compat.any(":" in v for v in pvlist):
      base.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'")

    current_pvs = len(pvlist)
    desired_stripes = params[constants.LDP_STRIPES]
    stripes = min(current_pvs, desired_stripes)

    if excl_stor:
      if spindles is None:
        base.ThrowError("Unspecified number of spindles: this is required"
                        "when exclusive storage is enabled, try running"
                        " gnt-cluster repair-disk-sizes")
      (err_msgs, _) = utils.LvmExclusiveCheckNodePvs(pvs_info)
      if err_msgs:
        for m in err_msgs:
          logging.warning(m)
      req_pvs = cls._ComputeNumPvs(size, pvs_info)
      if spindles < req_pvs:
        base.ThrowError("Requested number of spindles (%s) is not enough for"
                        " a disk of %d MB (at least %d spindles needed)",
                        spindles, size, req_pvs)
      else:
        req_pvs = spindles
      pvlist = cls._GetEmptyPvNames(pvs_info, req_pvs)
      current_pvs = len(pvlist)
      if current_pvs < req_pvs:
        base.ThrowError("Not enough empty PVs (spindles) to create a disk of %d"
                        " MB: %d available, %d needed",
                        size, current_pvs, req_pvs)
      assert current_pvs == len(pvlist)
      # We must update stripes to be sure to use all the desired spindles
      stripes = current_pvs
      if stripes > desired_stripes:
        # Don't warn when lowering stripes, as it's no surprise
        logging.warning("Using %s stripes instead of %s, to be able to use"
                        " %s spindles", stripes, desired_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:
        base.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:
        break
    if result.failed:
      base.ThrowError("LV create failed (%s): %s",
                      result.fail_reason, result.output)
    return LogicalVolume(unique_id, children, size, params, dyn_params, *args)

  @staticmethod
  def _GetVolumeInfo(lvm_cmd, fields):
    """Returns LVM Volume infos using lvm_cmd

    @param lvm_cmd: Should be one of "pvs", "vgs" or "lvs"
    @param fields: Fields to return
    @return: A list of dicts each with the parsed fields

    """
    if not fields:
      raise errors.ProgrammerError("No fields specified")

    sep = "|"
    cmd = [lvm_cmd, "--noheadings", "--nosuffix", "--units=m", "--unbuffered",
           "--separator=%s" % sep, "-o%s" % ",".join(fields)]

    result = utils.RunCmd(cmd)
    if result.failed:
      raise errors.CommandError("Can't get the volume information: %s - %s" %
                                (result.fail_reason, result.output))

    data = []
    for line in result.stdout.splitlines():
      splitted_fields = line.strip().split(sep)

      if len(fields) != len(splitted_fields):
        raise errors.CommandError("Can't parse %s output: line '%s'" %
                                  (lvm_cmd, line))

      data.append(splitted_fields)

    return data

  @classmethod
  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", 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 = []
    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
      # 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