X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/23d95cff660da2d5bcbf8298ec65b55f0a4a7406..92158c35ec991cbf164f01f5ce8efbf897cd5c2e:/lib/bdev.py diff --git a/lib/bdev.py b/lib/bdev.py index 5613258..7226f1f 100644 --- a/lib/bdev.py +++ b/lib/bdev.py @@ -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 @@ -38,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. @@ -176,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): @@ -247,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, @@ -592,7 +602,7 @@ class LogicalVolume(BlockDev): stripes = min(current_pvs, desired_stripes) if excl_stor: - err_msgs = utils.LvmExclusiveCheckNodePvs(pvs_info) + (err_msgs, _) = utils.LvmExclusiveCheckNodePvs(pvs_info) if err_msgs: for m in err_msgs: logging.warning(m) @@ -634,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 @@ -666,35 +676,59 @@ 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 @@ -804,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: @@ -997,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 @@ -1014,7 +1050,7 @@ class LogicalVolume(BlockDev): _ThrowError("Can't grow LV %s: %s", self.dev_path, result.output) -class DRBD8Status(object): +class DRBD8Status(object): # pylint: disable=R0902 """A DRBD status representation class. Note that this doesn't support unconfigured devices (cs:Unconfigured). @@ -1099,6 +1135,7 @@ class DRBD8Status(object): self.is_diskless = self.ldisk == self.DS_DISKLESS self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE + self.peer_disk_uptodate = self.rdisk == self.DS_UPTODATE self.is_in_resync = self.cstatus in self.CSET_SYNC self.is_in_use = self.cstatus != self.CS_UNCONFIGURED @@ -2317,7 +2354,7 @@ class DRBD8(BaseDRBD): class FileStorage(BlockDev): """File device. - This class represents the a file storage backend device. + This class represents a file storage backend device. The unique_id for the file device is a (file_driver, file_path) tuple. @@ -2702,14 +2739,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 @@ -2722,14 +2752,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) @@ -2737,16 +2760,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 @@ -2757,30 +2857,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. @@ -2821,13 +2922,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.