'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,

this patch addresses the comments made in the thread, regarding 'rbd
showmapped' output handling. This patch makes Ganeti compatible with current
(bobtail), older (<=argonaut) and future versions of the ceph tools. :) 

Apparently, pylint has some trouble inferring the type of the
simplejson.loads() result, so, for the moment,  I have disabled the check.
I'm not sure if that's supposed to happen though.

I also restructured the code a bit, to make it somewhat cleaner, after the
changes in output handling.

Thanks,
Stratos

 lib/bdev.py |   90 ++++++++++++++++++++++++++++++++++++++++++++---------------
 1 file changed, 68 insertions(+), 22 deletions(-)

diff --git a/lib/bdev.py b/lib/bdev.py
index b221235..cbcc04e 100644
--- a/lib/bdev.py
+++ b/lib/bdev.py
@@ -39,6 +39,7 @@ from ganeti import compat
 from ganeti import netutils
 from ganeti import pathutils
 
+import simplejson
 
 # Size of reads in _CanReadDevice
 _DEVICE_READ_SIZE = 128 * 1024
@@ -2726,13 +2727,8 @@ 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.
@@ -2746,14 +2742,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)
@@ -2762,6 +2751,59 @@ class RADOSBlockDevice(BlockDev):
     return rbd_dev
 
   @staticmethod
+  def _VolumeToBlockdev(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
+
+    """
+    # 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:
+      # For older versions of rbd, we have to parse the 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)
+
+      rbd_dev = RADOSBlockDevice._ParseRbdShowmappedOutput(result.output,
+              volume_name)
+      return rbd_dev
+
+
+    try:
+      devices = simplejson.loads(result.output)
+    except simplejson.JSONDecodeError:
+      _ThrowError("Invalid json input %s!\n", result.output)
+
+    rbd_dev = None
+    # pylint: disable=E1103
+    for d in devices.itervalues():
+      if "name" not in d:
+        _ThrowError("'name' key missing from json object %s!\n",
+                    devices)
+
+      if d["name"] == volume_name:
+        if rbd_dev is not None:
+          _ThrowError("The rbd volume %s is mapped more than once."
+                      " This shouldn't happen, try to unmap the extra"
+                      " devices manually.", volume_name)
+
+        rbd_dev = d["device"]
+
+    return rbd_dev
+
+
+  @staticmethod
   def _ParseRbdShowmappedOutput(output, volume_name):
     """Parse the output of `rbd showmapped'.
 
@@ -2781,21 +2823,25 @@ 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.
     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 because its format"
+                    " seems to have changed; expected %s fields, found %s",
+                    allfields, field_cnt)
 
     matched_lines = \
       filter(lambda l: len(l) == allfields and l[volumefield] == volume_name,
-- 
1.7.10.4

Reply via email to