Merge lp:~danilo/landscape-charm/maintenance-hook into lp:~landscape/landscape-charm/trunk

Proposed by Данило Шеган
Status: Merged
Approved by: Данило Шеган
Approved revision: 303
Merged at revision: 297
Proposed branch: lp:~danilo/landscape-charm/maintenance-hook
Merge into: lp:~landscape/landscape-charm/trunk
Prerequisite: lp:~danilo/landscape-charm/upgrade-only-in-maintenance
Diff against target: 298 lines (+138/-30)
8 files modified
lib/hook.py (+28/-0)
lib/migrate_schema.py (+8/-7)
lib/resume.py (+8/-7)
lib/tests/stubs.py (+4/-0)
lib/tests/test_hook.py (+45/-0)
lib/tests/test_migrate_schema.py (+22/-5)
lib/tests/test_resume.py (+20/-3)
lib/upgrade.py (+3/-8)
To merge this branch: bzr merge lp:~danilo/landscape-charm/maintenance-hook
Reviewer Review Type Date Requested Status
Benji York (community) Approve
🤖 Landscape Builder test results Approve
Chris Glass (community) Approve
Review via email: mp+260697@code.launchpad.net

Commit message

Make a generic MaintenanceHook which errors out if not in maintenance mode

Also moves UpgradeAction, MigrateSchemaAction and ResumeAction to the new hook.

Description of the change

Make a generic MaintenanceHook which errors out if not in maintenance mode

Also moves UpgradeAction, MigrateSchemaAction and ResumeAction to the new hook.

For testing, use landscape.yaml from https://pastebin.canonical.com/132332/ and deploy with

  juju-deployer -vdW -w180 -c landscape.yaml landscape

When playing around, note that if any of the 'upgrade', 'migrate-schema' or 'resume' are run when not in maintenance (iow, after 'pause' action), they should error out and no-op.

To post a comment you must log in.
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: make ci-test
Result: Success
Revno: 300
Branch: lp:~danilo/landscape-charm/maintenance-hook
Jenkins: https://ci.lscape.net/job/latch-test/1147/

review: Approve (test results)
301. By Данило Шеган

Make error message more generic.

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

Looks good! +1

review: Approve
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: make ci-test
Result: Success
Revno: 301
Branch: lp:~danilo/landscape-charm/maintenance-hook
Jenkins: https://ci.lscape.net/job/latch-test/1148/

review: Approve (test results)
Revision history for this message
Björn Tillenius (bjornt) :
302. By Данило Шеган

Merged upgrade-only-in-maintenance into maintenance-hook.

Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: make ci-test
Result: Success
Revno: 302
Branch: lp:~danilo/landscape-charm/maintenance-hook
Jenkins: https://ci.lscape.net/job/latch-test/1150/

review: Approve (test results)
Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :

I'd rather implement some state-tracking/state-machine code in the charm (in a similar way we currently do for leader info, e.g storing a file with the current state), without relying on the maintenance flag file, which is kind of an implementation detail of the server. Later down the road (with juju 1.24) we'll use unit status for this, but for now tracking the state at the charm level will get us closer to that.

Also, rather than exiting the hook with non-zero, we should code "action-fail":

https://jujucharms.com/docs/devel/authors-charm-actions#action-fail

(might need a change in charmhelpers to support the new actions executables).

Revision history for this message
Benji York (benji) wrote :

This branch looks good to me.

review: Approve
Revision history for this message
Данило Шеган (danilo) wrote :

@Free, Bjorn: I'll move to action_fail (it's already there in core.hookenv.action_fail).

As for not using maintenance file, the entire goal of this branch is to move all dependencies on maint-file into a single place, which we can easily change (I was thinking of future move to juju unit status, tbh: I would only implement internal representation of status if we really need it, and I am not sure yet if we do, for eg. waiting for batch scripts to finish).

As for internal implementation details of the server, we already rely heavily on those (we expect "lsctl stop" to stop cron scripts—if that's not an implementation detail, I do not know what is :)). But I don't see this as a major issue with the charm, since it's heavily tied with the server code, and what is the public API for the server code is what we decide is the API.

Thanks for reviews, all. I'm testing with action_fail now and will land if that goes fine. As I said, the goal of this branch is to allow easier change to whatever new machinism of tracking state we switch to, and thus believe it's valuable regardless of the fact that the approach is not perfect today.

303. By Данило Шеган

Switch to action_fail for MaintenanceHook.

Revision history for this message
Björn Tillenius (bjornt) wrote :

On Tue, Jun 02, 2015 at 07:02:10AM -0000, Данило Шеган wrote:
> @Free, Bjorn: I'll move to action_fail (it's already there in core.hookenv.action_fail).

Thanks.

> As for not using maintenance file, the entire goal of this branch is
> to move all dependencies on maint-file into a single place, which we can
> easily change (I was thinking of future move to juju unit status, tbh: I
> would only implement internal representation of status if we really need
> it, and I am not sure yet if we do, for eg. waiting for batch scripts to
> finish).

I think using the maintenance file is fine. Using it ensures that we
don't do anything bad if some other admin decided to start the servers
and removing the maintenance file. We should detect that situation
rather than ignore it.

When we have Juju unit status, we should check that as well, but I don't
see a reason not to check the maintenance file.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/hook.py'
2--- lib/hook.py 2015-01-28 11:53:18 +0000
3+++ lib/hook.py 2015-06-02 07:06:39 +0000
4@@ -1,6 +1,10 @@
5+import os.path
6+
7 from charmhelpers.core import hookenv
8 from charmhelpers.core.hookenv import ERROR
9
10+from lib.paths import default_paths
11+
12
13 class HookError(Exception):
14 """Raised by hooks when they want to fail.
15@@ -35,3 +39,27 @@
16 def _run(self):
17 """Do the job."""
18 raise NotImplementedError("Must be implemented by sub-classes")
19+
20+
21+class MaintenanceHook(Hook):
22+ """Hook that only runs when in maintenance mode."""
23+
24+ def __init__(self, hookenv=hookenv, paths=default_paths):
25+ """
26+ @param hookenv: The charm-helpers C{hookenv} module, will be replaced
27+ by tests.
28+ @param paths: The landscape-server default paths class.
29+ """
30+ self._hookenv = hookenv
31+ self._paths = paths
32+
33+ def __call__(self):
34+ """Invoke the hook.
35+
36+ @return: An integer with the exit code for the hook.
37+ """
38+ if not os.path.exists(self._paths.maintenance_flag()):
39+ self._hookenv.action_fail(
40+ "This action can only be called on a unit in paused state.")
41+ return 1
42+ super(MaintenanceHook, self).__call__()
43
44=== modified file 'lib/migrate_schema.py'
45--- lib/migrate_schema.py 2015-05-21 07:31:32 +0000
46+++ lib/migrate_schema.py 2015-06-02 07:06:39 +0000
47@@ -2,15 +2,16 @@
48
49 from charmhelpers.core import hookenv
50
51-from lib.hook import Hook
52-from lib.paths import SCHEMA_SCRIPT
53-
54-
55-class MigrateSchemaAction(Hook):
56+from lib.hook import MaintenanceHook
57+from lib.paths import default_paths, SCHEMA_SCRIPT
58+
59+
60+class MigrateSchemaAction(MaintenanceHook):
61 """Execute schema upgrade action logic."""
62
63- def __init__(self, hookenv=hookenv, subprocess=subprocess):
64- super(MigrateSchemaAction, self).__init__(hookenv=hookenv)
65+ def __init__(self, hookenv=hookenv, paths=default_paths,
66+ subprocess=subprocess):
67+ super(MigrateSchemaAction, self).__init__(hookenv=hookenv, paths=paths)
68 self._subprocess = subprocess
69
70 def _run(self):
71
72=== modified file 'lib/resume.py'
73--- lib/resume.py 2015-05-29 09:25:24 +0000
74+++ lib/resume.py 2015-06-02 07:06:39 +0000
75@@ -2,15 +2,16 @@
76
77 from charmhelpers.core import hookenv
78
79-from lib.hook import Hook
80-from lib.paths import LSCTL
81-
82-
83-class ResumeAction(Hook):
84+from lib.hook import MaintenanceHook
85+from lib.paths import default_paths, LSCTL
86+
87+
88+class ResumeAction(MaintenanceHook):
89 """Resume all Landscape services on the unit."""
90
91- def __init__(self, hookenv=hookenv, subprocess=subprocess):
92- super(ResumeAction, self).__init__(hookenv=hookenv)
93+ def __init__(self, hookenv=hookenv, paths=default_paths,
94+ subprocess=subprocess):
95+ super(ResumeAction, self).__init__(hookenv=hookenv, paths=paths)
96 self._subprocess = subprocess
97
98 def _run(self):
99
100=== modified file 'lib/tests/stubs.py'
101--- lib/tests/stubs.py 2015-05-26 12:05:02 +0000
102+++ lib/tests/stubs.py 2015-06-02 07:06:39 +0000
103@@ -16,6 +16,7 @@
104 self.relations = {}
105 self._config = Config()
106 self._charm_dir = charm_dir
107+ self._action_fails = []
108
109 def config(self):
110 return self._config
111@@ -64,6 +65,9 @@
112 def charm_dir(self):
113 return self._charm_dir
114
115+ def action_fail(self, message):
116+ self._action_fails.append(message)
117+
118
119 class FetchStub(object):
120 """Provide a testable stub for C{charmhelpers.fetch}."""
121
122=== added file 'lib/tests/test_hook.py'
123--- lib/tests/test_hook.py 1970-01-01 00:00:00 +0000
124+++ lib/tests/test_hook.py 2015-06-02 07:06:39 +0000
125@@ -0,0 +1,45 @@
126+import os
127+
128+from lib.tests.helpers import HookenvTest
129+from lib.tests.rootdir import RootDir
130+from lib.hook import MaintenanceHook
131+
132+
133+class DummyMaintenanceHook(MaintenanceHook):
134+ executed = False
135+
136+ def _run(self):
137+ self.executed = True
138+
139+
140+class MaintenanceHookTest(HookenvTest):
141+
142+ def setUp(self):
143+ super(MaintenanceHookTest, self).setUp()
144+ self.root_dir = self.useFixture(RootDir())
145+ self.paths = self.root_dir.paths
146+
147+ def test_run(self):
148+ """Calling a dummy hook runs only with maintenance flag set."""
149+
150+ open(self.paths.maintenance_flag(), "w")
151+ self.addCleanup(os.remove, self.paths.maintenance_flag())
152+
153+ action = DummyMaintenanceHook(
154+ hookenv=self.hookenv, paths=self.paths)
155+
156+ action()
157+ self.assertTrue(action.executed)
158+
159+ def test_run_without_maintenance_flag(self):
160+ """
161+ When maintenance flag file is absent, maintenance hooks are no-ops.
162+ """
163+ action = DummyMaintenanceHook(
164+ hookenv=self.hookenv, paths=self.paths)
165+
166+ action()
167+ self.assertFalse(action.executed)
168+ self.assertEqual(
169+ ["This action can only be called on a unit in paused state."],
170+ self.hookenv._action_fails)
171
172=== modified file 'lib/tests/test_migrate_schema.py'
173--- lib/tests/test_migrate_schema.py 2015-05-26 12:05:02 +0000
174+++ lib/tests/test_migrate_schema.py 2015-06-02 07:06:39 +0000
175@@ -1,7 +1,10 @@
176+import os
177+
178+from lib.migrate_schema import MigrateSchemaAction
179+from lib.paths import SCHEMA_SCRIPT
180 from lib.tests.helpers import HookenvTest
181-from lib.migrate_schema import MigrateSchemaAction
182+from lib.tests.rootdir import RootDir
183 from lib.tests.stubs import SubprocessStub
184-from lib.paths import SCHEMA_SCRIPT
185
186
187 class MigrateSchemaActionTest(HookenvTest):
188@@ -10,12 +13,26 @@
189 super(MigrateSchemaActionTest, self).setUp()
190 self.subprocess = SubprocessStub()
191 self.subprocess.add_fake_executable(SCHEMA_SCRIPT)
192- self.action = MigrateSchemaAction(
193- hookenv=self.hookenv, subprocess=self.subprocess)
194+ self.root_dir = self.useFixture(RootDir())
195+ self.paths = self.root_dir.paths
196
197 def test_run(self):
198 """
199 The MigrateSchemaAction calls the schema script.
200 """
201- self.action()
202+ open(self.paths.maintenance_flag(), "w")
203+ self.addCleanup(os.remove, self.paths.maintenance_flag())
204+
205+ action = MigrateSchemaAction(
206+ hookenv=self.hookenv, paths=self.paths, subprocess=self.subprocess)
207+ action()
208 self.assertEqual([([SCHEMA_SCRIPT], {})], self.subprocess.calls)
209+
210+ def test_run_without_maintenance_flag(self):
211+ """
212+ The MigrateSchemaAction calls the schema script.
213+ """
214+ action = MigrateSchemaAction(
215+ hookenv=self.hookenv, paths=self.paths, subprocess=self.subprocess)
216+ action()
217+ self.assertEqual([], self.subprocess.calls)
218
219=== modified file 'lib/tests/test_resume.py'
220--- lib/tests/test_resume.py 2015-06-01 08:57:04 +0000
221+++ lib/tests/test_resume.py 2015-06-02 07:06:39 +0000
222@@ -1,4 +1,7 @@
223+import os
224+
225 from lib.tests.helpers import HookenvTest
226+from lib.tests.rootdir import RootDir
227 from lib.tests.stubs import SubprocessStub
228 from lib.resume import ResumeAction
229 from lib.paths import LSCTL
230@@ -11,8 +14,8 @@
231 self.subprocess = SubprocessStub()
232 self.subprocess.add_fake_executable(LSCTL)
233 self.subprocess.add_fake_executable("service")
234- self.action = ResumeAction(
235- hookenv=self.hookenv, subprocess=self.subprocess)
236+ self.root_dir = self.useFixture(RootDir())
237+ self.paths = self.root_dir.paths
238
239 def test_run(self):
240 """
241@@ -20,6 +23,20 @@
242 enabling the cron service so that Landscape cron jobs can start
243 running again.
244 """
245- self.action()
246+ open(self.paths.maintenance_flag(), "w")
247+ self.addCleanup(os.remove, self.paths.maintenance_flag())
248+
249+ action = ResumeAction(
250+ hookenv=self.hookenv, subprocess=self.subprocess, paths=self.paths)
251+ action()
252 self.assertEqual(
253 [(("/usr/bin/lsctl", "start"), {})], self.subprocess.calls)
254+
255+ def test_run_without_maintenance_flag(self):
256+ """
257+ When no maintenance flag file is present, resume action is a no-op.
258+ """
259+ action = ResumeAction(
260+ hookenv=self.hookenv, subprocess=self.subprocess, paths=self.paths)
261+ action()
262+ self.assertEqual([], self.subprocess.calls)
263
264=== modified file 'lib/upgrade.py'
265--- lib/upgrade.py 2015-06-01 10:37:32 +0000
266+++ lib/upgrade.py 2015-06-02 07:06:39 +0000
267@@ -1,28 +1,23 @@
268-import os.path
269 import subprocess
270
271 from charmhelpers.core import hookenv
272 from charmhelpers import fetch
273
274 from lib.apt import Apt
275-from lib.hook import Hook, HookError
276+from lib.hook import MaintenanceHook
277 from lib.paths import default_paths
278
279
280-class UpgradeAction(Hook):
281+class UpgradeAction(MaintenanceHook):
282 """Execute package upgrade action logic."""
283
284 def __init__(self, hookenv=hookenv, fetch=fetch, paths=default_paths,
285 subprocess=subprocess):
286- super(UpgradeAction, self).__init__(hookenv=hookenv)
287+ super(UpgradeAction, self).__init__(hookenv=hookenv, paths=paths)
288 self._fetch = fetch
289- self._paths = paths
290 self._subprocess = subprocess
291
292 def _run(self):
293- if not os.path.exists(self._paths.maintenance_flag()):
294- raise HookError(
295- "Upgrade action can only be called on a unit in paused state.")
296 apt_install_options = [
297 # Ensure we keep the existing service.conf and
298 # /etc/defaults/landscape-server configuration files

Subscribers

People subscribed via source and target branches