commit b5d48e87510374658fce37421af0072d04dacc94
Author: Bernardo Dal Seno <bdalseno@google.com>
Date:   Wed May 22 00:33:08 2013 +0200

    Attaching a logical volume builds a list of PVs
    
    When an LV gets attached, the list of the PVs used by the LV is built. This
    will be used to count spindles for exclusive_storage, but it could also be
    useful to optimize disk growing and snapshotting.
    
    Signed-off-by: Bernardo Dal Seno <bdalseno@google.com>
    Reviewed-by: Helga Velroyen <helgav@google.com>

diff --git a/lib/storage/bdev.py b/lib/storage/bdev.py
index 4c754aa..24465ff 100644
--- a/lib/storage/bdev.py
+++ b/lib/storage/bdev.py
@@ -172,6 +172,7 @@ class LogicalVolume(base.BlockDev):
 
   """
   _VALID_NAME_RE = re.compile("^[a-zA-Z0-9+_.-]*$")
+  _PARSE_PV_DEV_RE = re.compile("^([^ ()]+)\([0-9]+\)$")
   _INVALID_NAMES = compat.UniqueFrozenset([".", "..", "snapshot", "pvmove"])
   _INVALID_SUBSTRINGS = compat.UniqueFrozenset(["_mlog", "_mimage"])
 
@@ -190,6 +191,7 @@ class LogicalVolume(base.BlockDev):
     self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
     self._degraded = True
     self.major = self.minor = self.pe_size = self.stripe_count = None
+    self.pv_names = None
     self.Attach()
 
   @staticmethod
@@ -501,10 +503,10 @@ class LogicalVolume(base.BlockDev):
 
     """
     elems = line.strip().rstrip(sep).split(sep)
-    if len(elems) != 5:
-      base.ThrowError("Can't parse LVS output, len(%s) != 5", str(elems))
+    if len(elems) != 6:
+      base.ThrowError("Can't parse LVS output, len(%s) != 6", str(elems))
 
-    (status, major, minor, pe_size, stripes) = elems
+    (status, major, minor, pe_size, stripes, pvs) = elems
     if len(status) < 6:
       base.ThrowError("lvs lv_attr is not at least 6 characters (%s)", status)
 
@@ -524,17 +526,26 @@ class LogicalVolume(base.BlockDev):
     except (TypeError, ValueError), err:
       base.ThrowError("Can't parse the number of stripes: %s", err)
 
-    return (status, major, minor, pe_size, stripes)
+    pv_names = []
+    for pv in pvs.split(","):
+      m = re.match(cls._PARSE_PV_DEV_RE, pv)
+      if not m:
+        base.ThrowError("Can't parse this device list: %s", pvs)
+      pv_names.append(m.group(1))
+    assert len(pv_names) > 0
+
+    return (status, major, minor, pe_size, stripes, pv_names)
 
   @classmethod
   def _GetLvInfo(cls, dev_path, _run_cmd=utils.RunCmd):
     """Get info about the given existing LV to be used.
 
     """
-    result = _run_cmd(["lvs", "--noheadings", "--separator=,",
+    sep = "|"
+    result = _run_cmd(["lvs", "--noheadings", "--separator=%s" % sep,
                        "--units=k", "--nosuffix",
                        "-olv_attr,lv_kernel_major,lv_kernel_minor,"
-                       "vg_extent_size,stripes", dev_path])
+                       "vg_extent_size,stripes,devices", dev_path])
     if result.failed:
       base.ThrowError("Can't find LV %s: %s, %s",
                       dev_path, result.fail_reason, result.output)
@@ -547,7 +558,12 @@ class LogicalVolume(base.BlockDev):
     if not out: # totally empty result? splitlines() returns at least
                 # one line for any non-empty string
       base.ThrowError("Can't parse LVS output, no lines? Got '%s'", str(out))
-    return cls._ParseLvInfoLine(out[-1], ",")
+    pv_names = set()
+    for line in out:
+      (status, major, minor, pe_size, stripes, more_pvs) = \
+        cls._ParseLvInfoLine(line, sep)
+      pv_names.update(more_pvs)
+    return (status, major, minor, pe_size, stripes, pv_names)
 
   def Attach(self):
     """Attach to an existing LV.
@@ -559,7 +575,7 @@ class LogicalVolume(base.BlockDev):
     """
     self.attached = False
     try:
-      (status, major, minor, pe_size, stripes) = \
+      (status, major, minor, pe_size, stripes, pv_names) = \
         self._GetLvInfo(self.dev_path)
     except errors.BlockDeviceError:
       return False
@@ -570,6 +586,7 @@ class LogicalVolume(base.BlockDev):
     self.stripe_count = stripes
     self._degraded = status[0] == "v" # virtual volume, i.e. doesn't backing
                                       # storage
+    self.pv_names = pv_names
     self.attached = True
     return True
 
diff --git a/test/py/ganeti.storage.bdev_unittest.py b/test/py/ganeti.storage.bdev_unittest.py
index 4235c57..f623cef 100755
--- a/test/py/ganeti.storage.bdev_unittest.py
+++ b/test/py/ganeti.storage.bdev_unittest.py
@@ -305,31 +305,36 @@ class TestLogicalVolume(unittest.TestCase):
   def testParseLvInfoLine(self):
     """Tests for LogicalVolume._ParseLvInfoLine."""
     broken_lines = [
-      "  toomuch#-wi-ao#253#3#4096.00#2",
-      "  -wi-ao#253#3#4096.00",
-      "  -wi-a#253#3#4096.00#2",
-      "  -wi-ao#25.3#3#4096.00#2",
-      "  -wi-ao#twenty#3#4096.00#2",
-      "  -wi-ao#253#3.1#4096.00#2",
-      "  -wi-ao#253#three#4096.00#2",
-      "  -wi-ao#253#3#four#2",
-      "  -wi-ao#253#3#4096..00#2",
-      "  -wi-ao#253#3#4096.00#2.0",
-      "  -wi-ao#253#3#4096.00#two",
+      "  toomuch#-wi-ao#253#3#4096.00#2#/dev/abc(20)",
+      "  -wi-ao#253#3#4096.00#/dev/abc(20)",
+      "  -wi-a#253#3#4096.00#2#/dev/abc(20)",
+      "  -wi-ao#25.3#3#4096.00#2#/dev/abc(20)",
+      "  -wi-ao#twenty#3#4096.00#2#/dev/abc(20)",
+      "  -wi-ao#253#3.1#4096.00#2#/dev/abc(20)",
+      "  -wi-ao#253#three#4096.00#2#/dev/abc(20)",
+      "  -wi-ao#253#3#four#2#/dev/abc(20)",
+      "  -wi-ao#253#3#4096..00#2#/dev/abc(20)",
+      "  -wi-ao#253#3#4096.00#2.0#/dev/abc(20)",
+      "  -wi-ao#253#3#4096.00#two#/dev/abc(20)",
       ]
     for broken in broken_lines:
       self.assertRaises(errors.BlockDeviceError,
                         bdev.LogicalVolume._ParseLvInfoLine, broken, "#")
 
+    # Examples of good lines from "lvs":
+    #  -wi-ao|253|3|4096.00|2|/dev/sdb(144),/dev/sdc(0)
+    #  -wi-a-|253|4|4096.00|1|/dev/sdb(208)
     true_out = [
-      ("-wi-ao", 253, 3, 4096.00, 2),
-      ("-wi-a-", 253, 7, 4096.00, 4),
-      ("-ri-a-", 253, 4, 4.00, 5),
-      ("-wc-ao", 15, 18, 4096.00, 32),
+      ("-wi-ao", 253, 3, 4096.00, 2, ["/dev/abc"]),
+      ("-wi-a-", 253, 7, 4096.00, 4, ["/dev/abc"]),
+      ("-ri-a-", 253, 4, 4.00, 5, ["/dev/abc", "/dev/def"]),
+      ("-wc-ao", 15, 18, 4096.00, 32, ["/dev/abc", "/dev/def", "/dev/ghi0"]),
       ]
     for exp in true_out:
-      for sep in "#;|,":
-        lvs_line = sep.join(("  %s", "%d", "%d", "%.2f", "%d")) % exp
+      for sep in "#;|":
+        pvs = ",".join("%s(%s)" % (d, i * 12) for (i, d) in enumerate(exp[-1]))
+        lvs_line = (sep.join(("  %s", "%d", "%d", "%.2f", "%d", "%s")) %
+                    (exp[0:-1] + (pvs,)))
         parsed = bdev.LogicalVolume._ParseLvInfoLine(lvs_line, sep)
         self.assertEqual(parsed, exp)
 
@@ -351,19 +356,36 @@ class TestLogicalVolume(unittest.TestCase):
                       "fake_path", _run_cmd=self._FakeRunCmd(True, ""))
     self.assertRaises(errors.BlockDeviceError, bdev.LogicalVolume._GetLvInfo,
                       "fake_path", _run_cmd=self._FakeRunCmd(True, "BadStdOut"))
-    good_line = "  -wi-ao,253,3,4096.00,2"
+    good_line = "  -wi-ao|253|3|4096.00|2|/dev/abc(20)"
     fake_cmd = self._FakeRunCmd(True, good_line)
     good_res = bdev.LogicalVolume._GetLvInfo("fake_path", _run_cmd=fake_cmd)
-    # Only the last line should be parsed and taken into account
+    # If the same line is repeated, the result should be the same
     for lines in [
       [good_line] * 2,
       [good_line] * 3,
-      ["bad line", good_line],
       ]:
       fake_cmd = self._FakeRunCmd(True, "\n".join(lines))
       same_res = bdev.LogicalVolume._GetLvInfo("fake_path", fake_cmd)
       self.assertEqual(same_res, good_res)
 
+    # Complex multi-line examples
+    one_line = "  -wi-ao|253|3|4096.00|2|/dev/sda(20),/dev/sdb(50),/dev/sdc(0)"
+    fake_cmd = self._FakeRunCmd(True, one_line)
+    one_res = bdev.LogicalVolume._GetLvInfo("fake_path", _run_cmd=fake_cmd)
+    # These should give the same results
+    for multi_lines in [
+      ("  -wi-ao|253|3|4096.00|2|/dev/sda(30),/dev/sdb(50)\n"
+       "  -wi-ao|253|3|4096.00|2|/dev/sdb(200),/dev/sdc(300)"),
+      ("  -wi-ao|253|3|4096.00|2|/dev/sda(0)\n"
+       "  -wi-ao|253|3|4096.00|2|/dev/sdb(20)\n"
+       "  -wi-ao|253|3|4096.00|2|/dev/sdc(30)"),
+      ("  -wi-ao|253|3|4096.00|2|/dev/sda(20)\n"
+       "  -wi-ao|253|3|4096.00|2|/dev/sdb(50),/dev/sdc(0)"),
+      ]:
+      fake_cmd = self._FakeRunCmd(True, multi_lines)
+      multi_res = bdev.LogicalVolume._GetLvInfo("fake_path", _run_cmd=fake_cmd)
+      self.assertEqual(multi_res, one_res)
+
 
 if __name__ == "__main__":
   testutils.GanetiTestProgram()
