Commit 3efa7659 authored by Thomas Thrainer's avatar Thomas Thrainer

Initial version of cmdlib test framework

The initial version of the cmdlib test framework is able to execute LU's
with the following components mocked:

 * Configuration
 * IAllocator interface
 * Lock manager
 * MCPU processor
 * RPC runner

A base test class is provided which makes it easy to execute opcodes and
to perform asserts on the output.

Tests for the LUTestDelay logical unit demonstrate the functionality of
the test framework.

The framework is not yet fully complete, further work will be done as
the tests are written.
Signed-off-by: default avatarThomas Thrainer <thomasth@google.com>
Reviewed-by: default avatarMichele Tartara <mtartara@google.com>
parent 72a7f6b3
......@@ -137,6 +137,8 @@ DIRS = \
test/data/ovfdata \
test/data/ovfdata/other \
test/py \
test/py/cmdlib \
test/py/cmdlib/testsupport \
tools
ALL_APIDOC_HS_DIRS = \
......@@ -1026,11 +1028,9 @@ EXTRA_DIST = \
doc/examples/gnt-debug/README \
doc/examples/gnt-debug/delay0.json \
doc/examples/gnt-debug/delay50.json \
test/py/lockperf.py \
test/py/testutils.py \
test/py/mocks.py \
$(dist_TESTS) \
$(TEST_FILES) \
$(python_test_support) \
man/footer.rst \
$(manrst) \
$(maninput) \
......@@ -1237,6 +1237,7 @@ TEST_FILES = \
python_tests = \
doc/examples/rapi_testutils.py \
test/py/cmdlib/test_unittest.py \
test/py/cfgupgrade_unittest.py \
test/py/docs_unittest.py \
test/py/ganeti.asyncnotifier_unittest.py \
......@@ -1322,6 +1323,20 @@ python_tests = \
test/py/qa.qa_config_unittest.py \
test/py/tempfile_fork_unittest.py
python_test_support = \
test/py/__init__.py \
test/py/lockperf.py \
test/py/testutils.py \
test/py/mocks.py \
test/py/cmdlib/__init__.py \
test/py/cmdlib/testsupport/__init__.py \
test/py/cmdlib/testsupport/cmdlib_testcase.py \
test/py/cmdlib/testsupport/config_mock.py \
test/py/cmdlib/testsupport/iallocator_mock.py \
test/py/cmdlib/testsupport/lock_manager_mock.py \
test/py/cmdlib/testsupport/processor_mock.py \
test/py/cmdlib/testsupport/rpc_runner_mock.py
haskell_tests = test/hs/htest
dist_TESTS = \
......@@ -1352,7 +1367,7 @@ TESTS = $(dist_TESTS) $(nodist_TESTS)
# Environment for all tests
PLAIN_TESTS_ENVIRONMENT = \
PYTHONPATH=. \
PYTHONPATH=.:./test/py \
TOP_SRCDIR=$(abs_top_srcdir) TOP_BUILDDIR=$(abs_top_builddir) \
PYTHON=$(PYTHON) FAKEROOT=$(FAKEROOT_PATH) \
$(RUN_IN_TEMPDIR)
......@@ -1386,6 +1401,7 @@ all_python_code = \
if PY_UNIT
all_python_code += $(python_tests)
all_python_code += $(python_test_support)
endif
srclink_files = \
......
......@@ -66,7 +66,7 @@ class LUTestDelay(NoHooksLU):
"""
if self.op.on_master:
if not utils.TestDelay(self.op.duration):
if not utils.TestDelay(self.op.duration)[0]:
raise errors.OpExecError("Error during master delay test")
if self.op.on_node_uuids:
result = self.rpc.call_test_delay(self.op.on_node_uuids, self.op.duration)
......
......@@ -167,7 +167,7 @@ def _CheckInstanceDiskIvNames(disks):
return result
class ConfigWriter:
class ConfigWriter(object):
"""The interface to the cluster configuration.
@ivar _temporary_lvs: reservation manager for temporary LVs
......
......@@ -1625,7 +1625,7 @@ BGL = "BGL"
NAL = "NAL"
class GanetiLockManager:
class GanetiLockManager(object):
"""The Ganeti Locking Library
The purpose of this small library is to manage locking for ganeti clusters
......
#
#
# Copyright (C) 2013 Google Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.
"""This module contains all python test code"""
\ No newline at end of file
#
#
# Copyright (C) 2013 Google Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.
"""This module contains all cmdlib unit tests"""
\ No newline at end of file
#!/usr/bin/python
#
# Copyright (C) 2013 Google Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.
"""Tests for LUTest*
"""
from ganeti import constants
from ganeti import errors
from ganeti import opcodes
from testsupport import *
import testutils
DELAY_DURATION = 0.01
class TestLUTestDelay(CmdlibTestCase):
def testRepeatedInvocation(self):
op = opcodes.OpTestDelay(duration=DELAY_DURATION,
repeat=3)
self.ExecOpCode(op)
self.assertLogContainsMessage(" - INFO: Test delay iteration 0/2")
self.mcpu.assertLogContainsEntry(constants.ELOG_MESSAGE,
" - INFO: Test delay iteration 1/2")
self.assertLogContainsRegex("2/2$")
def testInvalidDuration(self):
op = opcodes.OpTestDelay(duration=-1)
self.assertRaises(errors.OpExecError, self.ExecOpCode, op)
def testOnNodeUuid(self):
node_uuids = [self.cfg.GetMasterNode()]
op = opcodes.OpTestDelay(duration=DELAY_DURATION,
on_node_uuids=node_uuids)
self.ExecOpCode(op)
self.rpc.call_test_delay.assert_called_once_with(node_uuids, DELAY_DURATION)
def testOnNodeName(self):
op = opcodes.OpTestDelay(duration=DELAY_DURATION,
on_nodes=[self.cfg.GetMasterNodeName()])
self.ExecOpCode(op)
self.rpc.call_test_delay.assert_called_once_with([self.cfg.GetMasterNode()],
DELAY_DURATION)
def testSuccessfulRpc(self):
op = opcodes.OpTestDelay(duration=DELAY_DURATION,
on_nodes=[self.cfg.GetMasterNodeName()])
self.rpc.call_test_delay.return_value = \
RpcResultsBuilder(cfg=self.cfg) \
.AddSuccessfulNode(self.cfg.GetMasterNode()) \
.Build()
self.ExecOpCode(op)
self.rpc.call_test_delay.assert_called_once()
def testFailingRpc(self):
op = opcodes.OpTestDelay(duration=DELAY_DURATION,
on_nodes=[self.cfg.GetMasterNodeName()])
self.rpc.call_test_delay.return_value = \
RpcResultsBuilder(cfg=self.cfg) \
.AddFailedNode(self.cfg.GetMasterNode()) \
.Build()
self.assertRaises(errors.OpExecError, self.ExecOpCode, op)
def testMultipleNodes(self):
node1 = self.cfg.AddNewNode()
node2 = self.cfg.AddNewNode()
op = opcodes.OpTestDelay(duration=DELAY_DURATION,
on_nodes=[node1.name, node2.name])
self.rpc.call_test_delay.return_value = \
RpcResultsBuilder(cfg=self.cfg) \
.AddSuccessfulNode(node1) \
.AddSuccessfulNode(node2) \
.Build()
self.ExecOpCode(op)
self.rpc.call_test_delay.assert_called_once_with([node1.uuid, node2.uuid],
DELAY_DURATION)
if __name__ == "__main__":
testutils.GanetiTestProgram()
#
#
# Copyright (C) 2013 Google Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.
"""Support classes and functions for testing the cmdlib module.
"""
from cmdlib_testcase import CmdlibTestCase
from config_mock import ConfigMock
from iallocator_mock import CreateIAllocatorMock
from lock_manager_mock import LockManagerMock
from processor_mock import ProcessorMock
from rpc_runner_mock import CreateRpcRunnerMock, RpcResultsBuilder
__all__ = ["CmdlibTestCase",
"ConfigMock",
"CreateIAllocatorMock",
"CreateRpcRunnerMock",
"LockManagerMock",
"ProcessorMock",
"RpcResultsBuilder",
]
\ No newline at end of file
#
#
# Copyright (C) 2013 Google Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.
from config_mock import *
from iallocator_mock import *
from lock_manager_mock import *
from processor_mock import *
from rpc_runner_mock import *
import testutils
class GanetiContextMock(object):
def __init__(self, cfg, glm, rpc):
self.cfg = cfg
self.glm = glm
self.rpc = rpc
class CmdlibTestCase(testutils.GanetiTestCase):
"""Base class for cmdlib tests.
This class sets up a mocked environment for the execution of
L{ganeti.cmdlib.base.LogicalUnit} subclasses.
The environment can be customized via the following fields:
* C{cfg}: @see L{ConfigMock}
* C{glm}: @see L{LockManagerMock}
* C{rpc}: @see L{CreateRpcRunnerMock}
* C{iallocator}: @see L{CreateIAllocatorMock}
* C{mcpu}: @see L{ProcessorMock}
"""
def setUp(self):
super(CmdlibTestCase, self).setUp()
self.cfg = ConfigMock()
self.glm = LockManagerMock()
self.rpc = CreateRpcRunnerMock()
self.iallocator = CreateIAllocatorMock()
ctx = GanetiContextMock(self.cfg, self.glm, self.rpc)
self.mcpu = ProcessorMock(ctx)
def tearDown(self):
super(CmdlibTestCase, self).tearDown()
def ExecOpCode(self, opcode):
"""Executes the given opcode.
@param opcode: the opcode to execute
@return: the result of the LU's C{Exec} method
"""
self.glm.AddLocksFromConfig(self.cfg)
return self.mcpu.ExecOpCodeAndRecordOutput(opcode)
def assertLogContainsMessage(self, expected_msg):
"""Shortcut for L{ProcessorMock.assertLogContainsMessage}
"""
self.mcpu.assertLogContainsMessage(expected_msg)
def assertLogContainsRegex(self, expected_regex):
"""Shortcut for L{ProcessorMock.assertLogContainsRegex}
"""
self.mcpu.assertLogContainsRegex(expected_regex)
#
#
# Copyright (C) 2013 Google Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.
import uuid
from ganeti import config
from ganeti import constants
from ganeti import objects
import mocks
def _StubGetEntResolver():
return mocks.FakeGetentResolver()
class ConfigMock(config.ConfigWriter):
"""A mocked cluster configuration with added methods for easy customization.
"""
def __init__(self):
self._cur_group_id = 1
self._cur_node_id = 1
self._cur_inst_id = 1
super(ConfigMock, self).__init__(cfg_file="/dev/null",
_getents=_StubGetEntResolver())
def _GetUuid(self):
return str(uuid.uuid4())
def AddNewNodeGroup(self,
uuid=None,
name=None,
ndparams=None,
diskparams=None,
ipolicy=None,
hv_state_static=None,
disk_state_static=None,
alloc_policy=None,
networks=[]):
"""Add a new L{objects.NodeGroup} to the cluster configuration
See L{objects.NodeGroup} for parameter documentation.
@rtype: L{objects.NodeGroup}
@return: the newly added node group
"""
group_id = self._cur_group_id
self._cur_group_id += 1
if uuid is None:
uuid = self._GetUuid()
if name is None:
name = "mock_group_%d" % group_id
group = objects.NodeGroup(uuid=uuid,
name=name,
ndparams=ndparams,
diskparams=diskparams,
ipolicy=ipolicy,
hv_state_static=hv_state_static,
disk_state_static=disk_state_static,
alloc_policy=alloc_policy,
networks=networks,
members=[])
self.AddNodeGroup(group, None)
return group
def AddNewNode(self,
uuid=None,
name=None,
primary_ip=None,
secondary_ip=None,
master_candidate=True,
offline=False,
drained=False,
group=None,
master_capable=True,
vm_capable=True,
ndparams=None,
powered=True,
hv_state=None,
hv_state_static=None,
disk_state=None,
disk_state_static=None):
"""Add a new L{objects.Node} to the cluster configuration
See L{objects.Node} for parameter documentation.
@rtype: L{objects.Node}
@return: the newly added node
"""
node_id = self._cur_node_id
self._cur_node_id += 1
if uuid is None:
uuid = self._GetUuid()
if name is None:
name = "mock_node_%d.example.com" % node_id
if primary_ip is None:
primary_ip = "192.168.0.%d" % node_id
if secondary_ip is None:
secondary_ip = "192.168.1.%d" % node_id
if group is None:
group = self._default_group.uuid
if isinstance(group, objects.NodeGroup):
group = group.uuid
node = objects.Node(uuid=uuid,
name=name,
primary_ip=primary_ip,
secondary_ip=secondary_ip,
master_candidate=master_candidate,
offline=offline,
drained=drained,
group=group,
master_capable=master_capable,
vm_capable=vm_capable,
ndparams=ndparams,
powered=powered,
hv_state=hv_state,
hv_state_static=hv_state_static,
disk_state=disk_state,
disk_state_static=disk_state_static)
self.AddNode(node, None)
return node
def AddNewInstance(self,
uuid=None,
name=None,
primary_node=None,
os="mocked_os",
hypervisor=constants.HT_FAKE,
hvparams={},
beparams={},
osparams={},
admin_state=constants.ADMINST_DOWN,
nics=[],
disks=[],
disk_template=constants.DT_DISKLESS,
disks_active=False,
network_port=None):
"""Add a new L{objects.Instance} to the cluster configuration
See L{objects.Instance} for parameter documentation.
@rtype: L{objects.Instance}
@return: the newly added instance
"""
inst_id = self._cur_inst_id
self._cur_inst_id += 1
if uuid is None:
uuid = self._GetUuid()
if name is None:
name = "mock_inst_%d.example.com" % inst_id
if primary_node is None:
primary_node = self._master_node.uuid
if isinstance(primary_node, objects.Node):
primary_node = self._master_node.uuid
inst = objects.Instance(uuid=uuid,
name=name,
primary_node=primary_node,
os=os,
hypervisor=hypervisor,
hvparams=hvparams,
beparams=beparams,
osparams=osparams,
admin_state=admin_state,
nics=nics,
disks=disks,
disk_template=disk_template,
disks_active=disks_active,
network_port=network_port)
self.AddInstance(inst, None)
return inst
def _OpenConfig(self, accept_foreign):
self._config_data = objects.ConfigData(
version=constants.CONFIG_VERSION,
cluster=None,
nodegroups={},
nodes={},
instances={},
networks={})
master_node_uuid = self._GetUuid()
self._cluster = objects.Cluster(
serial_no=1,
rsahostkeypub="",
highest_used_port=(constants.FIRST_DRBD_PORT - 1),
mac_prefix="aa:00:00",
volume_group_name="xenvg",
drbd_usermode_helper="/bin/true",
nicparams={constants.PP_DEFAULT: constants.NICC_DEFAULTS},
ndparams=constants.NDC_DEFAULTS,
tcpudp_port_pool=set(),
enabled_hypervisors=[constants.HT_FAKE],
master_node=master_node_uuid,
master_ip="192.168.0.254",
master_netdev=constants.DEFAULT_BRIDGE,
cluster_name="cluster.example.com",
file_storage_dir="/tmp",
uid_pool=[],
)
self._config_data.cluster = self._cluster
self._default_group = self.AddNewNodeGroup(name="default")
self._master_node = self.AddNewNode(uuid=master_node_uuid)
def _WriteConfig(self, destination=None, feedback_fn=None):
pass
def _DistributeConfig(self, feedback_fn):
pass
def _GetRpc(self, address_list):
raise NotImplementedError
#
#
# Copyright (C) 2013 Google Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software