diff --git a/lib/bdev.py b/lib/bdev.py index b2212356bfc5a4936677ca8bf8cff00f4829e2c5..080880cafa017992cbfb7a162b6b12441f84fc1d 100644 --- a/lib/bdev.py +++ b/lib/bdev.py @@ -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. @@ -2726,14 +2734,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 @@ -2746,14 +2747,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) @@ -2761,16 +2755,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 @@ -2781,30 +2852,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. @@ -2845,13 +2917,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.