#
#
-# 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
import pyparsing as pyp
import os
import logging
+import math
from ganeti import utils
from ganeti import errors
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.
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):
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,
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
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.
"""
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]
_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:
@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
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
# (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
"""
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:
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]
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
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
"""
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())
_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
@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))
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):
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.
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]
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
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)
# 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
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.
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.
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
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
return device
-def Create(disk, children):
+def Create(disk, children, excl_stor):
"""Create a device.
@type disk: L{objects.Disk}
@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