From 644d54f1c6ea2a0426231e196218980f34964156 Mon Sep 17 00:00:00 2001 From: Adam Collard Date: Mon, 7 Sep 2015 16:55:10 +0100 Subject: [PATCH] Add pause and resume actions with unit tests --- actions.yaml | 11 ++ actions/__init__.py | 0 actions/actions.py | 83 +++++++++++++++ actions/pause | 1 + actions/resume | 1 + unit_tests/test_actions.py | 204 +++++++++++++++++++++++++++++++++++++ 6 files changed, 300 insertions(+) create mode 100644 actions.yaml create mode 100644 actions/__init__.py create mode 100755 actions/actions.py create mode 120000 actions/pause create mode 120000 actions/resume create mode 100644 unit_tests/test_actions.py diff --git a/actions.yaml b/actions.yaml new file mode 100644 index 0000000..780756f --- /dev/null +++ b/actions.yaml @@ -0,0 +1,11 @@ +pause: + description: | + Pause swift-proxy services. + If the swift-proxy deployment is clustered using the hacluster charm, the + corresponding hacluster unit on the node must first be paused as well. + Not doing so may lead to an interruption of service. +resume: + description: | + Resume swift-proxy services. + If the swift-proxy deployment is clustered using the hacluster charm, the + corresponding hacluster unit on the node must be resumed as well. diff --git a/actions/__init__.py b/actions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/actions/actions.py b/actions/actions.py new file mode 100755 index 0000000..b226be7 --- /dev/null +++ b/actions/actions.py @@ -0,0 +1,83 @@ +#!/usr/bin/python + +import argparse +import os +import sys +import yaml + +from charmhelpers.core.host import service_pause, service_resume +from charmhelpers.core.hookenv import action_fail, status_set + +from lib.swift_utils import services + + +def get_action_parser(actions_yaml_path, action_name, + get_services=services): + """Make an argparse.ArgumentParser seeded from actions.yaml definitions.""" + with open(actions_yaml_path) as fh: + doc = yaml.load(fh)[action_name]["description"] + parser = argparse.ArgumentParser(description=doc) + parser.add_argument("--services", default=get_services()) + # TODO: Add arguments for params defined in the actions.yaml + return parser + + +def pause(args): + """Pause all the swift services. + + @raises Exception if any services fail to stop + """ + for service in args.services: + stopped = service_pause(service) + if not stopped: + raise Exception("{} didn't stop cleanly.".format(service)) + status_set( + "maintenance", "Paused. Use 'resume' action to resume normal service.") + + +def resume(args): + """Resume all the swift services. + + @raises Exception if any services fail to start + """ + for service in args.services: + started = service_resume(service) + if not started: + raise Exception("{} didn't start cleanly.".format(service)) + status_set("active", "") + + +# A dictionary of all the defined actions to callables (which take +# parsed arguments). +ACTIONS = {"pause": pause, "resume": resume} + + +def main(argv): + action_name = _get_action_name() + actions_yaml_path = _get_actions_yaml_path() + parser = get_action_parser(actions_yaml_path, action_name) + args = parser.parse_args(argv) + try: + action = ACTIONS[action_name] + except KeyError: + return "Action %s undefined" % action_name + else: + try: + action(args) + except Exception as e: + action_fail(str(e)) + + +def _get_action_name(): + """Return the name of the action.""" + return os.path.basename(__file__) + + +def _get_actions_yaml_path(): + """Return the path to actions.yaml""" + cwd = os.path.dirname(__file__) + return os.path.join(cwd, "..", "actions.yaml") + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/actions/pause b/actions/pause new file mode 120000 index 0000000..405a394 --- /dev/null +++ b/actions/pause @@ -0,0 +1 @@ +actions.py \ No newline at end of file diff --git a/actions/resume b/actions/resume new file mode 120000 index 0000000..405a394 --- /dev/null +++ b/actions/resume @@ -0,0 +1 @@ +actions.py \ No newline at end of file diff --git a/unit_tests/test_actions.py b/unit_tests/test_actions.py new file mode 100644 index 0000000..82dff66 --- /dev/null +++ b/unit_tests/test_actions.py @@ -0,0 +1,204 @@ +import argparse +import tempfile +import unittest + +import mock +import yaml + +import actions.actions + + +class CharmTestCase(unittest.TestCase): + + def setUp(self, obj, patches): + super(CharmTestCase, self).setUp() + self.patches = patches + self.obj = obj + self.patch_all() + + def patch(self, method): + _m = mock.patch.object(self.obj, method) + mocked = _m.start() + self.addCleanup(_m.stop) + return mocked + + def patch_all(self): + for method in self.patches: + setattr(self, method, self.patch(method)) + + +class PauseTestCase(CharmTestCase): + + def setUp(self): + super(PauseTestCase, self).setUp( + actions.actions, ["service_pause", "status_set"]) + + class FakeArgs(object): + services = ['swift-proxy', 'haproxy', 'memcached', 'apache2'] + self.args = FakeArgs() + + def test_pauses_services(self): + """Pause action pauses all of the Swift services.""" + pause_calls = [] + + def fake_service_pause(svc): + pause_calls.append(svc) + return True + + self.service_pause.side_effect = fake_service_pause + + actions.actions.pause(self.args) + self.assertEqual( + pause_calls, ['swift-proxy', 'haproxy', 'memcached', 'apache2']) + + def test_bails_out_early_on_error(self): + """Pause action fails early if there are errors stopping a service.""" + pause_calls = [] + + def maybe_kill(svc): + if svc == "haproxy": + return False + else: + pause_calls.append(svc) + return True + + self.service_pause.side_effect = maybe_kill + self.assertRaisesRegexp( + Exception, "haproxy didn't stop cleanly.", + actions.actions.pause, self.args) + self.assertEqual(pause_calls, ["swift-proxy"]) + + def test_status_mode(self): + """Pause action sets the status to maintenance.""" + status_calls = [] + self.status_set.side_effect = lambda state, msg: status_calls.append( + state) + + actions.actions.pause(self.args) + self.assertEqual(status_calls, ["maintenance"]) + + def test_status_message(self): + """Pause action sets a status message reflecting that it's paused.""" + status_calls = [] + self.status_set.side_effect = lambda state, msg: status_calls.append( + msg) + + actions.actions.pause(self.args) + self.assertEqual( + status_calls, ["Paused. " + "Use 'resume' action to resume normal service."]) + + +class ResumeTestCase(CharmTestCase): + + def setUp(self): + super(ResumeTestCase, self).setUp( + actions.actions, ["service_resume", "status_set"]) + + class FakeArgs(object): + services = ['swift-proxy', 'haproxy', 'memcached', 'apache2'] + self.args = FakeArgs() + + def test_resumes_services(self): + """Resume action resumes all of the Swift services.""" + resume_calls = [] + + def fake_service_resume(svc): + resume_calls.append(svc) + return True + + self.service_resume.side_effect = fake_service_resume + actions.actions.resume(self.args) + self.assertEqual( + resume_calls, ['swift-proxy', 'haproxy', 'memcached', 'apache2']) + + def test_bails_out_early_on_error(self): + """Resume action fails early if there are errors starting a service.""" + resume_calls = [] + + def maybe_kill(svc): + if svc == "haproxy": + return False + else: + resume_calls.append(svc) + return True + + self.service_resume.side_effect = maybe_kill + self.assertRaisesRegexp( + Exception, "haproxy didn't start cleanly.", + actions.actions.resume, self.args) + self.assertEqual(resume_calls, ['swift-proxy']) + + def test_status_mode(self): + """Resume action sets the status to maintenance.""" + status_calls = [] + self.status_set.side_effect = lambda state, msg: status_calls.append( + state) + + actions.actions.resume(self.args) + self.assertEqual(status_calls, ["active"]) + + def test_status_message(self): + """Resume action sets an empty status message.""" + status_calls = [] + self.status_set.side_effect = lambda state, msg: status_calls.append( + msg) + + actions.actions.resume(self.args) + self.assertEqual(status_calls, [""]) + + +class GetActionParserTestCase(unittest.TestCase): + + def test_definition_from_yaml(self): + """ArgumentParser is seeded from actions.yaml.""" + actions_yaml = tempfile.NamedTemporaryFile( + prefix="GetActionParserTestCase", suffix="yaml") + actions_yaml.write(yaml.dump({"foo": {"description": "Foo is bar"}})) + actions_yaml.seek(0) + parser = actions.actions.get_action_parser(actions_yaml.name, "foo", + get_services=lambda: []) + self.assertEqual(parser.description, 'Foo is bar') + + +class MainTestCase(CharmTestCase): + + def setUp(self): + super(MainTestCase, self).setUp( + actions.actions, ["_get_action_name", + "get_action_parser", + "action_fail"]) + + def test_invokes_pause(self): + dummy_calls = [] + + def dummy_action(args): + dummy_calls.append(True) + + self._get_action_name.side_effect = lambda: "foo" + self.get_action_parser = lambda: argparse.ArgumentParser() + with mock.patch.dict(actions.actions.ACTIONS, {"foo": dummy_action}): + actions.actions.main([]) + self.assertEqual(dummy_calls, [True]) + + def test_unknown_action(self): + """Unknown actions aren't a traceback.""" + self._get_action_name.side_effect = lambda: "foo" + self.get_action_parser = lambda: argparse.ArgumentParser() + exit_string = actions.actions.main([]) + self.assertEqual("Action foo undefined", exit_string) + + def test_failing_action(self): + """Actions which traceback trigger action_fail() calls.""" + dummy_calls = [] + + self.action_fail.side_effect = dummy_calls.append + self._get_action_name.side_effect = lambda: "foo" + + def dummy_action(args): + raise ValueError("uh oh") + + self.get_action_parser = lambda: argparse.ArgumentParser() + with mock.patch.dict(actions.actions.ACTIONS, {"foo": dummy_action}): + actions.actions.main([]) + self.assertEqual(dummy_calls, ["uh oh"])