from ganeti import objects
+# Size of reads in _CanReadDevice
+_DEVICE_READ_SIZE = 128 * 1024
+
+
def _IgnoreError(fn, *args, **kwargs):
"""Executes the given function, ignoring BlockDeviceErrors.
fn(*args, **kwargs)
return True
except errors.BlockDeviceError, err:
- logging.warning("Caught BlockDeviceError but ignoring: %s" % str(err))
+ logging.warning("Caught BlockDeviceError but ignoring: %s", str(err))
return False
raise errors.BlockDeviceError(msg)
+def _CanReadDevice(path):
+ """Check if we can read from the given device.
+
+ This tries to read the first 128k of the device.
+
+ """
+ try:
+ utils.ReadFile(path, size=_DEVICE_READ_SIZE)
+ return True
+ except EnvironmentError:
+ logging.warning("Can't read from device %s", path, exc_info=True)
+ return False
+
+
class BlockDev(object):
"""Block device abstract class.
"""Logical Volume block device.
"""
+ _VALID_NAME_RE = re.compile("^[a-zA-Z0-9+_.-]*$")
+ _INVALID_NAMES = frozenset([".", "..", "snapshot", "pvmove"])
+ _INVALID_SUBSTRINGS = frozenset(["_mlog", "_mimage"])
+
def __init__(self, unique_id, children, size):
"""Attaches to a LV device.
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.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
+ 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.Attach()
raise errors.ProgrammerError("Invalid configuration data %s" %
str(unique_id))
vg_name, lv_name = unique_id
- pvs_info = cls.GetPVInfo(vg_name)
+ cls._ValidateName(vg_name)
+ 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)
pvs_info.sort()
pvlist = [ pv[1] for pv in pvs_info ]
if utils.any(pvlist, lambda v: ":" in v):
- _ThrowError("Some of your PVs have invalid character ':'"
- " in their name")
+ _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[0] for pv in pvs_info ])
current_pvs = len(pvlist)
stripes = min(current_pvs, constants.LVM_STRIPECOUNT)
return LogicalVolume(unique_id, children, size)
@staticmethod
- def GetPVInfo(vg_name):
+ def GetPVInfo(vg_names, filter_allocatable=True):
"""Get the free space info for PVs in a volume group.
- @param vg_name: the volume group name
+ @param vg_names: list of volume group names, if empty all will be returned
+ @param filter_allocatable: whether to skip over unallocatable PVs
@rtype: list
@return: list of tuples (free_space, name) with free_space in mebibytes
if len(fields) != 4:
logging.error("Can't parse pvs output: line '%s'", line)
return None
- # skip over pvs from another vg or ones which are not allocatable
- if fields[1] != vg_name or fields[3][0] != 'a':
+ # (possibly) skip over pvs which are not allocatable
+ if filter_allocatable and fields[3][0] != 'a':
+ continue
+ # (possibly) skip over pvs which are not in the right volume group(s)
+ if vg_names and fields[1] not in vg_names:
continue
- data.append((float(fields[2]), fields[0]))
+ data.append((float(fields[2]), fields[0], fields[1]))
return data
+ @classmethod
+ def _ValidateName(cls, name):
+ """Validates that a given name is valid as VG or LV name.
+
+ The list of valid characters and restricted names is taken out of
+ the lvm(8) manpage, with the simplification that we enforce both
+ VG and LV restrictions on the names.
+
+ """
+ if (not cls._VALID_NAME_RE.match(name) or
+ name in cls._INVALID_NAMES or
+ utils.any(cls._INVALID_SUBSTRINGS, lambda x: x in name)):
+ _ThrowError("Invalid LVM name '%s'", name)
+
def Remove(self):
"""Remove this logical volume.
if result.failed:
_ThrowError("Failed to rename the logical volume: %s", result.output)
self._lv_name = new_name
- self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
+ self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
def Attach(self):
"""Attach to an existing LV.
try:
major = int(major)
minor = int(minor)
- except ValueError, err:
+ except (TypeError, ValueError), err:
logging.error("lvs major/minor cannot be parsed: %s", str(err))
try:
snap = LogicalVolume((self._vg_name, snap_name), None, size)
_IgnoreError(snap.Remove)
- pvs_info = self.GetPVInfo(self._vg_name)
+ pvs_info = self.GetPVInfo([self._vg_name])
if not pvs_info:
_ThrowError("Can't compute PV info for vg %s", self._vg_name)
pvs_info.sort()
pvs_info.reverse()
- free_size, pv_name = pvs_info[0]
+ free_size, _, _ = pvs_info[0]
if free_size < size:
_ThrowError("Not enough free space: required %s,"
" available %s", size, free_size)
self.est_time = None
-class BaseDRBD(BlockDev):
+class BaseDRBD(BlockDev): # pylint: disable-msg=W0223
"""Base DRBD class.
This class contains a few bits of common functionality between the
"""
_VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)"
r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
+ _VALID_LINE_RE = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
+ _UNUSED_LINE_RE = re.compile("^ *([0-9]+): cs:Unconfigured$")
_DRBD_MAJOR = 147
_ST_UNCONFIGURED = "Unconfigured"
_ThrowError("Can't read any data from %s", filename)
return data
- @staticmethod
- def _MassageProcData(data):
+ @classmethod
+ def _MassageProcData(cls, data):
"""Transform the output of _GetProdData into a nicer form.
@return: a dictionary of minor: joined lines from /proc/drbd
for that minor
"""
- lmatch = re.compile("^ *([0-9]+):.*$")
results = {}
old_minor = old_line = None
for line in data:
if not line: # completely empty lines, as can be returned by drbd8.0+
continue
- lresult = lmatch.match(line)
+ lresult = cls._VALID_LINE_RE.match(line)
if lresult is not None:
if old_minor is not None:
results[old_minor] = old_line
data = cls._GetProcData()
used_devs = {}
- valid_line = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
for line in data:
- match = valid_line.match(line)
+ match = cls._VALID_LINE_RE.match(line)
if not match:
continue
minor = int(match.group(1))
result.fail_reason, result.output)
try:
sectors = int(result.stdout)
- except ValueError:
+ except (TypeError, ValueError):
_ThrowError("Invalid output from blockdev: '%s'", result.stdout)
bytes = sectors * 512
if bytes < 128 * 1024 * 1024: # less than 128MiB
def __init__(self, unique_id, children, size):
if children and children.count(None) > 0:
children = []
+ if len(children) not in (0, 2):
+ raise ValueError("Invalid configuration data %s" % str(children))
+ if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
+ raise ValueError("Invalid configuration data %s" % str(unique_id))
+ (self._lhost, self._lport,
+ self._rhost, self._rport,
+ self._aminor, self._secret) = unique_id
+ if children:
+ if not _CanReadDevice(children[1].dev_path):
+ logging.info("drbd%s: Ignoring unreadable meta device", self._aminor)
+ children = []
super(DRBD8, self).__init__(unique_id, children, size)
self.major = self._DRBD_MAJOR
version = self._GetVersion()
" usage: kernel is %s.%s, ganeti wants 8.x",
version['k_major'], version['k_minor'])
- if len(children) not in (0, 2):
- raise ValueError("Invalid configuration data %s" % str(children))
- if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
- raise ValueError("Invalid configuration data %s" % str(unique_id))
- (self._lhost, self._lport,
- self._rhost, self._rport,
- self._aminor, self._secret) = unique_id
if (self._lhost is not None and self._lhost == self._rhost and
self._lport == self._rport):
raise ValueError("Invalid configuration data, same local/remote %s" %
"""
data = cls._GetProcData()
- unused_line = re.compile("^ *([0-9]+): cs:Unconfigured$")
- used_line = re.compile("^ *([0-9]+): cs:")
highest = None
for line in data:
- match = unused_line.match(line)
+ match = cls._UNUSED_LINE_RE.match(line)
if match:
return int(match.group(1))
- match = used_line.match(line)
+ match = cls._VALID_LINE_RE.match(line)
if match:
minor = int(match.group(1))
highest = max(highest, minor)
"--create-device"]
if size:
args.extend(["-d", "%sm" % size])
+ if not constants.DRBD_BARRIERS: # disable barriers, if configured so
+ version = cls._GetVersion()
+ # various DRBD versions support different disk barrier options;
+ # what we aim here is to revert back to the 'drain' method of
+ # disk flushes and to disable metadata barriers, in effect going
+ # back to pre-8.0.7 behaviour
+ vmaj = version['k_major']
+ vmin = version['k_minor']
+ vrel = version['k_point']
+ assert vmaj == 8
+ if vmin == 0: # 8.0.x
+ if vrel >= 12:
+ args.extend(['-i', '-m'])
+ elif vmin == 2: # 8.2.x
+ if vrel >= 7:
+ args.extend(['-i', '-m'])
+ elif vmaj >= 3: # 8.3.x or newer
+ args.extend(['-i', '-a', 'm'])
result = utils.RunCmd(args)
if result.failed:
_ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
the attach if can return success.
"""
+ # TODO: Rewrite to not use a for loop just because there is 'break'
+ # pylint: disable-msg=W0631
net_data = (self._lhost, self._lport, self._rhost, self._rport)
for minor in (self._aminor,):
info = self._GetDevInfo(self._GetShowData(minor))
if err.errno != errno.ENOENT:
_ThrowError("Can't remove file '%s': %s", self.dev_path, err)
+ def Rename(self, new_id):
+ """Renames the file.
+
+ """
+ # TODO: implement rename for file-based storage
+ _ThrowError("Rename is not supported for file-based storage")
+
+ def Grow(self, amount):
+ """Grow the file
+
+ @param amount: the amount (in mebibytes) to grow with
+
+ """
+ # TODO: implement grow for file-based storage
+ _ThrowError("Grow not supported for file-based storage")
+
def Attach(self):
"""Attach to an existing file.
DEV_MAP = {
constants.LD_LV: LogicalVolume,
constants.LD_DRBD8: DRBD8,
- constants.LD_FILE: FileStorage,
}
+if constants.ENABLE_FILE_STORAGE:
+ DEV_MAP[constants.LD_FILE] = FileStorage
+
def FindDevice(dev_type, unique_id, children, size):
"""Search for an existing, assembled device.