diff --git a/lib/mcpu.py b/lib/mcpu.py index 8f4946932d0425546f7ade206d47be19c9ce34a0..2851f2977047af724eb1842b4cb73ae591753115 100644 --- a/lib/mcpu.py +++ b/lib/mcpu.py @@ -48,6 +48,19 @@ from ganeti import pathutils _OP_PREFIX = "Op" _LU_PREFIX = "LU" +#: LU classes which don't need to acquire the node allocation lock +#: (L{locking.NAL}) when they acquire all node or node resource locks +_NODE_ALLOC_WHITELIST = frozenset() + +#: LU classes which don't need to acquire the node allocation lock +#: (L{locking.NAL}) in the same mode (shared/exclusive) as the node +#: or node resource locks +_NODE_ALLOC_MODE_WHITELIST = frozenset([ + cmdlib.LUBackupExport, + cmdlib.LUBackupRemove, + cmdlib.LUOobCommand, + ]) + class LockAcquireTimeout(Exception): """Exception to report timeouts on acquiring locks. @@ -246,6 +259,44 @@ def _RpcResultsToHooksResults(rpc_results): for (node, rpc_res) in rpc_results.items()) +def _VerifyLocks(lu, glm, _mode_whitelist=_NODE_ALLOC_MODE_WHITELIST, + _nal_whitelist=_NODE_ALLOC_WHITELIST): + """Performs consistency checks on locks acquired by a logical unit. + + @type lu: L{cmdlib.LogicalUnit} + @param lu: Logical unit instance + @type glm: L{locking.GanetiLockManager} + @param glm: Lock manager + + """ + if not __debug__: + return + + have_nal = glm.check_owned(locking.LEVEL_NODE_ALLOC, locking.NAL) + + for level in [locking.LEVEL_NODE, locking.LEVEL_NODE_RES]: + # TODO: Verify using actual lock mode, not using LU variables + if level in lu.needed_locks: + share_node_alloc = lu.share_locks[locking.LEVEL_NODE_ALLOC] + share_level = lu.share_locks[level] + + if lu.__class__ in _mode_whitelist: + assert share_node_alloc != share_level, \ + "LU is whitelisted to use different modes for node allocation lock" + else: + assert bool(share_node_alloc) == bool(share_level), \ + ("Node allocation lock must be acquired using the same mode as nodes" + " and node resources") + + if lu.__class__ in _nal_whitelist: + assert not have_nal, \ + "LU is whitelisted for not acquiring the node allocation lock" + elif lu.needed_locks[level] == locking.ALL_SET or glm.owning_all(level): + assert have_nal, \ + ("Node allocation lock must be used if an LU acquires all nodes" + " or node resources") + + class Processor(object): """Object which runs OpCodes""" DISPATCH_TABLE = _ComputeDispatchTable() @@ -356,9 +407,13 @@ class Processor(object): given LU and its opcodes. """ + glm = self.context.glm adding_locks = level in lu.add_locks acquiring_locks = level in lu.needed_locks + if level not in locking.LEVELS: + _VerifyLocks(lu, glm) + if self._cbs: self._cbs.NotifyStart() @@ -405,7 +460,7 @@ class Processor(object): lu.remove_locks[level] = add_locks try: - self.context.glm.add(level, add_locks, acquired=1, shared=share) + glm.add(level, add_locks, acquired=1, shared=share) except errors.LockError: logging.exception("Detected lock error in level %s for locks" " %s, shared=%s", level, add_locks, share) @@ -418,10 +473,10 @@ class Processor(object): result = self._LockAndExecLU(lu, level + 1, calc_timeout) finally: if level in lu.remove_locks: - self.context.glm.remove(level, lu.remove_locks[level]) + glm.remove(level, lu.remove_locks[level]) finally: - if self.context.glm.is_owned(level): - self.context.glm.release(level) + if glm.is_owned(level): + glm.release(level) else: result = self._LockAndExecLU(lu, level + 1, calc_timeout) diff --git a/test/ganeti.mcpu_unittest.py b/test/ganeti.mcpu_unittest.py index 0678140390de541d020afc757c3b786555b1a605..33d2d670a119096ee89c30b9fa47ca943de66872 100755 --- a/test/ganeti.mcpu_unittest.py +++ b/test/ganeti.mcpu_unittest.py @@ -28,6 +28,7 @@ import itertools from ganeti import mcpu from ganeti import opcodes from ganeti import cmdlib +from ganeti import locking from ganeti import constants from ganeti.constants import \ LOCK_ATTEMPTS_TIMEOUT, \ @@ -166,5 +167,128 @@ class TestProcessResult(unittest.TestCase): self.assertEqual(op2.debug_level, 3) +class _FakeLuWithLocks: + def __init__(self, needed_locks, share_locks): + self.needed_locks = needed_locks + self.share_locks = share_locks + + +class _FakeGlm: + def __init__(self, owning_nal): + self._owning_nal = owning_nal + + def check_owned(self, level, names): + assert level == locking.LEVEL_NODE_ALLOC + assert names == locking.NAL + return self._owning_nal + + def owning_all(self, level): + return False + + +class TestVerifyLocks(unittest.TestCase): + def testNoLocks(self): + lu = _FakeLuWithLocks({}, {}) + glm = _FakeGlm(False) + mcpu._VerifyLocks(lu, glm, + _mode_whitelist=NotImplemented, + _nal_whitelist=NotImplemented) + + def testNotAllSameMode(self): + for level in [locking.LEVEL_NODE, locking.LEVEL_NODE_RES]: + lu = _FakeLuWithLocks({ + level: ["foo"], + }, { + level: 0, + locking.LEVEL_NODE_ALLOC: 0, + }) + glm = _FakeGlm(False) + mcpu._VerifyLocks(lu, glm, _mode_whitelist=[], _nal_whitelist=[]) + + def testDifferentMode(self): + for level in [locking.LEVEL_NODE, locking.LEVEL_NODE_RES]: + lu = _FakeLuWithLocks({ + level: ["foo"], + }, { + level: 0, + locking.LEVEL_NODE_ALLOC: 1, + }) + glm = _FakeGlm(False) + try: + mcpu._VerifyLocks(lu, glm, _mode_whitelist=[], _nal_whitelist=[]) + except AssertionError, err: + self.assertTrue("using the same mode as nodes" in str(err)) + else: + self.fail("Exception not raised") + + # Once more with the whitelist + mcpu._VerifyLocks(lu, glm, _mode_whitelist=[_FakeLuWithLocks], + _nal_whitelist=[]) + + def testSameMode(self): + for level in [locking.LEVEL_NODE, locking.LEVEL_NODE_RES]: + lu = _FakeLuWithLocks({ + level: ["foo"], + locking.LEVEL_NODE_ALLOC: locking.ALL_SET, + }, { + level: 1, + locking.LEVEL_NODE_ALLOC: 1, + }) + glm = _FakeGlm(True) + + try: + mcpu._VerifyLocks(lu, glm, _mode_whitelist=[_FakeLuWithLocks], + _nal_whitelist=[]) + except AssertionError, err: + self.assertTrue("whitelisted to use different modes" in str(err)) + else: + self.fail("Exception not raised") + + # Once more without the whitelist + mcpu._VerifyLocks(lu, glm, _mode_whitelist=[], _nal_whitelist=[]) + + def testAllWithoutAllocLock(self): + for level in [locking.LEVEL_NODE, locking.LEVEL_NODE_RES]: + lu = _FakeLuWithLocks({ + level: locking.ALL_SET, + }, { + level: 0, + locking.LEVEL_NODE_ALLOC: 0, + }) + glm = _FakeGlm(False) + try: + mcpu._VerifyLocks(lu, glm, _mode_whitelist=[], _nal_whitelist=[]) + except AssertionError, err: + self.assertTrue("allocation lock must be used if" in str(err)) + else: + self.fail("Exception not raised") + + # Once more with the whitelist + mcpu._VerifyLocks(lu, glm, _mode_whitelist=[], + _nal_whitelist=[_FakeLuWithLocks]) + + def testAllWithAllocLock(self): + for level in [locking.LEVEL_NODE, locking.LEVEL_NODE_RES]: + lu = _FakeLuWithLocks({ + level: locking.ALL_SET, + locking.LEVEL_NODE_ALLOC: locking.ALL_SET, + }, { + level: 0, + locking.LEVEL_NODE_ALLOC: 0, + }) + glm = _FakeGlm(True) + + try: + mcpu._VerifyLocks(lu, glm, _mode_whitelist=[], + _nal_whitelist=[_FakeLuWithLocks]) + except AssertionError, err: + self.assertTrue("whitelisted for not acquiring" in str(err)) + else: + self.fail("Exception not raised") + + # Once more without the whitelist + mcpu._VerifyLocks(lu, glm, _mode_whitelist=[], _nal_whitelist=[]) + + if __name__ == "__main__": testutils.GanetiTestProgram()