'rbd showmapped' output formatting differs between older and newer versions of the ceph tools. Try to use json output formatting if available (currently available only in the ceph master branch). For bobtail, argonaut and older releases fallback to manually parsing the 'rbd showmapped' output, handling the differences in the output format for each rbd version correctly.
Signed-off-by: Stratos Psomadakis <[email protected]> --- Hi, the style issues should be resolved now (hopefully). I also restructured the code, as per Guido's and Iustin's suggestions (raise exception and log, when falling back to plain output parsing), and switched to ganeti.serializer, instead of simplejson. Thanks, Stratos lib/bdev.py | 143 +++++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 105 insertions(+), 38 deletions(-) diff --git a/lib/bdev.py b/lib/bdev.py index b221235..8580ae1 100644 --- a/lib/bdev.py +++ b/lib/bdev.py @@ -38,11 +38,20 @@ from ganeti import objects from ganeti import compat from ganeti import netutils from ganeti import pathutils +from ganeti import serializer +from simplejson import JSONDecodeError # Size of reads in _CanReadDevice _DEVICE_READ_SIZE = 128 * 1024 +class RbdShowmappedJsonError(Exception): + """`rbd showmmapped' json formatting error Exception class. + + """ + def __init__(self): + Exception.__init__(self) + logging.warning("rbd json formatting failed, falling back to plain") def _IgnoreError(fn, *args, **kwargs): """Executes the given function, ignoring BlockDeviceErrors. @@ -2726,14 +2735,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 +2748,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,50 +2756,128 @@ 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: + return cls._ParseRbdShowmappedJson(pool, volume_name) + except RbdShowmappedJsonError: + return cls._ParseRbdShowmappedPlain(pool, volume_name) + + @staticmethod + def _ParseRbdShowmappedJson(pool, volume_name): + """Parse the json output of `rbd showmapped'. + + This method parses the json output of `rbd showmapped', if available, and + returns the rbd block device path (e.g. /dev/rbd0) that matches the given + rbd volume. + + @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 + + """ + # 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(result.output) + raise RbdShowmappedJsonError + + try: + devices = serializer.LoadJson(result.output) + except JSONDecodeError: + _ThrowError("Unable to parse JSON data") + + 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(pool, 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' + @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 """ + # 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) + + output = result.output allfields = 5 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 emptmy output. if not splitted_lines: - _ThrowError("rbd showmapped returned empty output") + return None # Check showmapped header line, 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 +2918,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. -- 1.7.10.4
