Statistics
| Branch: | Tag: | Revision:

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