This patch optimizes unnecessary calls to the lvs command when running gnt-cluster verify (among other operations) by caching multiple series of lvs statements into a single operation and storing the results in a dictionary with device paths used as keys.
Signed-off-by: Federico Morg Pareschi <m...@google.com> --- lib/backend.py | 27 ++++++-- lib/storage/base.py | 4 +- lib/storage/bdev.py | 94 ++++++++++++++------------- lib/storage/drbd.py | 4 +- lib/storage/extstorage.py | 7 +- lib/storage/filestorage.py | 4 +- lib/storage/gluster.py | 4 +- test/py/ganeti.storage.bdev_unittest.py | 110 ++++++++++++++------------------ 8 files changed, 132 insertions(+), 122 deletions(-) diff --git a/lib/backend.py b/lib/backend.py index 56b5b18..ba71fbe 100644 --- a/lib/backend.py +++ b/lib/backend.py @@ -3804,9 +3804,13 @@ def BlockdevGetmirrorstatusMulti(disks): """ result = [] + lvs_cache = None + is_plain_disk = any([_CheckForPlainDisk(d) for d in disks]) + if is_plain_disk: + lvs_cache = bdev.LogicalVolume._GetLvGlobalInfo() for disk in disks: try: - rbd = _RecursiveFindBD(disk) + rbd = _RecursiveFindBD(disk, lvs_cache=lvs_cache) if rbd is None: result.append((False, "Can't find device %s" % disk)) continue @@ -3823,7 +3827,22 @@ def BlockdevGetmirrorstatusMulti(disks): return result -def _RecursiveFindBD(disk): +def _CheckForPlainDisk(disk, found=False): + """Check within a disk and its children if there is a plain disk type. + + @type disk: L{objects.Disk} + @param disk: the disk we are checking + @rtype: bool + @return: whether or not there is a plain disk type + + """ + found |= disk.dev_type == constants.DT_PLAIN + if disk.children: + return any([_CheckForPlainDisk(d, found) for d in disk.children]) + return found + + +def _RecursiveFindBD(disk, lvs_cache=None): """Check if a device is activated. If so, return information about the real device. @@ -3838,9 +3857,9 @@ def _RecursiveFindBD(disk): children = [] if disk.children: for chdisk in disk.children: - children.append(_RecursiveFindBD(chdisk)) + children.append(_RecursiveFindBD(chdisk, lvs_cache=lvs_cache)) - return bdev.FindDevice(disk, children) + return bdev.FindDevice(disk, children, lvs_cache=lvs_cache) def _OpenRealBD(disk): diff --git a/lib/storage/base.py b/lib/storage/base.py index dcafb41..588caf2 100644 --- a/lib/storage/base.py +++ b/lib/storage/base.py @@ -82,7 +82,7 @@ class BlockDev(object): """ # pylint: disable=W0613 - def __init__(self, unique_id, children, size, params, dyn_params, *args): + def __init__(self, unique_id, children, size, params, dyn_params, **kwargs): self._children = children self.dev_path = None self.unique_id = unique_id @@ -121,7 +121,7 @@ class BlockDev(object): @classmethod def Create(cls, unique_id, children, size, spindles, params, excl_stor, - dyn_params, *args): + dyn_params, **kwargs): """Create the device. If the device cannot be created, it will return None diff --git a/lib/storage/bdev.py b/lib/storage/bdev.py index e3a48de..2d97284 100644 --- a/lib/storage/bdev.py +++ b/lib/storage/bdev.py @@ -78,14 +78,14 @@ class LogicalVolume(base.BlockDev): _INVALID_NAMES = compat.UniqueFrozenset([".", "..", "snapshot", "pvmove"]) _INVALID_SUBSTRINGS = compat.UniqueFrozenset(["_mlog", "_mimage"]) - def __init__(self, unique_id, children, size, params, dyn_params, *args): + def __init__(self, unique_id, children, size, params, dyn_params, **kwargs): """Attaches to a LV device. The unique_id is a tuple (vg_name, lv_name) """ super(LogicalVolume, self).__init__(unique_id, children, size, params, - dyn_params, *args) + dyn_params, **kwargs) if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: raise ValueError("Invalid configuration data %s" % str(unique_id)) self._vg_name, self._lv_name = unique_id @@ -95,7 +95,12 @@ class LogicalVolume(base.BlockDev): self._degraded = True self.major = self.minor = self.pe_size = self.stripe_count = None self.pv_names = None - self.Attach() + lvs_cache = kwargs.get("lvs_cache") + if lvs_cache: + lv_info = lvs_cache.get(self.dev_path) + self.Attach(lv_info=lv_info) + else: + self.Attach() @staticmethod def _GetStdPvSize(pvs_info): @@ -436,9 +441,9 @@ class LogicalVolume(base.BlockDev): self._lv_name = new_name self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name) - @classmethod - def _ParseLvInfoLine(cls, line, sep): - """Parse one line of the lvs output used in L{_GetLvInfo}. + @staticmethod + def _ParseLvInfoLine(line, sep): + """Parse one line of the lvs output used in L{_GetLvGlobalInfo}. """ elems = line.strip().split(sep) @@ -447,13 +452,13 @@ class LogicalVolume(base.BlockDev): # separator to the right of the output. The PV info might be empty for # thin volumes, so stripping off the separators might cut off the last # empty element - do this instead. - if len(elems) == 7 and elems[-1] == "": + if len(elems) == 8 and elems[-1] == "": elems.pop() - if len(elems) != 6: - base.ThrowError("Can't parse LVS output, len(%s) != 6", str(elems)) + if len(elems) != 7: + base.ThrowError("Can't parse LVS output, len(%s) != 7", str(elems)) - (status, major, minor, pe_size, stripes, pvs) = elems + (path, 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) @@ -476,43 +481,41 @@ class LogicalVolume(base.BlockDev): pv_names = [] if pvs != "": for pv in pvs.split(","): - m = re.match(cls._PARSE_PV_DEV_RE, pv) + m = re.match(LogicalVolume._PARSE_PV_DEV_RE, pv) if not m: base.ThrowError("Can't parse this device list: %s", pvs) pv_names.append(m.group(1)) - return (status, major, minor, pe_size, stripes, pv_names) + return (path, (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. + @staticmethod + def _GetLvGlobalInfo(_run_cmd=utils.RunCmd): + """Obtain the current state of the existing LV disks. + + @return: a dict containing the state of each disk with the disk path as key """ sep = "|" result = _run_cmd(["lvs", "--noheadings", "--separator=%s" % sep, "--units=k", "--nosuffix", - "-olv_attr,lv_kernel_major,lv_kernel_minor," - "vg_extent_size,stripes,devices", dev_path]) + "-olv_path,lv_attr,lv_kernel_major,lv_kernel_minor," + "vg_extent_size,stripes,devices"]) if result.failed: - base.ThrowError("Can't find LV %s: %s, %s", - dev_path, result.fail_reason, result.output) - # the output can (and will) have multiple lines for multi-segment - # LVs, as the 'stripes' parameter is a segment one, so we take - # only the last entry, which is the one we're interested in; note - # that with LVM2 anyway the 'stripes' value must be constant - # across segments, so this is a no-op actually + logging.warning("lvs command failed, the LV cache will be empty!") + logging.info("lvs failure: %r", result.stderr) + return {} out = result.stdout.splitlines() - 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)) - pv_names = set() + if not out: + logging.warning("lvs command returned an empty output, the LV cache will" + "be empty!") + return {} + info = {} 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) + (key, value) = LogicalVolume._ParseLvInfoLine(line, sep) + info[key] = value + return info - def Attach(self): + def Attach(self, lv_info=None): """Attach to an existing LV. This method will try to see if an existing and active LV exists @@ -521,11 +524,11 @@ class LogicalVolume(base.BlockDev): """ self.attached = False - try: + if not lv_info: (status, major, minor, pe_size, stripes, pv_names) = \ - self._GetLvInfo(self.dev_path) - except errors.BlockDeviceError: - return False + LogicalVolume._GetLvGlobalInfo()[self.dev_path] + else: + (status, major, minor, pe_size, stripes, pv_names) = lv_info self.major = major self.minor = minor @@ -747,14 +750,14 @@ class PersistentBlockDevice(base.BlockDev): For the time being, pathnames are required to lie under /dev. """ - def __init__(self, unique_id, children, size, params, dyn_params, *args): + def __init__(self, unique_id, children, size, params, dyn_params, **kwargs): """Attaches to a static block device. The unique_id is a path under /dev. """ super(PersistentBlockDevice, self).__init__(unique_id, children, size, - params, dyn_params, *args) + params, dyn_params, **kwargs) if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: raise ValueError("Invalid configuration data %s" % str(unique_id)) self.dev_path = unique_id[1] @@ -803,7 +806,6 @@ class PersistentBlockDevice(base.BlockDev): def Attach(self): """Attach to an existing block device. - """ self.attached = False try: @@ -870,12 +872,12 @@ class RADOSBlockDevice(base.BlockDev): this to be functional. """ - def __init__(self, unique_id, children, size, params, dyn_params, *args): + def __init__(self, unique_id, children, size, params, dyn_params, **kwargs): """Attaches to an rbd device. """ super(RADOSBlockDevice, self).__init__(unique_id, children, size, params, - dyn_params, *args) + dyn_params, **kwargs) if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: raise ValueError("Invalid configuration data %s" % str(unique_id)) @@ -1305,7 +1307,7 @@ def _VerifyDiskParams(disk): missing) -def FindDevice(disk, children): +def FindDevice(disk, children, **kwargs): """Search for an existing, assembled device. This will succeed only if the device exists and is assembled, but it @@ -1321,7 +1323,7 @@ def FindDevice(disk, children): _VerifyDiskType(disk.dev_type) device = DEV_MAP[disk.dev_type](disk.logical_id, children, disk.size, disk.params, disk.dynamic_params, - disk.name, disk.uuid) + name=disk.name, uuid=disk.uuid, **kwargs) if not device.attached: return None return device @@ -1344,7 +1346,7 @@ def Assemble(disk, children): _VerifyDiskParams(disk) device = DEV_MAP[disk.dev_type](disk.logical_id, children, disk.size, disk.params, disk.dynamic_params, - disk.name, disk.uuid) + name=disk.name, uuid=disk.uuid) device.Assemble() return device @@ -1368,7 +1370,7 @@ def Create(disk, children, excl_stor): device = DEV_MAP[disk.dev_type].Create(disk.logical_id, children, disk.size, disk.spindles, disk.params, excl_stor, disk.dynamic_params, - disk.name, disk.uuid) + name=disk.name, uuid=disk.uuid) return device # Please keep this at the bottom of the file for visibility. diff --git a/lib/storage/drbd.py b/lib/storage/drbd.py index 5c4817b..081e96a 100644 --- a/lib/storage/drbd.py +++ b/lib/storage/drbd.py @@ -183,7 +183,7 @@ class DRBD8Dev(base.BlockDev): # timeout constants _NET_RECONFIG_TIMEOUT = 60 - def __init__(self, unique_id, children, size, params, dyn_params, *args): + def __init__(self, unique_id, children, size, params, dyn_params, **kwargs): if children and children.count(None) > 0: children = [] if len(children) not in (0, 2): @@ -210,7 +210,7 @@ class DRBD8Dev(base.BlockDev): logging.info("drbd%s: Ignoring unreadable meta device", self._aminor) children = [] super(DRBD8Dev, self).__init__(unique_id, children, size, params, - dyn_params, *args) + dyn_params, **kwargs) self.major = self._DRBD_MAJOR info = DRBD8.GetProcInfo() diff --git a/lib/storage/extstorage.py b/lib/storage/extstorage.py index 311662f..32f7f45 100644 --- a/lib/storage/extstorage.py +++ b/lib/storage/extstorage.py @@ -52,13 +52,14 @@ class ExtStorageDevice(base.BlockDev): handling of the externally provided block devices. """ - def __init__(self, unique_id, children, size, params, dyn_params, *args): + def __init__(self, unique_id, children, size, params, dyn_params, **kwargs): """Attaches to an extstorage block device. """ super(ExtStorageDevice, self).__init__(unique_id, children, size, params, - dyn_params, *args) - (self.name, self.uuid) = args + dyn_params, **kwargs) + self.name = kwargs["name"] + self.uuid = kwargs["uuid"] if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: raise ValueError("Invalid configuration data %s" % str(unique_id)) diff --git a/lib/storage/filestorage.py b/lib/storage/filestorage.py index 74234e8..ba89975 100644 --- a/lib/storage/filestorage.py +++ b/lib/storage/filestorage.py @@ -190,14 +190,14 @@ class FileStorage(base.BlockDev): The unique_id for the file device is a (file_driver, file_path) tuple. """ - def __init__(self, unique_id, children, size, params, dyn_params, *args): + def __init__(self, unique_id, children, size, params, dyn_params, **kwargs): """Initalizes a file device backend. """ if children: raise errors.BlockDeviceError("Invalid setup for file device") super(FileStorage, self).__init__(unique_id, children, size, params, - dyn_params, *args) + dyn_params, **kwargs) if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: raise ValueError("Invalid configuration data %s" % str(unique_id)) self.driver = unique_id[0] diff --git a/lib/storage/gluster.py b/lib/storage/gluster.py index 6418c9d..6aab127 100644 --- a/lib/storage/gluster.py +++ b/lib/storage/gluster.py @@ -293,7 +293,7 @@ class GlusterStorage(base.BlockDev): The unique_id for the file device is a (file_driver, file_path) tuple. """ - def __init__(self, unique_id, children, size, params, dyn_params, *args): + def __init__(self, unique_id, children, size, params, dyn_params, **kwargs): """Initalizes a file device backend. """ @@ -316,7 +316,7 @@ class GlusterStorage(base.BlockDev): self.file = None super(GlusterStorage, self).__init__(unique_id, children, size, - params, dyn_params, *args) + params, dyn_params, **kwargs) self.Attach() diff --git a/test/py/ganeti.storage.bdev_unittest.py b/test/py/ganeti.storage.bdev_unittest.py index 8b6106c..89f3b49 100755 --- a/test/py/ganeti.storage.bdev_unittest.py +++ b/test/py/ganeti.storage.bdev_unittest.py @@ -239,38 +239,43 @@ class TestLogicalVolume(unittest.TestCase): def testParseLvInfoLine(self): """Tests for LogicalVolume._ParseLvInfoLine.""" broken_lines = [ - " 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)", + " toomuch#devpath#-wi-ao#253#3#4096.00#2#/dev/abc(20)", + " devpath#-wi-ao#253#3#4096.00#/dev/abc(20)", + " devpath#-wi-a#253#3#4096.00#2#/dev/abc(20)", + " devpath#-wi-ao#25.3#3#4096.00#2#/dev/abc(20)", + " devpath#-wi-ao#twenty#3#4096.00#2#/dev/abc(20)", + " devpath#-wi-ao#253#3.1#4096.00#2#/dev/abc(20)", + " devpath#-wi-ao#253#three#4096.00#2#/dev/abc(20)", + " devpath#-wi-ao#253#3#four#2#/dev/abc(20)", + " devpath#-wi-ao#253#3#4096..00#2#/dev/abc(20)", + " devpath#-wi-ao#253#3#4096.00#2.0#/dev/abc(20)", + " devpath#-wi-ao#253#3#4096.00#two#/dev/abc(20)", + " devpath#-wi-ao#253#3#4096.00#2#/dev/abc20", ] 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) + # + # /dev/something|-wi-ao|253|3|4096.00|2|/dev/sdb(144),/dev/sdc(0) + # /dev/somethingelse|-wi-a-|253|4|4096.00|1|/dev/sdb(208) true_out = [ - ("-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"]), - # Physical devices might be missing with thin volumes - ("twc-ao", 15, 18, 4096.00, 32, []), - ] + ("/dev/path", ("-wi-ao", 253, 3, 4096.00, 2, ["/dev/abc"])), + ("/dev/path", ("-wi-a-", 253, 7, 4096.00, 4, ["/dev/abc"])), + ("/dev/path", ("-ri-a-", 253, 4, 4.00, 5, ["/dev/abc", "/dev/def"])), + ("/dev/path", ("-wc-ao", 15, 18, 4096.00, 32, + ["/dev/abc", "/dev/def", "/dev/ghi0"])), + # Physical devices might be missing with thin volumes + ("/dev/path", ("twc-ao", 15, 18, 4096.00, 32, [])), + ] for exp in true_out: 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,))) + devpath = exp[0] + lvs = exp[1] + pvs = ",".join("%s(%s)" % (d, i * 12) for (i, d) in enumerate(lvs[-1])) + lvs_line = (sep.join((" %s", "%s", "%d", "%d", "%.2f", "%d", "%s")) % + ((devpath,) + lvs[0:-1] + (pvs,))) parsed = bdev.LogicalVolume._ParseLvInfoLine(lvs_line, sep) self.assertEqual(parsed, exp) @@ -283,44 +288,27 @@ class TestLogicalVolume(unittest.TestCase): return lambda cmd: utils.RunResult(exit_code, None, stdout, "", cmd, utils.process._TIMEOUT_NONE, 5) - def testGetLvInfo(self): - """Tests for LogicalVolume._GetLvInfo.""" - self.assertRaises(errors.BlockDeviceError, bdev.LogicalVolume._GetLvInfo, - "fake_path", - _run_cmd=self._FakeRunCmd(False, "Fake error msg")) - self.assertRaises(errors.BlockDeviceError, bdev.LogicalVolume._GetLvInfo, - "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|/dev/abc(20)" - fake_cmd = self._FakeRunCmd(True, good_line) - good_res = bdev.LogicalVolume._GetLvInfo("fake_path", _run_cmd=fake_cmd) - # If the same line is repeated, the result should be the same - for lines in [ - [good_line] * 2, - [good_line] * 3, - ]: - 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) + def testGetLvGlobalInfo(self): + """Tests for LogicalVolume._GetLvGlobalInfo.""" + + good_lines="/dev/1|-wi-ao|253|3|4096.00|2|/dev/sda(20)\n" \ + "/dev/2|-wi-ao|253|3|4096.00|2|/dev/sda(21)\n" + expected_output = {"/dev/1": ("-wi-ao", 253, 3, 4096, 2, ["/dev/sda"]), + "/dev/2": ("-wi-ao", 253, 3, 4096, 2, ["/dev/sda"])} + + self.assertEqual({}, + bdev.LogicalVolume._GetLvGlobalInfo( + _run_cmd=self._FakeRunCmd(False, "Fake error msg"))) + self.assertEqual({}, + bdev.LogicalVolume._GetLvGlobalInfo( + _run_cmd=self._FakeRunCmd(True, ""))) + self.assertRaises(errors.BlockDeviceError, + bdev.LogicalVolume._GetLvGlobalInfo, + _run_cmd=self._FakeRunCmd(True, "BadStdOut")) + + fake_cmd = self._FakeRunCmd(True, good_lines) + good_res = bdev.LogicalVolume._GetLvGlobalInfo(_run_cmd=fake_cmd) + self.assertEqual(expected_output, good_res) @testutils.patch_object(bdev.LogicalVolume, "Attach") def testLogicalVolumeImport(self, attach_mock): -- 2.8.0.rc3.226.g39d4020