Merge lp:~adam-collard/charms/trusty/swift-proxy/add-pause-resume-actions into lp:~openstack-charmers-archive/charms/trusty/swift-proxy/next

Proposed by Adam Collard
Status: Merged
Merged at revision: 109
Proposed branch: lp:~adam-collard/charms/trusty/swift-proxy/add-pause-resume-actions
Merge into: lp:~openstack-charmers-archive/charms/trusty/swift-proxy/next
Diff against target: 493 lines (+395/-19)
7 files modified
actions.yaml (+11/-0)
actions/actions.py (+83/-0)
charmhelpers/core/host.py (+32/-16)
metadata.yaml (+2/-1)
tests/basic_deployment.py (+54/-2)
tests/charmhelpers/contrib/amulet/utils.py (+9/-0)
unit_tests/test_actions.py (+204/-0)
To merge this branch: bzr merge lp:~adam-collard/charms/trusty/swift-proxy/add-pause-resume-actions
Reviewer Review Type Date Requested Status
Chris Glass (community) Approve
Geoff Teale (community) Approve
Review via email: mp+270407@code.launchpad.net

Description of the change

This branch adds actions for pausing and resuming services on swift-storage units.

In addition to just stopping the services, the actions also set the status of the unit to be "maintenance" with a message on how to resume.

Further refinements to the charm to support pause mode will be coming in follow-up branches, notably guarding calls to service_start and service_restart to prevent config-changed, *-relation-changed and other hooks from (re)-starting a unit which should be paused.

It's a mirror of https://code.launchpad.net/~adam-collard/charms/trusty/swift-storage/add-pause-resume-actions/+merge/268233

Note that this depends on charm-helpers from https://code.launchpad.net/~tealeg/charm-helpers/add-amulet-utils-status-get/+merge/270311

To post a comment you must log in.
Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_lint_check #9595 swift-proxy-next for adam-collard mp270407
    LINT OK: passed

Build: http://10.245.162.77:8080/job/charm_lint_check/9595/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_unit_test #8833 swift-proxy-next for adam-collard mp270407
    UNIT OK: passed

Build: http://10.245.162.77:8080/job/charm_unit_test/8833/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_amulet_test #6319 swift-proxy-next for adam-collard mp270407
    AMULET OK: passed

Build: http://10.245.162.77:8080/job/charm_amulet_test/6319/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_unit_test #8834 swift-proxy-next for adam-collard mp270407
    UNIT OK: passed

Build: http://10.245.162.77:8080/job/charm_unit_test/8834/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_lint_check #9596 swift-proxy-next for adam-collard mp270407
    LINT OK: passed

Build: http://10.245.162.77:8080/job/charm_lint_check/9596/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_amulet_test #6320 swift-proxy-next for adam-collard mp270407
    AMULET OK: passed

Build: http://10.245.162.77:8080/job/charm_amulet_test/6320/

Revision history for this message
Geoff Teale (tealeg) wrote :

+1 Sometimes the unit-test + functional-test feels a bit belt-and-braces, but I suppose that's a good thing.

I'd like to see the status get tested in the functional side (As per the in line comment) but that can wait until it's all landed in charmhelpers.

review: Approve
114. By Adam Collard

Sync from tealeg's charm-helpers branch, add tests for status (tealeg's review)

Revision history for this message
Adam Collard (adam-collard) :
Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_lint_check #9708 swift-proxy-next for adam-collard mp270407
    LINT FAIL: lint-test failed
    LINT FAIL: charm-proof failed

LINT Results (max last 2 lines):
make: *** [lint] Error 100
ERROR:root:Make target returned non-zero.

Full lint test output: http://paste.ubuntu.com/12328244/
Build: http://10.245.162.77:8080/job/charm_lint_check/9708/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_unit_test #8937 swift-proxy-next for adam-collard mp270407
    UNIT OK: passed

Build: http://10.245.162.77:8080/job/charm_unit_test/8937/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_amulet_test #6333 swift-proxy-next for adam-collard mp270407
    AMULET OK: passed

Build: http://10.245.162.77:8080/job/charm_amulet_test/6333/

115. By Adam Collard

Rename categories to tags (please the linter)

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_lint_check #9715 swift-proxy-next for adam-collard mp270407
    LINT OK: passed

Build: http://10.245.162.77:8080/job/charm_lint_check/9715/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_unit_test #8948 swift-proxy-next for adam-collard mp270407
    UNIT OK: passed

Build: http://10.245.162.77:8080/job/charm_unit_test/8948/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_amulet_test #6346 swift-proxy-next for adam-collard mp270407
    AMULET OK: passed

Build: http://10.245.162.77:8080/job/charm_amulet_test/6346/

Revision history for this message
Chris Glass (tribaal) wrote :

Looks good! +1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added directory 'actions'
=== added file 'actions.yaml'
--- actions.yaml 1970-01-01 00:00:00 +0000
+++ actions.yaml 2015-09-10 14:11:09 +0000
@@ -0,0 +1,11 @@
1pause:
2 description: |
3 Pause swift-proxy services.
4 If the swift-proxy deployment is clustered using the hacluster charm, the
5 corresponding hacluster unit on the node must first be paused as well.
6 Not doing so may lead to an interruption of service.
7resume:
8 description: |
9 Resume swift-proxy services.
10 If the swift-proxy deployment is clustered using the hacluster charm, the
11 corresponding hacluster unit on the node must be resumed as well.
012
=== added file 'actions/__init__.py'
=== added file 'actions/actions.py'
--- actions/actions.py 1970-01-01 00:00:00 +0000
+++ actions/actions.py 2015-09-10 14:11:09 +0000
@@ -0,0 +1,83 @@
1#!/usr/bin/python
2
3import argparse
4import os
5import sys
6import yaml
7
8from charmhelpers.core.host import service_pause, service_resume
9from charmhelpers.core.hookenv import action_fail, status_set
10
11from lib.swift_utils import services
12
13
14def get_action_parser(actions_yaml_path, action_name,
15 get_services=services):
16 """Make an argparse.ArgumentParser seeded from actions.yaml definitions."""
17 with open(actions_yaml_path) as fh:
18 doc = yaml.load(fh)[action_name]["description"]
19 parser = argparse.ArgumentParser(description=doc)
20 parser.add_argument("--services", default=get_services())
21 # TODO: Add arguments for params defined in the actions.yaml
22 return parser
23
24
25def pause(args):
26 """Pause all the swift services.
27
28 @raises Exception if any services fail to stop
29 """
30 for service in args.services:
31 stopped = service_pause(service)
32 if not stopped:
33 raise Exception("{} didn't stop cleanly.".format(service))
34 status_set(
35 "maintenance", "Paused. Use 'resume' action to resume normal service.")
36
37
38def resume(args):
39 """Resume all the swift services.
40
41 @raises Exception if any services fail to start
42 """
43 for service in args.services:
44 started = service_resume(service)
45 if not started:
46 raise Exception("{} didn't start cleanly.".format(service))
47 status_set("active", "")
48
49
50# A dictionary of all the defined actions to callables (which take
51# parsed arguments).
52ACTIONS = {"pause": pause, "resume": resume}
53
54
55def main(argv):
56 action_name = _get_action_name()
57 actions_yaml_path = _get_actions_yaml_path()
58 parser = get_action_parser(actions_yaml_path, action_name)
59 args = parser.parse_args(argv)
60 try:
61 action = ACTIONS[action_name]
62 except KeyError:
63 return "Action %s undefined" % action_name
64 else:
65 try:
66 action(args)
67 except Exception as e:
68 action_fail(str(e))
69
70
71def _get_action_name():
72 """Return the name of the action."""
73 return os.path.basename(__file__)
74
75
76def _get_actions_yaml_path():
77 """Return the path to actions.yaml"""
78 cwd = os.path.dirname(__file__)
79 return os.path.join(cwd, "..", "actions.yaml")
80
81
82if __name__ == "__main__":
83 sys.exit(main(sys.argv[1:]))
084
=== added symlink 'actions/charmhelpers'
=== target is u'../charmhelpers/'
=== added symlink 'actions/lib'
=== target is u'../lib/'
=== added symlink 'actions/pause'
=== target is u'actions.py'
=== added symlink 'actions/resume'
=== target is u'actions.py'
=== modified file 'charmhelpers/core/host.py'
--- charmhelpers/core/host.py 2015-08-19 13:49:37 +0000
+++ charmhelpers/core/host.py 2015-09-10 14:11:09 +0000
@@ -63,32 +63,48 @@
63 return service_result63 return service_result
6464
6565
66def service_pause(service_name, init_dir=None):66def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"):
67 """Pause a system service.67 """Pause a system service.
6868
69 Stop it, and prevent it from starting again at boot."""69 Stop it, and prevent it from starting again at boot."""
70 if init_dir is None:
71 init_dir = "/etc/init"
72 stopped = service_stop(service_name)70 stopped = service_stop(service_name)
73 # XXX: Support systemd too71 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
74 override_path = os.path.join(72 sysv_file = os.path.join(initd_dir, service_name)
75 init_dir, '{}.override'.format(service_name))73 if os.path.exists(upstart_file):
76 with open(override_path, 'w') as fh:74 override_path = os.path.join(
77 fh.write("manual\n")75 init_dir, '{}.override'.format(service_name))
76 with open(override_path, 'w') as fh:
77 fh.write("manual\n")
78 elif os.path.exists(sysv_file):
79 subprocess.check_call(["update-rc.d", service_name, "disable"])
80 else:
81 # XXX: Support SystemD too
82 raise ValueError(
83 "Unable to detect {0} as either Upstart {1} or SysV {2}".format(
84 service_name, upstart_file, sysv_file))
78 return stopped85 return stopped
7986
8087
81def service_resume(service_name, init_dir=None):88def service_resume(service_name, init_dir="/etc/init",
89 initd_dir="/etc/init.d"):
82 """Resume a system service.90 """Resume a system service.
8391
84 Reenable starting again at boot. Start the service"""92 Reenable starting again at boot. Start the service"""
85 # XXX: Support systemd too93 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
86 if init_dir is None:94 sysv_file = os.path.join(initd_dir, service_name)
87 init_dir = "/etc/init"95 if os.path.exists(upstart_file):
88 override_path = os.path.join(96 override_path = os.path.join(
89 init_dir, '{}.override'.format(service_name))97 init_dir, '{}.override'.format(service_name))
90 if os.path.exists(override_path):98 if os.path.exists(override_path):
91 os.unlink(override_path)99 os.unlink(override_path)
100 elif os.path.exists(sysv_file):
101 subprocess.check_call(["update-rc.d", service_name, "enable"])
102 else:
103 # XXX: Support SystemD too
104 raise ValueError(
105 "Unable to detect {0} as either Upstart {1} or SysV {2}".format(
106 service_name, upstart_file, sysv_file))
107
92 started = service_start(service_name)108 started = service_start(service_name)
93 return started109 return started
94110
95111
=== modified file 'metadata.yaml'
--- metadata.yaml 2014-10-30 03:30:36 +0000
+++ metadata.yaml 2015-09-10 14:11:09 +0000
@@ -4,7 +4,8 @@
4description: |4description: |
5 Swift is a distributed virtual object store. This formula deploys the proxy node5 Swift is a distributed virtual object store. This formula deploys the proxy node
6 to be related to storage nodes.6 to be related to storage nodes.
7categories:7tags:
8 - openstack
8 - cache-proxy9 - cache-proxy
9provides:10provides:
10 nrpe-external-master:11 nrpe-external-master:
1112
=== modified file 'tests/basic_deployment.py'
--- tests/basic_deployment.py 2015-04-20 11:17:45 +0000
+++ tests/basic_deployment.py 2015-09-10 14:11:09 +0000
@@ -1,5 +1,3 @@
1#!/usr/bin/python
2
3import amulet1import amulet
4import swiftclient2import swiftclient
53
@@ -335,6 +333,60 @@
335333
336 self.d.configure('swift-proxy', {'node-timeout': '60'})334 self.d.configure('swift-proxy', {'node-timeout': '60'})
337335
336 def _assert_services(self, should_run):
337 swift_proxy_services = ['swift-proxy-server',
338 'haproxy',
339 'apache2',
340 'memcached']
341 u.get_unit_process_ids(
342 {self.swift_proxy_sentry: swift_proxy_services},
343 expect_success=should_run)
344 # No point using validate_unit_process_ids, since we don't
345 # care about how many PIDs, merely that they're running, so
346 # would populate expected with either True or False. This
347 # validation is already performed in get_process_id_list
348
349 def _test_pause(self):
350 u.log.info("Testing pause action")
351 self._assert_services(should_run=True)
352 pause_action_id = u.run_action(self.swift_proxy_sentry, "pause")
353 assert u.wait_on_action(pause_action_id), "Pause action failed."
354
355 self._assert_services(should_run=False)
356 status, message = u.status_get(self.swift_proxy_sentry)
357 if status != "maintenance":
358 msg = ("Pause action failed to move unit to maintenance "
359 "status (got {} instead)".format(status))
360 amulet.raise_status(amulet.FAIL, msg=msg)
361 if message != "Paused. Use 'resume' action to resume normal service.":
362 msg = ("Pause action failed to set message"
363 " (got {} instead)".format(message))
364 amulet.raise_status(amulet.FAIL, msg=msg)
365
366 def _test_resume(self):
367 u.log.info("Testing resume action")
368 # service is left paused by _test_pause
369 self._assert_services(should_run=False)
370 resume_action_id = u.run_action(self.swift_proxy_sentry, "resume")
371 assert u.wait_on_action(resume_action_id), "Resume action failed."
372
373 self._assert_services(should_run=True)
374 status, message = u.status_get(self.swift_proxy_sentry)
375 if status != "active":
376 msg = ("Resume action failed to move unit to active "
377 "status (got {} instead)".format(status))
378 amulet.raise_status(amulet.FAIL, msg=msg)
379 if message != "":
380 msg = ("Resume action failed to clear message"
381 " (got {} instead)".format(message))
382 amulet.raise_status(amulet.FAIL, msg=msg)
383
384 def test_z_pause_resume_actions(self):
385 """Pause and then resume swift-proxy."""
386 u.log.debug('Checking pause/resume actions...')
387 self._test_pause()
388 self._test_resume()
389
338 def test_swift_config(self):390 def test_swift_config(self):
339 """Verify the data in the swift config file."""391 """Verify the data in the swift config file."""
340 unit = self.swift_proxy_sentry392 unit = self.swift_proxy_sentry
341393
=== modified file 'tests/charmhelpers/contrib/amulet/utils.py'
--- tests/charmhelpers/contrib/amulet/utils.py 2015-08-18 17:34:36 +0000
+++ tests/charmhelpers/contrib/amulet/utils.py 2015-09-10 14:11:09 +0000
@@ -594,3 +594,12 @@
594 output = _check_output(command, universal_newlines=True)594 output = _check_output(command, universal_newlines=True)
595 data = json.loads(output)595 data = json.loads(output)
596 return data.get(u"status") == "completed"596 return data.get(u"status") == "completed"
597
598 def status_get(self, unit):
599 """Return the current service status of this unit."""
600 raw_status, return_code = unit.run(
601 "status-get --format=json --include-data")
602 if return_code != 0:
603 return ("unknown", "")
604 status = json.loads(raw_status)
605 return (status["status"], status["message"])
597606
=== added file 'unit_tests/test_actions.py'
--- unit_tests/test_actions.py 1970-01-01 00:00:00 +0000
+++ unit_tests/test_actions.py 2015-09-10 14:11:09 +0000
@@ -0,0 +1,204 @@
1import argparse
2import tempfile
3import unittest
4
5import mock
6import yaml
7
8import actions.actions
9
10
11class CharmTestCase(unittest.TestCase):
12
13 def setUp(self, obj, patches):
14 super(CharmTestCase, self).setUp()
15 self.patches = patches
16 self.obj = obj
17 self.patch_all()
18
19 def patch(self, method):
20 _m = mock.patch.object(self.obj, method)
21 mocked = _m.start()
22 self.addCleanup(_m.stop)
23 return mocked
24
25 def patch_all(self):
26 for method in self.patches:
27 setattr(self, method, self.patch(method))
28
29
30class PauseTestCase(CharmTestCase):
31
32 def setUp(self):
33 super(PauseTestCase, self).setUp(
34 actions.actions, ["service_pause", "status_set"])
35
36 class FakeArgs(object):
37 services = ['swift-proxy', 'haproxy', 'memcached', 'apache2']
38 self.args = FakeArgs()
39
40 def test_pauses_services(self):
41 """Pause action pauses all of the Swift services."""
42 pause_calls = []
43
44 def fake_service_pause(svc):
45 pause_calls.append(svc)
46 return True
47
48 self.service_pause.side_effect = fake_service_pause
49
50 actions.actions.pause(self.args)
51 self.assertEqual(
52 pause_calls, ['swift-proxy', 'haproxy', 'memcached', 'apache2'])
53
54 def test_bails_out_early_on_error(self):
55 """Pause action fails early if there are errors stopping a service."""
56 pause_calls = []
57
58 def maybe_kill(svc):
59 if svc == "haproxy":
60 return False
61 else:
62 pause_calls.append(svc)
63 return True
64
65 self.service_pause.side_effect = maybe_kill
66 self.assertRaisesRegexp(
67 Exception, "haproxy didn't stop cleanly.",
68 actions.actions.pause, self.args)
69 self.assertEqual(pause_calls, ["swift-proxy"])
70
71 def test_status_mode(self):
72 """Pause action sets the status to maintenance."""
73 status_calls = []
74 self.status_set.side_effect = lambda state, msg: status_calls.append(
75 state)
76
77 actions.actions.pause(self.args)
78 self.assertEqual(status_calls, ["maintenance"])
79
80 def test_status_message(self):
81 """Pause action sets a status message reflecting that it's paused."""
82 status_calls = []
83 self.status_set.side_effect = lambda state, msg: status_calls.append(
84 msg)
85
86 actions.actions.pause(self.args)
87 self.assertEqual(
88 status_calls, ["Paused. "
89 "Use 'resume' action to resume normal service."])
90
91
92class ResumeTestCase(CharmTestCase):
93
94 def setUp(self):
95 super(ResumeTestCase, self).setUp(
96 actions.actions, ["service_resume", "status_set"])
97
98 class FakeArgs(object):
99 services = ['swift-proxy', 'haproxy', 'memcached', 'apache2']
100 self.args = FakeArgs()
101
102 def test_resumes_services(self):
103 """Resume action resumes all of the Swift services."""
104 resume_calls = []
105
106 def fake_service_resume(svc):
107 resume_calls.append(svc)
108 return True
109
110 self.service_resume.side_effect = fake_service_resume
111 actions.actions.resume(self.args)
112 self.assertEqual(
113 resume_calls, ['swift-proxy', 'haproxy', 'memcached', 'apache2'])
114
115 def test_bails_out_early_on_error(self):
116 """Resume action fails early if there are errors starting a service."""
117 resume_calls = []
118
119 def maybe_kill(svc):
120 if svc == "haproxy":
121 return False
122 else:
123 resume_calls.append(svc)
124 return True
125
126 self.service_resume.side_effect = maybe_kill
127 self.assertRaisesRegexp(
128 Exception, "haproxy didn't start cleanly.",
129 actions.actions.resume, self.args)
130 self.assertEqual(resume_calls, ['swift-proxy'])
131
132 def test_status_mode(self):
133 """Resume action sets the status to maintenance."""
134 status_calls = []
135 self.status_set.side_effect = lambda state, msg: status_calls.append(
136 state)
137
138 actions.actions.resume(self.args)
139 self.assertEqual(status_calls, ["active"])
140
141 def test_status_message(self):
142 """Resume action sets an empty status message."""
143 status_calls = []
144 self.status_set.side_effect = lambda state, msg: status_calls.append(
145 msg)
146
147 actions.actions.resume(self.args)
148 self.assertEqual(status_calls, [""])
149
150
151class GetActionParserTestCase(unittest.TestCase):
152
153 def test_definition_from_yaml(self):
154 """ArgumentParser is seeded from actions.yaml."""
155 actions_yaml = tempfile.NamedTemporaryFile(
156 prefix="GetActionParserTestCase", suffix="yaml")
157 actions_yaml.write(yaml.dump({"foo": {"description": "Foo is bar"}}))
158 actions_yaml.seek(0)
159 parser = actions.actions.get_action_parser(actions_yaml.name, "foo",
160 get_services=lambda: [])
161 self.assertEqual(parser.description, 'Foo is bar')
162
163
164class MainTestCase(CharmTestCase):
165
166 def setUp(self):
167 super(MainTestCase, self).setUp(
168 actions.actions, ["_get_action_name",
169 "get_action_parser",
170 "action_fail"])
171
172 def test_invokes_pause(self):
173 dummy_calls = []
174
175 def dummy_action(args):
176 dummy_calls.append(True)
177
178 self._get_action_name.side_effect = lambda: "foo"
179 self.get_action_parser = lambda: argparse.ArgumentParser()
180 with mock.patch.dict(actions.actions.ACTIONS, {"foo": dummy_action}):
181 actions.actions.main([])
182 self.assertEqual(dummy_calls, [True])
183
184 def test_unknown_action(self):
185 """Unknown actions aren't a traceback."""
186 self._get_action_name.side_effect = lambda: "foo"
187 self.get_action_parser = lambda: argparse.ArgumentParser()
188 exit_string = actions.actions.main([])
189 self.assertEqual("Action foo undefined", exit_string)
190
191 def test_failing_action(self):
192 """Actions which traceback trigger action_fail() calls."""
193 dummy_calls = []
194
195 self.action_fail.side_effect = dummy_calls.append
196 self._get_action_name.side_effect = lambda: "foo"
197
198 def dummy_action(args):
199 raise ValueError("uh oh")
200
201 self.get_action_parser = lambda: argparse.ArgumentParser()
202 with mock.patch.dict(actions.actions.ACTIONS, {"foo": dummy_action}):
203 actions.actions.main([])
204 self.assertEqual(dummy_calls, ["uh oh"])

Subscribers

People subscribed via source and target branches