root / test / py / cmdlib / testsupport / cmdlib_testcase.py @ 317a3fdb
History | View | Annotate | Download (13.2 kB)
1 |
#
|
---|---|
2 |
#
|
3 |
|
4 |
# Copyright (C) 2013 Google Inc.
|
5 |
#
|
6 |
# This program is free software; you can redistribute it and/or modify
|
7 |
# it under the terms of the GNU General Public License as published by
|
8 |
# the Free Software Foundation; either version 2 of the License, or
|
9 |
# (at your option) any later version.
|
10 |
#
|
11 |
# This program is distributed in the hope that it will be useful, but
|
12 |
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
13 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
14 |
# General Public License for more details.
|
15 |
#
|
16 |
# You should have received a copy of the GNU General Public License
|
17 |
# along with this program; if not, write to the Free Software
|
18 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
19 |
# 02110-1301, USA.
|
20 |
|
21 |
|
22 |
"""Main module of the cmdlib test framework"""
|
23 |
|
24 |
|
25 |
import inspect |
26 |
import mock |
27 |
import re |
28 |
import traceback |
29 |
|
30 |
from cmdlib.testsupport.config_mock import ConfigMock |
31 |
from cmdlib.testsupport.iallocator_mock import patchIAllocator |
32 |
from cmdlib.testsupport.lock_manager_mock import LockManagerMock |
33 |
from cmdlib.testsupport.netutils_mock import patchNetutils, \ |
34 |
SetupDefaultNetutilsMock
|
35 |
from cmdlib.testsupport.processor_mock import ProcessorMock |
36 |
from cmdlib.testsupport.rpc_runner_mock import CreateRpcRunnerMock, \ |
37 |
RpcResultsBuilder, patchRpc, SetupDefaultRpcModuleMock |
38 |
from cmdlib.testsupport.ssh_mock import patchSsh |
39 |
|
40 |
from ganeti.cmdlib.base import LogicalUnit |
41 |
from ganeti import errors |
42 |
from ganeti import locking |
43 |
from ganeti import objects |
44 |
from ganeti import opcodes |
45 |
from ganeti import runtime |
46 |
|
47 |
import testutils |
48 |
|
49 |
|
50 |
class GanetiContextMock(object): |
51 |
# pylint: disable=W0212
|
52 |
cfg = property(fget=lambda self: self._test_case.cfg) |
53 |
# pylint: disable=W0212
|
54 |
glm = property(fget=lambda self: self._test_case.glm) |
55 |
# pylint: disable=W0212
|
56 |
rpc = property(fget=lambda self: self._test_case.rpc) |
57 |
|
58 |
def __init__(self, test_case): |
59 |
self._test_case = test_case
|
60 |
|
61 |
def AddNode(self, node, ec_id): |
62 |
self._test_case.cfg.AddNode(node, ec_id)
|
63 |
self._test_case.glm.add(locking.LEVEL_NODE, node.uuid)
|
64 |
self._test_case.glm.add(locking.LEVEL_NODE_RES, node.uuid)
|
65 |
|
66 |
def ReaddNode(self, node): |
67 |
pass
|
68 |
|
69 |
def RemoveNode(self, node): |
70 |
self._test_case.cfg.RemoveNode(node.uuid)
|
71 |
self._test_case.glm.remove(locking.LEVEL_NODE, node.uuid)
|
72 |
self._test_case.glm.remove(locking.LEVEL_NODE_RES, node.uuid)
|
73 |
|
74 |
|
75 |
class MockLU(LogicalUnit): |
76 |
def BuildHooksNodes(self): |
77 |
pass
|
78 |
|
79 |
def BuildHooksEnv(self): |
80 |
pass
|
81 |
|
82 |
|
83 |
# pylint: disable=R0904
|
84 |
class CmdlibTestCase(testutils.GanetiTestCase): |
85 |
"""Base class for cmdlib tests.
|
86 |
|
87 |
This class sets up a mocked environment for the execution of
|
88 |
L{ganeti.cmdlib.base.LogicalUnit} subclasses.
|
89 |
|
90 |
The environment can be customized via the following fields:
|
91 |
|
92 |
* C{cfg}: @see L{ConfigMock}
|
93 |
* C{glm}: @see L{LockManagerMock}
|
94 |
* C{rpc}: @see L{CreateRpcRunnerMock}
|
95 |
* C{iallocator_cls}: @see L{patchIAllocator}
|
96 |
* C{mcpu}: @see L{ProcessorMock}
|
97 |
* C{netutils_mod}: @see L{patchNetutils}
|
98 |
* C{ssh_mod}: @see L{patchSsh}
|
99 |
|
100 |
"""
|
101 |
|
102 |
REMOVE = object()
|
103 |
|
104 |
cluster = property(fget=lambda self: self.cfg.GetClusterInfo(), |
105 |
doc="Cluster configuration object")
|
106 |
master = property(fget=lambda self: self.cfg.GetMasterNodeInfo(), |
107 |
doc="Master node")
|
108 |
master_uuid = property(fget=lambda self: self.cfg.GetMasterNode(), |
109 |
doc="Master node UUID")
|
110 |
# pylint: disable=W0212
|
111 |
group = property(fget=lambda self: self._GetDefaultGroup(), |
112 |
doc="Default node group")
|
113 |
|
114 |
os = property(fget=lambda self: self.cfg.GetDefaultOs(), |
115 |
doc="Default OS")
|
116 |
os_name_variant = property(
|
117 |
fget=lambda self: self.os.name + objects.OS.VARIANT_DELIM + |
118 |
self.os.supported_variants[0], |
119 |
doc="OS name and variant string")
|
120 |
|
121 |
def setUp(self): |
122 |
super(CmdlibTestCase, self).setUp() |
123 |
self._iallocator_patcher = None |
124 |
self._netutils_patcher = None |
125 |
self._ssh_patcher = None |
126 |
self._rpc_patcher = None |
127 |
|
128 |
try:
|
129 |
runtime.InitArchInfo() |
130 |
except errors.ProgrammerError:
|
131 |
# during tests, the arch info can be initialized multiple times
|
132 |
pass
|
133 |
|
134 |
self.ResetMocks()
|
135 |
|
136 |
def _StopPatchers(self): |
137 |
if self._iallocator_patcher is not None: |
138 |
self._iallocator_patcher.stop()
|
139 |
self._iallocator_patcher = None |
140 |
if self._netutils_patcher is not None: |
141 |
self._netutils_patcher.stop()
|
142 |
self._netutils_patcher = None |
143 |
if self._ssh_patcher is not None: |
144 |
self._ssh_patcher.stop()
|
145 |
self._ssh_patcher = None |
146 |
if self._rpc_patcher is not None: |
147 |
self._rpc_patcher.stop()
|
148 |
self._rpc_patcher = None |
149 |
|
150 |
def tearDown(self): |
151 |
super(CmdlibTestCase, self).tearDown() |
152 |
|
153 |
self._StopPatchers()
|
154 |
|
155 |
def _GetTestModule(self): |
156 |
module = inspect.getsourcefile(self.__class__).split("/")[-1] |
157 |
suffix = "_unittest.py"
|
158 |
assert module.endswith(suffix), "Naming convention for cmdlib test" \ |
159 |
" modules is: <module>%s (found '%s')"\
|
160 |
% (suffix, module) |
161 |
return module[:-len(suffix)] |
162 |
|
163 |
def ResetMocks(self): |
164 |
"""Resets all mocks back to their initial state.
|
165 |
|
166 |
This is useful if you want to execute more than one opcode in a single
|
167 |
test.
|
168 |
|
169 |
"""
|
170 |
self.cfg = ConfigMock()
|
171 |
self.glm = LockManagerMock()
|
172 |
self.rpc = CreateRpcRunnerMock()
|
173 |
self.ctx = GanetiContextMock(self) |
174 |
self.mcpu = ProcessorMock(self.ctx) |
175 |
|
176 |
self._StopPatchers()
|
177 |
try:
|
178 |
self._iallocator_patcher = patchIAllocator(self._GetTestModule()) |
179 |
self.iallocator_cls = self._iallocator_patcher.start() |
180 |
except (ImportError, AttributeError): |
181 |
# this test module does not use iallocator, no patching performed
|
182 |
self._iallocator_patcher = None |
183 |
|
184 |
try:
|
185 |
self._netutils_patcher = patchNetutils(self._GetTestModule()) |
186 |
self.netutils_mod = self._netutils_patcher.start() |
187 |
SetupDefaultNetutilsMock(self.netutils_mod, self.cfg) |
188 |
except (ImportError, AttributeError): |
189 |
# this test module does not use netutils, no patching performed
|
190 |
self._netutils_patcher = None |
191 |
|
192 |
try:
|
193 |
self._ssh_patcher = patchSsh(self._GetTestModule()) |
194 |
self.ssh_mod = self._ssh_patcher.start() |
195 |
except (ImportError, AttributeError): |
196 |
# this test module does not use ssh, no patching performed
|
197 |
self._ssh_patcher = None |
198 |
|
199 |
try:
|
200 |
self._rpc_patcher = patchRpc(self._GetTestModule()) |
201 |
self.rpc_mod = self._rpc_patcher.start() |
202 |
SetupDefaultRpcModuleMock(self.rpc_mod)
|
203 |
except (ImportError, AttributeError): |
204 |
# this test module does not use rpc, no patching performed
|
205 |
self._rpc_patcher = None |
206 |
|
207 |
def GetMockLU(self): |
208 |
"""Creates a mock L{LogialUnit} with access to the mocked config etc.
|
209 |
|
210 |
@rtype: L{LogialUnit}
|
211 |
@return: A mock LU
|
212 |
|
213 |
"""
|
214 |
return MockLU(self.mcpu, mock.MagicMock(), self.ctx, self.rpc) |
215 |
|
216 |
def RpcResultsBuilder(self, use_node_names=False): |
217 |
"""Creates a pre-configured L{RpcResultBuilder}
|
218 |
|
219 |
@type use_node_names: bool
|
220 |
@param use_node_names: @see L{RpcResultBuilder}
|
221 |
@rtype: L{RpcResultBuilder}
|
222 |
@return: a pre-configured builder for RPC results
|
223 |
|
224 |
"""
|
225 |
return RpcResultsBuilder(cfg=self.cfg, use_node_names=use_node_names) |
226 |
|
227 |
def ExecOpCode(self, opcode): |
228 |
"""Executes the given opcode.
|
229 |
|
230 |
@param opcode: the opcode to execute
|
231 |
@return: the result of the LU's C{Exec} method
|
232 |
|
233 |
"""
|
234 |
self.glm.AddLocksFromConfig(self.cfg) |
235 |
|
236 |
return self.mcpu.ExecOpCodeAndRecordOutput(opcode) |
237 |
|
238 |
def ExecOpCodeExpectException(self, opcode, |
239 |
expected_exception, |
240 |
expected_regex=None):
|
241 |
"""Executes the given opcode and expects an exception.
|
242 |
|
243 |
@param opcode: @see L{ExecOpCode}
|
244 |
@type expected_exception: class
|
245 |
@param expected_exception: the exception which must be raised
|
246 |
@type expected_regex: string
|
247 |
@param expected_regex: if not C{None}, a regular expression which must be
|
248 |
present in the string representation of the exception
|
249 |
|
250 |
"""
|
251 |
try:
|
252 |
self.ExecOpCode(opcode)
|
253 |
except expected_exception, e:
|
254 |
if expected_regex is not None: |
255 |
assert re.search(expected_regex, str(e)) is not None, \ |
256 |
"Caught exception '%s' did not match '%s'" % \
|
257 |
(str(e), expected_regex)
|
258 |
except Exception, e: |
259 |
tb = traceback.format_exc() |
260 |
raise AssertionError("%s\n(See original exception above)\n" |
261 |
"Expected exception '%s' was not raised,"
|
262 |
" got '%s' of class '%s' instead." %
|
263 |
(tb, expected_exception, e, e.__class__)) |
264 |
else:
|
265 |
raise AssertionError("Expected exception '%s' was not raised" % |
266 |
expected_exception) |
267 |
|
268 |
def ExecOpCodeExpectOpPrereqError(self, opcode, expected_regex=None): |
269 |
"""Executes the given opcode and expects a L{errors.OpPrereqError}
|
270 |
|
271 |
@see L{ExecOpCodeExpectException}
|
272 |
|
273 |
"""
|
274 |
self.ExecOpCodeExpectException(opcode, errors.OpPrereqError, expected_regex)
|
275 |
|
276 |
def ExecOpCodeExpectOpExecError(self, opcode, expected_regex=None): |
277 |
"""Executes the given opcode and expects a L{errors.OpExecError}
|
278 |
|
279 |
@see L{ExecOpCodeExpectException}
|
280 |
|
281 |
"""
|
282 |
self.ExecOpCodeExpectException(opcode, errors.OpExecError, expected_regex)
|
283 |
|
284 |
def RunWithLockedLU(self, opcode, test_func): |
285 |
"""Takes the given opcode, creates a LU and runs func on it.
|
286 |
|
287 |
The passed LU did already perform locking, but no methods which actually
|
288 |
require locking are executed on the LU.
|
289 |
|
290 |
@param opcode: the opcode to get the LU for.
|
291 |
@param test_func: the function to execute with the LU as parameter.
|
292 |
@return: the result of test_func
|
293 |
|
294 |
"""
|
295 |
self.glm.AddLocksFromConfig(self.cfg) |
296 |
|
297 |
return self.mcpu.RunWithLockedLU(opcode, test_func) |
298 |
|
299 |
def assertLogContainsMessage(self, expected_msg): |
300 |
"""Shortcut for L{ProcessorMock.assertLogContainsMessage}
|
301 |
|
302 |
"""
|
303 |
self.mcpu.assertLogContainsMessage(expected_msg)
|
304 |
|
305 |
def assertLogContainsRegex(self, expected_regex): |
306 |
"""Shortcut for L{ProcessorMock.assertLogContainsRegex}
|
307 |
|
308 |
"""
|
309 |
self.mcpu.assertLogContainsRegex(expected_regex)
|
310 |
|
311 |
def assertHooksCall(self, nodes, hook_path, phase, |
312 |
environment=None, count=None, index=0): |
313 |
"""Asserts a call to C{rpc.call_hooks_runner}
|
314 |
|
315 |
@type nodes: list of string
|
316 |
@param nodes: node UUID's or names hooks run on
|
317 |
@type hook_path: string
|
318 |
@param hook_path: path (or name) of the hook run
|
319 |
@type phase: string
|
320 |
@param phase: phase in which the hook runs in
|
321 |
@type environment: dict
|
322 |
@param environment: the environment passed to the hooks. C{None} to skip
|
323 |
asserting it
|
324 |
@type count: int
|
325 |
@param count: the number of hook invocations. C{None} to skip asserting it
|
326 |
@type index: int
|
327 |
@param index: the index of the hook invocation to assert
|
328 |
|
329 |
"""
|
330 |
if count is not None: |
331 |
self.assertEqual(count, self.rpc.call_hooks_runner.call_count) |
332 |
|
333 |
args = self.rpc.call_hooks_runner.call_args[index]
|
334 |
|
335 |
self.assertEqual(set(nodes), set(args[0])) |
336 |
self.assertEqual(hook_path, args[1]) |
337 |
self.assertEqual(phase, args[2]) |
338 |
if environment is not None: |
339 |
self.assertEqual(environment, args[3]) |
340 |
|
341 |
def assertSingleHooksCall(self, nodes, hook_path, phase, |
342 |
environment=None):
|
343 |
"""Asserts a single call to C{rpc.call_hooks_runner}
|
344 |
|
345 |
@see L{assertHooksCall} for parameter description.
|
346 |
|
347 |
"""
|
348 |
self.assertHooksCall(nodes, hook_path, phase,
|
349 |
environment=environment, count=1)
|
350 |
|
351 |
def CopyOpCode(self, opcode, **kwargs): |
352 |
"""Creates a copy of the given opcode and applies modifications to it
|
353 |
|
354 |
@type opcode: opcode.OpCode
|
355 |
@param opcode: the opcode to copy
|
356 |
@type kwargs: dict
|
357 |
@param kwargs: dictionary of fields to overwrite in the copy. The special
|
358 |
value L{REMOVE} can be used to remove fields from the copy.
|
359 |
@return: a copy of the given opcode
|
360 |
|
361 |
"""
|
362 |
state = opcode.__getstate__() |
363 |
|
364 |
for key, value in kwargs.items(): |
365 |
if value == self.REMOVE and key in state: |
366 |
del state[key]
|
367 |
else:
|
368 |
state[key] = value |
369 |
|
370 |
return opcodes.OpCode.LoadOpCode(state)
|
371 |
|
372 |
def _GetDefaultGroup(self): |
373 |
for group in self.cfg.GetAllNodeGroupsInfo().values(): |
374 |
if group.name == "default": |
375 |
return group
|
376 |
assert False |
377 |
|
378 |
|
379 |
# pylint: disable=C0103
|
380 |
def withLockedLU(func): |
381 |
"""Convenience decorator which runs the decorated method with the LU.
|
382 |
|
383 |
This uses L{CmdlibTestCase.RunWithLockedLU} to run the decorated method.
|
384 |
For this to work, the opcode to run has to be an instance field named "op",
|
385 |
"_op", "opcode" or "_opcode".
|
386 |
|
387 |
If the instance has a method called "PrepareLU", this method is invoked with
|
388 |
the LU right before the test method is called.
|
389 |
|
390 |
"""
|
391 |
def wrapper(*args, **kwargs): |
392 |
test = args[0]
|
393 |
assert isinstance(test, CmdlibTestCase) |
394 |
|
395 |
op = None
|
396 |
for attr_name in ["op", "_op", "opcode", "_opcode"]: |
397 |
if hasattr(test, attr_name): |
398 |
op = getattr(test, attr_name)
|
399 |
break
|
400 |
assert op is not None |
401 |
|
402 |
prepare_fn = None
|
403 |
if hasattr(test, "PrepareLU"): |
404 |
prepare_fn = getattr(test, "PrepareLU") |
405 |
assert callable(prepare_fn) |
406 |
|
407 |
# pylint: disable=W0142
|
408 |
def callWithLU(lu): |
409 |
if prepare_fn:
|
410 |
prepare_fn(lu) |
411 |
|
412 |
new_args = list(args)
|
413 |
new_args.append(lu) |
414 |
func(*new_args, **kwargs) |
415 |
|
416 |
return test.RunWithLockedLU(op, callWithLU)
|
417 |
return wrapper
|