Merge lp:~chad.smith/charm-helpers/resume-pause-idempotent into lp:charm-helpers

Proposed by Chad Smith
Status: Merged
Approved by: Stuart Bishop
Approved revision: 484
Merged at revision: 482
Proposed branch: lp:~chad.smith/charm-helpers/resume-pause-idempotent
Merge into: lp:charm-helpers
Diff against target: 284 lines (+176/-68)
3 files modified
VERSION (+1/-1)
charmhelpers/core/host.py (+6/-2)
tests/core/test_host.py (+169/-65)
To merge this branch: bzr merge lp:~chad.smith/charm-helpers/resume-pause-idempotent
Reviewer Review Type Date Requested Status
Stuart Bishop (community) Approve
charmers Pending
Review via email: mp+277637@code.launchpad.net

Description of the change

Minor tweak to service_resume and service_pause functions to allow them be idempotent. Supports possibility of successful repeated pause or resume retries if needed.

To post a comment you must log in.
Revision history for this message
Stuart Bishop (stub) wrote :

I don't think we need to worry about backwards compatibility here, which is good, since existing charms that would trigger the new behaviour are currently ending up in an error state.

Is service_running(foo) guaranteed to return true immediately after a start or false immediately after a stop? I don't think sysv makes any such guarantee (not sure about upstart), so I think service_start() will fail with daemons that take a while to startup and write their pid file.

I'd personally trust look-before-you-leap more, only calling stop if service_running() returns true and only calling start if service_running returns false etc. But I don't know what upstart guarantees (nor systemd, when we add xenial support).

review: Needs Information
Revision history for this message
Chad Smith (chad.smith) wrote :

Thanks for the review, I've updated the functions to check service_running first before calling start or stop appropriately.

Yeah, we'll have to add systemd support to this in short order.

483. By Chad Smith

update service_resume and service_pause to check if service_running before calling stop or start

484. By Chad Smith

fix logic on service_pause to call stop only when service is *not* running. Add/fix unit tests

Revision history for this message
Stuart Bishop (stub) wrote :

These changes look good.

I've made a couple of subjective stylistic comments inline, none of which are important enough to stop landing this as is.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'VERSION'
2--- VERSION 2015-08-04 22:20:26 +0000
3+++ VERSION 2015-11-17 20:21:43 +0000
4@@ -1,1 +1,1 @@
5-0.5.0
6+0.5.1
7
8=== modified file 'charmhelpers/core/host.py'
9--- charmhelpers/core/host.py 2015-10-26 09:53:06 +0000
10+++ charmhelpers/core/host.py 2015-11-17 20:21:43 +0000
11@@ -67,7 +67,9 @@
12 """Pause a system service.
13
14 Stop it, and prevent it from starting again at boot."""
15- stopped = service_stop(service_name)
16+ stopped = True
17+ if service_running(service_name):
18+ stopped = service_stop(service_name)
19 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
20 sysv_file = os.path.join(initd_dir, service_name)
21 if os.path.exists(upstart_file):
22@@ -105,7 +107,9 @@
23 "Unable to detect {0} as either Upstart {1} or SysV {2}".format(
24 service_name, upstart_file, sysv_file))
25
26- started = service_start(service_name)
27+ started = service_running(service_name)
28+ if not started:
29+ started = service_start(service_name)
30 return started
31
32
33
34=== modified file 'tests/core/test_host.py'
35--- tests/core/test_host.py 2015-10-26 09:53:06 +0000
36+++ tests/core/test_host.py 2015-11-17 20:21:43 +0000
37@@ -108,40 +108,94 @@
38
39 service.assert_called_with('restart', service_name)
40
41- @patch.object(host, 'service')
42- def test_pauses_an_upstart_service(self, service):
43- service_name = 'foo-service'
44- service.side_effect = [True]
45- tempdir = mkdtemp(prefix="test_pauses_an_upstart_service")
46- conf_path = os.path.join(tempdir, "{}.conf".format(service_name))
47- # Just needs to exist
48- with open(conf_path, "w") as fh:
49- fh.write("")
50- self.addCleanup(rmtree, tempdir)
51- self.assertTrue(host.service_pause(service_name, init_dir=tempdir))
52-
53- service.assert_called_with('stop', service_name)
54- override_path = os.path.join(
55- tempdir, "{}.override".format(service_name))
56- with open(override_path, "r") as fh:
57- override_contents = fh.read()
58- self.assertEqual("manual\n", override_contents)
59-
60- @patch('subprocess.check_call')
61- @patch.object(host, 'service')
62- def test_pauses_a_sysv_service(self, service, check_call):
63- service_name = 'foo-service'
64- service.side_effect = [True]
65- tempdir = mkdtemp(prefix="test_pauses_a_sysv_service")
66- sysv_path = os.path.join(tempdir, service_name)
67- # Just needs to exist
68- with open(sysv_path, "w") as fh:
69- fh.write("")
70- self.addCleanup(rmtree, tempdir)
71- self.assertTrue(host.service_pause(
72- service_name, init_dir=tempdir, initd_dir=tempdir))
73-
74- service.assert_called_with('stop', service_name)
75+ @patch('subprocess.check_output')
76+ @patch.object(host, 'service')
77+ def test_pauses_a_running_upstart_service(self, service, check_output):
78+ """Pause on a running service will call service stop."""
79+ check_output.return_value = b'foo-service start/running, process 123'
80+ service_name = 'foo-service'
81+ service.side_effect = [True]
82+ tempdir = mkdtemp(prefix="test_pauses_an_upstart_service")
83+ conf_path = os.path.join(tempdir, "{}.conf".format(service_name))
84+ # Just needs to exist
85+ with open(conf_path, "w") as fh:
86+ fh.write("")
87+ self.addCleanup(rmtree, tempdir)
88+ self.assertTrue(host.service_pause(service_name, init_dir=tempdir))
89+
90+ service.assert_called_with('stop', service_name)
91+ override_path = os.path.join(
92+ tempdir, "{}.override".format(service_name))
93+ with open(override_path, "r") as fh:
94+ override_contents = fh.read()
95+ self.assertEqual("manual\n", override_contents)
96+
97+ @patch('subprocess.check_output')
98+ @patch.object(host, 'service')
99+ def test_pauses_a_stopped_upstart_service(self, service, check_output):
100+ """Pause on a stopped service will not call service stop."""
101+ check_output.return_value = b'foo-service stop/waiting'
102+ service_name = 'foo-service'
103+ service.side_effect = [True]
104+ tempdir = mkdtemp(prefix="test_pauses_an_upstart_service")
105+ conf_path = os.path.join(tempdir, "{}.conf".format(service_name))
106+ # Just needs to exist
107+ with open(conf_path, "w") as fh:
108+ fh.write("")
109+ self.addCleanup(rmtree, tempdir)
110+ self.assertTrue(host.service_pause(service_name, init_dir=tempdir))
111+
112+ # Stop isn't called because service is already stopped
113+ self.assertRaises(
114+ AssertionError, service.assert_called_with, 'stop', service_name)
115+ override_path = os.path.join(
116+ tempdir, "{}.override".format(service_name))
117+ with open(override_path, "r") as fh:
118+ override_contents = fh.read()
119+ self.assertEqual("manual\n", override_contents)
120+
121+ @patch('subprocess.check_output')
122+ @patch('subprocess.check_call')
123+ @patch.object(host, 'service')
124+ def test_pauses_a_running_sysv_service(self, service, check_call,
125+ check_output):
126+ """Pause calls service stop on a running sysv service."""
127+ check_output.return_value = b'foo-service start/running, process 123'
128+ service_name = 'foo-service'
129+ service.side_effect = [True]
130+ tempdir = mkdtemp(prefix="test_pauses_a_sysv_service")
131+ sysv_path = os.path.join(tempdir, service_name)
132+ # Just needs to exist
133+ with open(sysv_path, "w") as fh:
134+ fh.write("")
135+ self.addCleanup(rmtree, tempdir)
136+ self.assertTrue(host.service_pause(
137+ service_name, init_dir=tempdir, initd_dir=tempdir))
138+
139+ service.assert_called_with('stop', service_name)
140+ check_call.assert_called_with(["update-rc.d", service_name, "disable"])
141+
142+ @patch('subprocess.check_output')
143+ @patch('subprocess.check_call')
144+ @patch.object(host, 'service')
145+ def test_pauses_a_stopped_sysv_service(self, service, check_call,
146+ check_output):
147+ """Pause does not call service stop on a stopped sysv service."""
148+ check_output.return_value = b'foo-service stop/waiting'
149+ service_name = 'foo-service'
150+ service.side_effect = [True]
151+ tempdir = mkdtemp(prefix="test_pauses_a_sysv_service")
152+ sysv_path = os.path.join(tempdir, service_name)
153+ # Just needs to exist
154+ with open(sysv_path, "w") as fh:
155+ fh.write("")
156+ self.addCleanup(rmtree, tempdir)
157+ self.assertTrue(host.service_pause(
158+ service_name, init_dir=tempdir, initd_dir=tempdir))
159+
160+ # Stop isn't called because service is already stopped
161+ self.assertRaises(
162+ AssertionError, service.assert_called_with, 'stop', service_name)
163 check_call.assert_called_with(["update-rc.d", service_name, "disable"])
164
165 @patch.object(host, 'service')
166@@ -157,37 +211,87 @@
167 "Unable to detect {0}".format(service_name), str(exception))
168 self.assertIn(tempdir, str(exception))
169
170- @patch.object(host, 'service')
171- def test_resumes_an_upstart_service(self, service):
172- service_name = 'foo-service'
173- service.side_effect = [True]
174- tempdir = mkdtemp(prefix="test_resumes_an_upstart_service")
175- conf_path = os.path.join(tempdir, "{}.conf".format(service_name))
176- with open(conf_path, "w") as fh:
177- fh.write("")
178- self.addCleanup(rmtree, tempdir)
179- self.assertTrue(host.service_resume(service_name, init_dir=tempdir))
180-
181- service.assert_called_with('start', service_name)
182- override_path = os.path.join(
183- tempdir, "{}.override".format(service_name))
184- self.assertFalse(os.path.exists(override_path))
185-
186- @patch('subprocess.check_call')
187- @patch.object(host, 'service')
188- def test_resumes_a_sysv_service(self, service, check_call):
189- service_name = 'foo-service'
190- service.side_effect = [True]
191- tempdir = mkdtemp(prefix="test_resumes_a_sysv_service")
192- sysv_path = os.path.join(tempdir, service_name)
193- # Just needs to exist
194- with open(sysv_path, "w") as fh:
195- fh.write("")
196- self.addCleanup(rmtree, tempdir)
197- self.assertTrue(host.service_resume(
198- service_name, init_dir=tempdir, initd_dir=tempdir))
199-
200- service.assert_called_with('start', service_name)
201+ @patch('subprocess.check_output')
202+ @patch.object(host, 'service')
203+ def test_resumes_a_running_upstart_service(self, service, check_output):
204+ """When the service is already running, service start isn't called."""
205+ check_output.return_value = b'foo-service start/running, process 111'
206+ service_name = 'foo-service'
207+ service.side_effect = [True]
208+ tempdir = mkdtemp(prefix="test_resumes_an_upstart_service")
209+ conf_path = os.path.join(tempdir, "{}.conf".format(service_name))
210+ with open(conf_path, "w") as fh:
211+ fh.write("")
212+ self.addCleanup(rmtree, tempdir)
213+ self.assertTrue(host.service_resume(service_name, init_dir=tempdir))
214+
215+ # Start isn't called because service is already running
216+ self.assertRaises(
217+ AssertionError, service.assert_called_with, 'start', service_name)
218+ override_path = os.path.join(
219+ tempdir, "{}.override".format(service_name))
220+ self.assertFalse(os.path.exists(override_path))
221+
222+ @patch('subprocess.check_output')
223+ @patch.object(host, 'service')
224+ def test_resumes_a_stopped_upstart_service(self, service, check_output):
225+ """When the service is stopped, service start is called."""
226+ check_output.return_value = b'foo-service stop/waiting'
227+ service_name = 'foo-service'
228+ service.side_effect = [True]
229+ tempdir = mkdtemp(prefix="test_resumes_an_upstart_service")
230+ conf_path = os.path.join(tempdir, "{}.conf".format(service_name))
231+ with open(conf_path, "w") as fh:
232+ fh.write("")
233+ self.addCleanup(rmtree, tempdir)
234+ self.assertTrue(host.service_resume(service_name, init_dir=tempdir))
235+
236+ service.assert_called_with('start', service_name)
237+ override_path = os.path.join(
238+ tempdir, "{}.override".format(service_name))
239+ self.assertFalse(os.path.exists(override_path))
240+
241+ @patch('subprocess.check_output')
242+ @patch('subprocess.check_call')
243+ @patch.object(host, 'service')
244+ def test_resumes_a_sysv_service(self, service, check_call, check_output):
245+ """When process is in a stop/waiting state, service start is called."""
246+ check_output.return_value = b'foo-service stop/waiting'
247+ service_name = 'foo-service'
248+ service.side_effect = [True]
249+ tempdir = mkdtemp(prefix="test_resumes_a_sysv_service")
250+ sysv_path = os.path.join(tempdir, service_name)
251+ # Just needs to exist
252+ with open(sysv_path, "w") as fh:
253+ fh.write("")
254+ self.addCleanup(rmtree, tempdir)
255+ self.assertTrue(host.service_resume(
256+ service_name, init_dir=tempdir, initd_dir=tempdir))
257+
258+ service.assert_called_with('start', service_name)
259+ check_call.assert_called_with(["update-rc.d", service_name, "enable"])
260+
261+ @patch('subprocess.check_output')
262+ @patch('subprocess.check_call')
263+ @patch.object(host, 'service')
264+ def test_resume_a_running_sysv_service(self, service, check_call,
265+ check_output):
266+ """When process is already running, service start isn't called."""
267+ check_output.return_value = b'foo-service start/running, process 123'
268+ service_name = 'foo-service'
269+ service.side_effect = [True]
270+ tempdir = mkdtemp(prefix="test_resumes_a_sysv_service")
271+ sysv_path = os.path.join(tempdir, service_name)
272+ # Just needs to exist
273+ with open(sysv_path, "w") as fh:
274+ fh.write("")
275+ self.addCleanup(rmtree, tempdir)
276+ self.assertTrue(host.service_resume(
277+ service_name, init_dir=tempdir, initd_dir=tempdir))
278+
279+ # Start isn't called because service is already running
280+ self.assertRaises(
281+ AssertionError, service.assert_called_with, 'start', service_name)
282 check_call.assert_called_with(["update-rc.d", service_name, "enable"])
283
284 @patch.object(host, 'service')

Subscribers

People subscribed via source and target branches