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