Statistics
| Branch: | Tag: | Revision:

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