Gitweb: http://git.fedorahosted.org/git/?p=fence-agents.git;a=commitdiff;h=d8872849a... Commit: d8872849a6a45537ffe70c5643187f3753e19ddd Parent: 710a56b931e6431526419d13cfb095d691a6cf84 Author: Marek 'marx' Grac mgrac@redhat.com AuthorDate: Fri Nov 23 15:42:51 2012 +0100 Committer: Marek 'marx' Grac mgrac@redhat.com CommitterDate: Thu Feb 7 21:46:35 2013 +0100
fencing: Testing tool and dummy fence agent
New testing tool for repeating sequences of action on given devices is ready. It should improve testing process of fence agents and simplify it. So more volunteers can test their devices.
New fence agent "dummy" is used for testing fencing library as it does not require any hardware/software. --- configure.ac | 1 + fence/agents/dummy/Makefile.am | 17 ++++ fence/agents/dummy/fence_dummy.py | 59 +++++++++++++ tests/actions.d/power-on-off.cfg | 2 + tests/actions.d/sleep.cfg | 2 + tests/actions.d/status.cfg | 2 + tests/devices.d/dummy-with_action.cfg | 9 ++ tests/devices.d/dummy.cfg | 3 + tests/devices.d/invalid-missing_option.cfg | 4 + tests/devices.d/true-with_action.cfg | 8 ++ tests/devices.d/true.cfg | 8 ++ tests/fence_testing.py | 122 ++++++++++++++++++++++++++++ tests/fence_testing_test.py | 70 ++++++++++++++++ tests/test.py | 21 +++++ 14 files changed, 328 insertions(+), 0 deletions(-)
diff --git a/configure.ac b/configure.ac index e30581c..de4124d 100644 --- a/configure.ac +++ b/configure.ac @@ -258,6 +258,7 @@ AC_CONFIG_FILES([Makefile fence/agents/cpint/Makefile fence/agents/drac/Makefile fence/agents/drac5/Makefile + fence/agents/dummy/Makefile fence/agents/eaton_snmp/Makefile fence/agents/egenera/Makefile fence/agents/eps/Makefile diff --git a/fence/agents/dummy/Makefile.am b/fence/agents/dummy/Makefile.am new file mode 100644 index 0000000..11405eb --- /dev/null +++ b/fence/agents/dummy/Makefile.am @@ -0,0 +1,17 @@ +MAINTAINERCLEANFILES = Makefile.in + +TARGET = fence_dummy + +SRC = $(TARGET).py + +EXTRA_DIST = $(SRC) + +sbin_SCRIPTS = $(TARGET) + +man_MANS = $(TARGET).8 + +include $(top_srcdir)/make/fencebuild.mk +include $(top_srcdir)/make/fenceman.mk + +clean-local: clean-man + rm -f $(TARGET) diff --git a/fence/agents/dummy/fence_dummy.py b/fence/agents/dummy/fence_dummy.py new file mode 100644 index 0000000..ccf7f20 --- /dev/null +++ b/fence/agents/dummy/fence_dummy.py @@ -0,0 +1,59 @@ +#!/usr/bin/python + +import sys, re, pexpect, exceptions +sys.path.append("@FENCEAGENTSLIBDIR@") +from fencing import * + +#BEGIN_VERSION_GENERATION +RELEASE_VERSION="New Dummy Agent - test release on steroids" +REDHAT_COPYRIGHT="" +BUILD_DATE="" +#END_VERSION_GENERATION + +def get_power_status(conn, options): + try: + status_file = open(options["--status-file"], 'r') + except: + return "off" + + status = status_file.read() + status_file.close() + + return status.lower() + +def set_power_status(conn, options): + if not (options["--action"] in [ "on", "off" ]): + return + + status_file = open(options["--status-file"], 'w') + status_file.write(options["--action"]) + status_file.close() + +def main(): + device_opt = [ "no_password", "status_file" ] + + atexit.register(atexit_handler) + + all_opt["status_file"] = { + "getopt" : "s:", + "longopt" : "status-file", + "help":"--status-file=<file> Name of file that holds current status", + "required" : "0", + "shortdesc" : "File with status", + "default" : "/tmp/fence_dummy.status", + "order": 1 + } + + options = check_input(device_opt, process_input(device_opt)) + + docs = { } + docs["shortdesc"] = "Dummy fence agent" + docs["longdesc"] = "fence_dummy" + docs["vendorurl"] = "http://www.example.com" + show_docs(options, docs) + + result = fence_action(None, options, set_power_status, get_power_status, None) + sys.exit(result) + +if __name__ == "__main__": + main() diff --git a/tests/actions.d/power-on-off.cfg b/tests/actions.d/power-on-off.cfg new file mode 100644 index 0000000..d9df57a --- /dev/null +++ b/tests/actions.d/power-on-off.cfg @@ -0,0 +1,2 @@ +name = "Power ON & OFF" +actions = [ { "command" : "on", "return_code" : "^0$" }, { "command" : "status", "return_code" : "^0$" }, { "command" : "off", "return_code" : "^0$" }, { "command" : "status", "return_code" : "^2$" } ] diff --git a/tests/actions.d/sleep.cfg b/tests/actions.d/sleep.cfg new file mode 100644 index 0000000..c0fad72 --- /dev/null +++ b/tests/actions.d/sleep.cfg @@ -0,0 +1,2 @@ +name = "Pure Sleep" +actions = [ { "command" : "sleep(1)", "return_code" : "^0$" }, { "command" : "sleep(3)", "return_code" : "^0$" }, { "command" : "sleep(5)", "return_code" : "^0$" } ] diff --git a/tests/actions.d/status.cfg b/tests/actions.d/status.cfg new file mode 100644 index 0000000..760f94b --- /dev/null +++ b/tests/actions.d/status.cfg @@ -0,0 +1,2 @@ +name = "Simple Status" +actions = [ { "command" : "status", "return_code" : "^[02]$" }, { "command" : "sleep(1)", "return_code" : "^0$" } ] diff --git a/tests/devices.d/dummy-with_action.cfg b/tests/devices.d/dummy-with_action.cfg new file mode 100644 index 0000000..5550c36 --- /dev/null +++ b/tests/devices.d/dummy-with_action.cfg @@ -0,0 +1,9 @@ +name = "Dummy fence device configuration" +agent = "/bin/true" +[options] + login = "foo", "--username", "-l" + passwd = "bar", "--password", "-p" + ipaddr = "fence.example.com", "--ip", "-a" + port = "1", "--plug" + action = "status", "--action", "-o" + diff --git a/tests/devices.d/dummy.cfg b/tests/devices.d/dummy.cfg new file mode 100644 index 0000000..d912475 --- /dev/null +++ b/tests/devices.d/dummy.cfg @@ -0,0 +1,3 @@ +name = "Dummy fence device configuration" +agent = "/home/marx/GIT/fence-agents/fence/agents/dummy/fence_dummy" +[options] diff --git a/tests/devices.d/invalid-missing_option.cfg b/tests/devices.d/invalid-missing_option.cfg new file mode 100644 index 0000000..0805fdc --- /dev/null +++ b/tests/devices.d/invalid-missing_option.cfg @@ -0,0 +1,4 @@ +name = "Invalid device definition: Both short and long options are missing" +agent = "/bin/true" +[options] + login = "foo" diff --git a/tests/devices.d/true-with_action.cfg b/tests/devices.d/true-with_action.cfg new file mode 100644 index 0000000..d95c9fe --- /dev/null +++ b/tests/devices.d/true-with_action.cfg @@ -0,0 +1,8 @@ +name = "Dummy fence device configuration" +agent = "/bin/true" +[options] + login = [ "foo", "--username", "-l" ] + passwd = [ "bar", "--password", "-p" ] + ipaddr = [ "fence.example.com", "--ip", "-a" ] + port = [ "1", "--plug" ] + action = [ "status", "--action", "-o" ] diff --git a/tests/devices.d/true.cfg b/tests/devices.d/true.cfg new file mode 100644 index 0000000..4ba0aa5 --- /dev/null +++ b/tests/devices.d/true.cfg @@ -0,0 +1,8 @@ +name = "Dummy fence device configuration" +agent = "/bin/true" +[options] + login = [ "foo", "--username", "-l" ] + passwd = [ "bar", "--password", "-p" ] + ipaddr = [ "fence.example.com", "--ip", "-a" ] + port = [ "1", "--plug" ] + diff --git a/tests/fence_testing.py b/tests/fence_testing.py new file mode 100755 index 0000000..566872e --- /dev/null +++ b/tests/fence_testing.py @@ -0,0 +1,122 @@ +""" Library for fence agents testing via predefined scenarios """ +from configobj import ConfigObj +import re, sys, os + +EC_CONFIG_FAIL = 1 + +def _prepare_command(agent_file, method): + """ Parse configuration of fence device and prepare (command + STDIN values) to execute. + + Fence device configuration is used to generate a command which can be executed. + Because fence agents supports several options how to enter data, we can select + from three different methods ("stdin", "getopt" - short options, "longopt"). + When method "stdin" is used then this function will generate also text which should + be entered on STDIN instead of command itself. + + Example of agent definition: + name = "Dummy fence device configuration" + agent = "/bin/true" + [options] + login = [ "foo", "--username", "-l" ] + passwd = [ "bar", "--password", "-p" ] + ipaddr = [ "fence.example.com", "--ip", "-a" ] + port = [ "1", "--plug" ] + """ + assert (method in ["stdin", "getopt", "longopt"]), "Invalid method entered" + + config = ConfigObj(agent_file, unrepr = True) + + assert (config.has_key("agent")), "Fence agent has to be defined" + final_command = config["agent"] + stdin_values = None + + for opt in config["options"].keys(): + assert isinstance(config["options"][opt], list), "Option %s have to have at least value and longopt"% (opt) + assert len(config["options"][opt]) >= 2, "Option %s have to have at least value and longopt"% (opt) + value = config["options"][opt][0] + if opt == "action": + ## ignore action as it is not part of fence device definition + continue + + if method == "stdin": + option = opt + if stdin_values == None: + stdin_values = "" + stdin_values += option + "=" + value + "\n" + elif method == "longopt": + option = config["options"][opt][1] + final_command += " " + option + " " + value + elif method == "getopt": + if len(config["options"][opt]) == (2 + 1): + option = config["options"][opt][2] + else: + option = config["options"][opt][1] + final_command += " " + option + " " + value + + return (final_command, stdin_values) + +def test_action(agent, action_file, method): + """ Run defined sequence of actions on a given fence agent. + + This function will run one set of test on a fence agent. Test itself consists of + sequence of action and expected return codes. User can select from actions supported + by fence agent (on, off, reboot, list, status, monitor) and sleep(X) command where X + is in seconds and determine the length of pause between commands. Each action has to + have defined regular expression which define acceptable return codes from fence agent + or sleep. + + Example of action configuration file: + name = "Simple Status" + actions = [ { "command" : "status", "return_code" : "^[02]$" }, { "command" : "sleep(1)", "return_code" : "^0$" } ] + """ + re_sleep_command = re.compile('sleep(([0-9]+))', re.IGNORECASE) + config = ConfigObj(action_file, unrepr = True) + + (command, stdin_options) = _prepare_command(agent, method) + + for action in config["actions"]: + assert action.has_key("command"), "Action %s need to have defined 'command'"% (action_file) + assert action.has_key("return_code"), "Command %s (in %s) need to have 'return_code' defined"% (action_file, action["command"]) + + sleep_wait = None + current_command = None + current_stdin_options = None + + if not (action["command"] in [ "status", "reboot", "on", "off", "list", "monitor" ]): + is_sleep = re.search(re_sleep_command, action["command"]) + if is_sleep != None: + sleep_wait = is_sleep.group(1) + else: + sys.stderr.write("ERROR: %s contains unsupported action "%s"\n"% (action_file, action["command"])) + sys.exit(1) + + if sleep_wait != None: + current_command = "/bin/sleep " + sleep_wait + current_stdin_options = None + else: + current_command = command + current_stdin_options = stdin_options + + if method == "stdin": + if current_stdin_options == None: + current_stdin_options = "" + current_stdin_options += "action=%s"% (action["command"]) + elif method == "longopt": + current_command += " --action=%s"% (action["command"]) + elif method == "getopt": + current_command += " -o %s"% (action["command"]) + + # @note: Broken pipe can occur here and I'm not sure why - non-deterministic + if method == "stdin" and sleep_wait == None: + current_command = "/bin/echo -e "" + current_stdin_options + "" | " + current_command + + result = os.system(current_command + " &> /dev/null") + exitcode = (result >> 8) & 0xFF + + is_valid_result_code = re.search(action["return_code"], str(exitcode), re.IGNORECASE) + + if is_valid_result_code == None: + print "TEST FAILED: %s failed on %s when using (%s)\n"% (agent, action_file, method) + print "TEST INFO: %s returns %s\n"% (action["command"], str(exitcode)) + return + print "TEST PASSED: %s worked on %s (%s)\n"% (agent, action_file, method) diff --git a/tests/fence_testing_test.py b/tests/fence_testing_test.py new file mode 100755 index 0000000..368ff70 --- /dev/null +++ b/tests/fence_testing_test.py @@ -0,0 +1,70 @@ +#!/usr/bin/python + +import unittest +import fence_testing + +class TestPrepareCommand(unittest.TestCase): + DEVICE_MISSING_OPTION = "devices.d/invalid-missing_option.cfg" + DEVICE_CORRECT = "devices.d/true.cfg" + DEVICE_CORRECT_WITH_ACTION = "devices.d/true-with_action.cfg" + + def test_missing_device(self): + self.assertRaises(fence_testing._prepare_command, None, "getopt") + + def test_missing_option(self): + self.assertRaises(AssertionError, fence_testing._prepare_command, self.DEVICE_MISSING_OPTION, "stdin") + + def test_valid_methods(self): + fence_testing._prepare_command(self.DEVICE_CORRECT, "getopt") + fence_testing._prepare_command(self.DEVICE_CORRECT, "longopt") + fence_testing._prepare_command(self.DEVICE_CORRECT, "stdin") + + def test_invalid_method(self): + self.assertRaises(AssertionError, fence_testing._prepare_command, self.DEVICE_CORRECT, "invalid") + + def test_is_action_ignored(self): + (command1, _) = fence_testing._prepare_command(self.DEVICE_CORRECT, "getopt") + (command2, _) = fence_testing._prepare_command(self.DEVICE_CORRECT_WITH_ACTION, "getopt") + self.assertEquals(command1, command2) + + def test_is_stdin_empty(self): + (_, stdin) = fence_testing._prepare_command(self.DEVICE_CORRECT, "getopt") + self.assertEquals(None, stdin) + (_, stdin) = fence_testing._prepare_command(self.DEVICE_CORRECT, "longopt") + self.assertEquals(None, stdin) + + def test_prepared_command_getopt(self): + ## Test also fallback to longopt if short is not present + (command, _) = fence_testing._prepare_command(self.DEVICE_CORRECT, "getopt") + self.assertEquals("/bin/true -l foo -p bar -a fence.example.com --plug 1", command) + + def test_prepared_command_longopt(self): + (command, _) = fence_testing._prepare_command(self.DEVICE_CORRECT, "longopt") + self.assertEquals("/bin/true --username foo --password bar --ip fence.example.com --plug 1", command) + + def test_prepared_command_stdin(self): + (command, stdin) = fence_testing._prepare_command(self.DEVICE_CORRECT, "stdin") + self.assertEquals("/bin/true", command) + self.assertEquals("login=foo\npasswd=bar\nipaddr=fence.example.com\nport=1\n", stdin) + +class TestTestAction(unittest.TestCase): + def test_valid_actions(self): + pass + + def test_invalid_actions(self): + pass + + def test_valid_return_code(self): + pass + + def test_invalid_return_code(self): + pass + + def test_valid_re_contains(self): + pass + + def test_invalid_re_contains(self): + pass + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test.py b/tests/test.py new file mode 100755 index 0000000..8e82ed9 --- /dev/null +++ b/tests/test.py @@ -0,0 +1,21 @@ +#!/usr/bin/python + +from fence_testing import test_action + +def main(): + ## @todo: utility1 - run single 'agent' 'action' 'method' + ## @todo: utility2 - run complex tests (using utility1?) -> file with test suites + + AGENTDEF = "devices.d/true.cfg" + DUMMYDEF = "devices.d/dummy.cfg" + + ACT_STATUS = "actions.d/status.cfg" + ACT_ONOFF = "actions.d/power-on-off.cfg" + +# test_action(AGENTDEF, ACTIONDEF, "stdin") +# test_action(AGENTDEF, ACTIONDEF, "getopt") + test_action(DUMMYDEF, ACT_STATUS, "getopt") + test_action(DUMMYDEF, ACT_ONOFF, "getopt") + +if __name__ == "__main__": + main() \ No newline at end of file