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=[]) ]
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)
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