From 5520d04d9c527014473f8513857c016719e37fbc Mon Sep 17 00:00:00 2001
From: Thomas Thrainer <thomasth@google.com>
Date: Mon, 29 Apr 2013 13:51:24 +0200
Subject: [PATCH] Add `drbdsetup show` parser for DRBD 8.4

Common functionality between the DRBD 8.3 and DRBD 8.4 parser has been
extracted into BaseShowInfo. A test which verifies the behaviour is
included, but the DRBD84ShowInfo class is not yet used within the DRBD8
class.

Signed-off-by: Thomas Thrainer <thomasth@google.com>
Signed-off-by: Michele Tartara <mtartara@google.com>
Reviewed-by: Michele Tartara <mtartara@google.com>
---
 Makefile.am                           |   1 +
 lib/block/drbd.py                     |  19 +--
 lib/block/drbd_info.py                | 194 +++++++++++++++++---------
 test/data/bdev-drbd-8.4.txt           |  19 +++
 test/py/ganeti.block.bdev_unittest.py |  31 ++--
 5 files changed, 185 insertions(+), 79 deletions(-)
 create mode 100644 test/data/bdev-drbd-8.4.txt

diff --git a/Makefile.am b/Makefile.am
index 97dd9df35..2837fec9d 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1036,6 +1036,7 @@ TEST_FILES = \
 	test/hs/shelltests/htools-mon-collector.test \
 	test/data/bdev-drbd-8.0.txt \
 	test/data/bdev-drbd-8.3.txt \
+	test/data/bdev-drbd-8.4.txt \
 	test/data/bdev-drbd-disk.txt \
 	test/data/bdev-drbd-net-ip4.txt \
 	test/data/bdev-drbd-net-ip6.txt \
diff --git a/lib/block/drbd.py b/lib/block/drbd.py
index 34421994c..588976848 100644
--- a/lib/block/drbd.py
+++ b/lib/block/drbd.py
@@ -33,7 +33,7 @@ from ganeti import netutils
 from ganeti import objects
 from ganeti.block import base
 from ganeti.block.drbd_info import DRBD8Info
-from ganeti.block.drbd_info import DRBD8ShowInfo
+from ganeti.block.drbd_info import DRBD83ShowInfo
 
 
 # Size of reads in _CanReadDevice
@@ -452,7 +452,7 @@ class DRBD8(base.BlockDev):
                       minor, result.fail_reason, result.output)
 
     def _CheckNetworkConfig():
-      info = DRBD8ShowInfo.GetDevInfo(self._GetShowData(minor))
+      info = DRBD83ShowInfo.GetDevInfo(self._GetShowData(minor))
       if not "local_addr" in info or not "remote_addr" in info:
         raise utils.RetryAgain()
 
@@ -474,7 +474,7 @@ class DRBD8(base.BlockDev):
                       self._aminor)
     if len(devices) != 2:
       base.ThrowError("drbd%d: need two devices for AddChildren", self.minor)
-    info = DRBD8ShowInfo.GetDevInfo(self._GetShowData(self.minor))
+    info = DRBD83ShowInfo.GetDevInfo(self._GetShowData(self.minor))
     if "local_dev" in info:
       base.ThrowError("drbd%d: already attached to a local disk", self.minor)
     backend, meta = devices
@@ -497,7 +497,7 @@ class DRBD8(base.BlockDev):
       base.ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
                       self._aminor)
     # early return if we don't actually have backing storage
-    info = DRBD8ShowInfo.GetDevInfo(self._GetShowData(self.minor))
+    info = DRBD83ShowInfo.GetDevInfo(self._GetShowData(self.minor))
     if "local_dev" not in info:
       return
     if len(self._children) != 2:
@@ -849,7 +849,7 @@ class DRBD8(base.BlockDev):
     # pylint: disable=W0631
     net_data = (self._lhost, self._lport, self._rhost, self._rport)
     for minor in (self._aminor,):
-      info = DRBD8ShowInfo.GetDevInfo(self._GetShowData(minor))
+      info = DRBD83ShowInfo.GetDevInfo(self._GetShowData(minor))
       match_l = self._MatchesLocal(info)
       match_r = self._MatchesNet(info)
 
@@ -861,7 +861,8 @@ class DRBD8(base.BlockDev):
         # disk matches, but not attached to network, attach and recheck
         self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
                           hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
-        if self._MatchesNet(DRBD8ShowInfo.GetDevInfo(self._GetShowData(minor))):
+        if self._MatchesNet(DRBD83ShowInfo.GetDevInfo(
+            self._GetShowData(minor))):
           break
         else:
           base.ThrowError("drbd%d: network attach successful, but 'drbdsetup"
@@ -871,7 +872,8 @@ class DRBD8(base.BlockDev):
         # no local disk, but network attached and it matches
         self._AssembleLocal(minor, self._children[0].dev_path,
                             self._children[1].dev_path, self.size)
-        if self._MatchesNet(DRBD8ShowInfo.GetDevInfo(self._GetShowData(minor))):
+        if self._MatchesNet(DRBD83ShowInfo.GetDevInfo(
+            self._GetShowData(minor))):
           break
         else:
           base.ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
@@ -898,7 +900,8 @@ class DRBD8(base.BlockDev):
         # None)
         self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
                           hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
-        if self._MatchesNet(DRBD8ShowInfo.GetDevInfo(self._GetShowData(minor))):
+        if self._MatchesNet(DRBD83ShowInfo.GetDevInfo(
+            self._GetShowData(minor))):
           break
         else:
           base.ThrowError("drbd%d: network attach successful, but 'drbdsetup"
diff --git a/lib/block/drbd_info.py b/lib/block/drbd_info.py
index 4d1e14d66..991494cd6 100644
--- a/lib/block/drbd_info.py
+++ b/lib/block/drbd_info.py
@@ -255,69 +255,50 @@ class DRBD8Info(object):
     return DRBD8Info.CreateFromLines(lines)
 
 
-class DRBD8ShowInfo(object):
-  """Helper class which parses the output of drbdsetup show
+class BaseShowInfo(object):
+  """Base class for parsing the `drbdsetup show` output.
+
+  Holds various common pyparsing expressions which are used by subclasses. Also
+  provides caching of the constructed parser.
 
   """
   _PARSE_SHOW = None
 
-  @classmethod
-  def _GetShowParser(cls):
-    """Return a parser for `drbd show` output.
-
-    This will either create or return an already-created parser for the
-    output of the command `drbd show`.
-
-    """
-    if cls._PARSE_SHOW is not None:
-      return cls._PARSE_SHOW
-
-    # pyparsing setup
-    lbrace = pyp.Literal("{").suppress()
-    rbrace = pyp.Literal("}").suppress()
-    lbracket = pyp.Literal("[").suppress()
-    rbracket = pyp.Literal("]").suppress()
-    semi = pyp.Literal(";").suppress()
-    colon = pyp.Literal(":").suppress()
-    # this also converts the value to an int
-    number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
-
-    comment = pyp.Literal("#") + pyp.Optional(pyp.restOfLine)
-    defa = pyp.Literal("_is_default").suppress()
-    dbl_quote = pyp.Literal('"').suppress()
-
-    keyword = pyp.Word(pyp.alphanums + "-")
-
-    # value types
-    value = pyp.Word(pyp.alphanums + "_-/.:")
-    quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
-    ipv4_addr = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
-                 pyp.Word(pyp.nums + ".") + colon + number)
-    ipv6_addr = (pyp.Optional(pyp.Literal("ipv6")).suppress() +
-                 pyp.Optional(lbracket) + pyp.Word(pyp.hexnums + ":") +
-                 pyp.Optional(rbracket) + colon + number)
-    # meta device, extended syntax
-    meta_value = ((value ^ quoted) + lbracket + number + rbracket)
-    # device name, extended syntax
-    device_value = pyp.Literal("minor").suppress() + number
-
-    # a statement
-    stmt = (~rbrace + keyword + ~lbrace +
-            pyp.Optional(ipv4_addr ^ ipv6_addr ^ value ^ quoted ^ meta_value ^
-                         device_value) +
-            pyp.Optional(defa) + semi +
-            pyp.Optional(pyp.restOfLine).suppress())
-
-    # an entire section
-    section_name = pyp.Word(pyp.alphas + "_")
-    section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
-
-    bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
-    bnf.ignore(comment)
-
-    cls._PARSE_SHOW = bnf
-
-    return bnf
+  # pyparsing setup
+  _lbrace = pyp.Literal("{").suppress()
+  _rbrace = pyp.Literal("}").suppress()
+  _lbracket = pyp.Literal("[").suppress()
+  _rbracket = pyp.Literal("]").suppress()
+  _semi = pyp.Literal(";").suppress()
+  _colon = pyp.Literal(":").suppress()
+  # this also converts the value to an int
+  _number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
+
+  _comment = pyp.Literal("#") + pyp.Optional(pyp.restOfLine)
+  _defa = pyp.Literal("_is_default").suppress()
+  _dbl_quote = pyp.Literal('"').suppress()
+
+  _keyword = pyp.Word(pyp.alphanums + "-")
+
+  # value types
+  _value = pyp.Word(pyp.alphanums + "_-/.:")
+  _quoted = _dbl_quote + pyp.CharsNotIn('"') + _dbl_quote
+  _ipv4_addr = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
+                pyp.Word(pyp.nums + ".") + _colon + _number)
+  _ipv6_addr = (pyp.Optional(pyp.Literal("ipv6")).suppress() +
+                pyp.Optional(_lbracket) + pyp.Word(pyp.hexnums + ":") +
+                pyp.Optional(_rbracket) + _colon + _number)
+  # meta device, extended syntax
+  _meta_value = ((_value ^ _quoted) + _lbracket + _number + _rbracket)
+  # device name, extended syntax
+  _device_value = pyp.Literal("minor").suppress() + _number
+
+  # a statement
+  _stmt = (~_rbrace + _keyword + ~_lbrace +
+           pyp.Optional(_ipv4_addr ^ _ipv6_addr ^ _value ^ _quoted ^
+                        _meta_value ^ _device_value) +
+           pyp.Optional(_defa) + _semi +
+           pyp.Optional(pyp.restOfLine).suppress())
 
   @classmethod
   def GetDevInfo(cls, show_data):
@@ -336,9 +317,8 @@ class DRBD8ShowInfo(object):
       - remote_addr
 
     """
-    retval = {}
     if not show_data:
-      return retval
+      return {}
 
     try:
       # run pyparse
@@ -346,8 +326,49 @@ class DRBD8ShowInfo(object):
     except pyp.ParseException, err:
       base.ThrowError("Can't parse drbdsetup show output: %s", str(err))
 
-    # and massage the results into our desired format
-    for section in results:
+    return cls._TransformParseResult(results)
+
+  @classmethod
+  def _TransformParseResult(cls, parse_result):
+    raise NotImplementedError
+
+  @classmethod
+  def _GetShowParser(cls):
+    """Return a parser for `drbd show` output.
+
+    This will either create or return an already-created parser for the
+    output of the command `drbd show`.
+
+    """
+    if cls._PARSE_SHOW is None:
+      cls._PARSE_SHOW = cls._ConstructShowParser()
+
+    return cls._PARSE_SHOW
+
+  @classmethod
+  def _ConstructShowParser(cls):
+    raise NotImplementedError
+
+
+class DRBD83ShowInfo(BaseShowInfo):
+  @classmethod
+  def _ConstructShowParser(cls):
+    # an entire section
+    section_name = pyp.Word(pyp.alphas + "_")
+    section = section_name + \
+              cls._lbrace + \
+              pyp.ZeroOrMore(pyp.Group(cls._stmt)) + \
+              cls._rbrace
+
+    bnf = pyp.ZeroOrMore(pyp.Group(section ^ cls._stmt))
+    bnf.ignore(cls._comment)
+
+    return bnf
+
+  @classmethod
+  def _TransformParseResult(cls, parse_result):
+    retval = {}
+    for section in parse_result:
       sname = section[0]
       if sname == "_this_host":
         for lst in section[1:]:
@@ -363,3 +384,50 @@ class DRBD8ShowInfo(object):
           if lst[0] == "address":
             retval["remote_addr"] = tuple(lst[1:])
     return retval
+
+
+class DRBD84ShowInfo(BaseShowInfo):
+  @classmethod
+  def _ConstructShowParser(cls):
+    # an entire section (sections can be nested in DRBD 8.4, and there exist
+    # sections like "volume 0")
+    section_name = pyp.Word(pyp.alphas + "_") + \
+                   pyp.Optional(pyp.Word(pyp.nums)).suppress() # skip volume idx
+    section = pyp.Forward()
+    # pylint: disable=W0106
+    section << (section_name +
+                cls._lbrace +
+                pyp.ZeroOrMore(pyp.Group(cls._stmt ^ section)) +
+                cls._rbrace)
+
+    resource_name = pyp.Word(pyp.alphanums + "_-.")
+    resource = (pyp.Literal("resource") + resource_name).suppress() + \
+               cls._lbrace + \
+               pyp.ZeroOrMore(pyp.Group(section)) + \
+               cls._rbrace
+
+    resource.ignore(cls._comment)
+
+    return resource
+
+  @classmethod
+  def _TransformParseResult(cls, parse_result):
+    retval = {}
+    for section in parse_result:
+      sname = section[0]
+      if sname == "_this_host":
+        for lst in section[1:]:
+          if lst[0] == "address":
+            retval["local_addr"] = tuple(lst[1:])
+          elif lst[0] == "volume":
+            for inner in lst[1:]:
+              if inner[0] == "disk" and len(inner) == 2:
+                retval["local_dev"] = inner[1]
+              elif inner[0] == "meta-disk" and len(inner) == 3:
+                retval["meta_dev"] = inner[1]
+                retval["meta_index"] = inner[2]
+      elif sname == "_remote_host":
+        for lst in section[1:]:
+          if lst[0] == "address":
+            retval["remote_addr"] = tuple(lst[1:])
+    return retval
diff --git a/test/data/bdev-drbd-8.4.txt b/test/data/bdev-drbd-8.4.txt
new file mode 100644
index 000000000..024d5c757
--- /dev/null
+++ b/test/data/bdev-drbd-8.4.txt
@@ -0,0 +1,19 @@
+resource test0 {
+    options {
+    }
+    net {
+    }
+    _remote_host {
+        address                 ipv4 192.0.2.2:11000;
+    }
+    _this_host {
+        address                 ipv4 192.0.2.1:11000;
+        volume 0 {
+            device                      minor 0;
+            disk                        "/dev/xenvg/test.data";
+            meta-disk                   "/dev/xenvg/test.meta" [ 0 ];
+            disk {
+            }
+        }
+    }
+}
diff --git a/test/py/ganeti.block.bdev_unittest.py b/test/py/ganeti.block.bdev_unittest.py
index 8955a9f01..5a27b06b8 100755
--- a/test/py/ganeti.block.bdev_unittest.py
+++ b/test/py/ganeti.block.bdev_unittest.py
@@ -103,14 +103,18 @@ class TestDRBD8Runner(testutils.GanetiTestCase):
       )
     return retval
 
-  def testParserCreation(self):
+  def testParser83Creation(self):
     """Test drbdsetup show parser creation"""
-    drbd_info.DRBD8ShowInfo._GetShowParser()
+    drbd_info.DRBD83ShowInfo._GetShowParser()
+
+  def testParser84Creation(self):
+    """Test drbdsetup show parser creation"""
+    drbd_info.DRBD84ShowInfo._GetShowParser()
 
   def testParser80(self):
     """Test drbdsetup show parser for disk and network version 8.0"""
     data = testutils.ReadTestData("bdev-drbd-8.0.txt")
-    result = drbd_info.DRBD8ShowInfo.GetDevInfo(data)
+    result = drbd_info.DRBD83ShowInfo.GetDevInfo(data)
     self.failUnless(self._has_disk(result, "/dev/xenvg/test.data",
                                    "/dev/xenvg/test.meta"),
                     "Wrong local disk info")
@@ -121,18 +125,29 @@ class TestDRBD8Runner(testutils.GanetiTestCase):
   def testParser83(self):
     """Test drbdsetup show parser for disk and network version 8.3"""
     data = testutils.ReadTestData("bdev-drbd-8.3.txt")
-    result = drbd_info.DRBD8ShowInfo.GetDevInfo(data)
+    result = drbd_info.DRBD83ShowInfo.GetDevInfo(data)
     self.failUnless(self._has_disk(result, "/dev/xenvg/test.data",
                                    "/dev/xenvg/test.meta"),
                     "Wrong local disk info")
     self.failUnless(self._has_net(result, ("192.0.2.1", 11000),
                                   ("192.0.2.2", 11000)),
-                    "Wrong network info (8.0.x)")
+                    "Wrong network info (8.3.x)")
+
+  def testParser84(self):
+    """Test drbdsetup show parser for disk and network version 8.4"""
+    data = testutils.ReadTestData("bdev-drbd-8.4.txt")
+    result = drbd_info.DRBD84ShowInfo.GetDevInfo(data)
+    self.failUnless(self._has_disk(result, "/dev/xenvg/test.data",
+                                   "/dev/xenvg/test.meta"),
+                    "Wrong local disk info")
+    self.failUnless(self._has_net(result, ("192.0.2.1", 11000),
+                                  ("192.0.2.2", 11000)),
+                    "Wrong network info (8.4.x)")
 
   def testParserNetIP4(self):
     """Test drbdsetup show parser for IPv4 network"""
     data = testutils.ReadTestData("bdev-drbd-net-ip4.txt")
-    result = drbd_info.DRBD8ShowInfo.GetDevInfo(data)
+    result = drbd_info.DRBD83ShowInfo.GetDevInfo(data)
     self.failUnless(("local_dev" not in result and
                      "meta_dev" not in result and
                      "meta_index" not in result),
@@ -144,7 +159,7 @@ class TestDRBD8Runner(testutils.GanetiTestCase):
   def testParserNetIP6(self):
     """Test drbdsetup show parser for IPv6 network"""
     data = testutils.ReadTestData("bdev-drbd-net-ip6.txt")
-    result = drbd_info.DRBD8ShowInfo.GetDevInfo(data)
+    result = drbd_info.DRBD83ShowInfo.GetDevInfo(data)
     self.failUnless(("local_dev" not in result and
                      "meta_dev" not in result and
                      "meta_index" not in result),
@@ -156,7 +171,7 @@ class TestDRBD8Runner(testutils.GanetiTestCase):
   def testParserDisk(self):
     """Test drbdsetup show parser for disk"""
     data = testutils.ReadTestData("bdev-drbd-disk.txt")
-    result = drbd_info.DRBD8ShowInfo.GetDevInfo(data)
+    result = drbd_info.DRBD83ShowInfo.GetDevInfo(data)
     self.failUnless(self._has_disk(result, "/dev/xenvg/test.data",
                                    "/dev/xenvg/test.meta"),
                     "Wrong local disk info")
-- 
GitLab