Merge lp:~bjornt/landscape-charm/workload-status into lp:~landscape/landscape-charm/trunk

Proposed by Björn Tillenius
Status: Merged
Approved by: Björn Tillenius
Approved revision: 355
Merged at revision: 340
Proposed branch: lp:~bjornt/landscape-charm/workload-status
Merge into: lp:~landscape/landscape-charm/trunk
Diff against target: 530 lines (+240/-52)
12 files modified
lib/action.py (+16/-14)
lib/callbacks/scripts.py (+12/-0)
lib/callbacks/tests/test_scripts.py (+47/-0)
lib/pause.py (+2/-0)
lib/resume.py (+6/-0)
lib/tests/stubs.py (+8/-0)
lib/tests/test_action.py (+34/-11)
lib/tests/test_migrate_schema.py (+5/-5)
lib/tests/test_pause.py (+14/-0)
lib/tests/test_resume.py (+72/-15)
lib/tests/test_upgrade.py (+4/-6)
tests/basic/test_actions.py (+20/-1)
To merge this branch: bzr merge lp:~bjornt/landscape-charm/workload-status
Reviewer Review Type Date Requested Status
Chris Glass (community) Approve
🤖 Landscape Builder test results Approve
Free Ekanayaka (community) Approve
Review via email: mp+280123@code.launchpad.net

Commit message

Initial go at adding workload status to the charm.

It currently sets the unit to active when the Landscape services are
running and maintenance when the are not running. It doesn't yet handle
when something is in an error state or when the unit is blocked.

The resume action is also changed to look at the workload status instead
of the maintenance flag, which solves the problem where non-leader units
couldn't be resumed.

Description of the change

Initial go at adding workload status to the charm.

It currently sets the unit to active when the Landscape services are
running and maintenance when the are not running. It doesn't yet handle
when something is in an error state or when the unit is blocked.

The resume action is also changed to look at the workload status instead
of the maintenance flag, which solves the problem where non-leader units
couldn't be resumed.

I also had to sync charmhelpers to be able to set workload status, which
required some test changes to cope with it.

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

Command: make ci-test
Result: Fail
Revno: 351
Branch: lp:~bjornt/landscape-charm/workload-status
Jenkins: https://ci.lscape.net/job/latch-test/3044/

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

Could we back out the charmhelpers upgrade from this branch? As far as I'm concerned it can even be a trivial as long as unit/integration tests pass (I'm assuming there's not too much breakage in our own code, not sure it's the case).

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

Sure, I've committed the charmhelpers change in trunk. I didn't do that before, since I couldn't run the integration tests. But I finally managed to work around the problem and managed to get a successful run.

352. By Björn Tillenius

Merge trunk.

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

Command: make ci-test
Result: Success
Revno: 352
Branch: lp:~bjornt/landscape-charm/workload-status
Jenkins: https://ci.lscape.net/job/latch-test/3055/

review: Approve (test results)
353. By Björn Tillenius

Merge trunk.

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

Command: make ci-test
Result: Fail
Revno: 353
Branch: lp:~bjornt/landscape-charm/workload-status
Jenkins: https://ci.lscape.net/job/latch-test/3057/

review: Needs Fixing (test results)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: make ci-test
Result: Fail
Revno: 353
Branch: lp:~bjornt/landscape-charm/workload-status
Jenkins: https://ci.lscape.net/job/latch-test/3060/

review: Needs Fixing (test results)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: make ci-test
Result: Success
Revno: 353
Branch: lp:~bjornt/landscape-charm/workload-status
Jenkins: https://ci.lscape.net/job/latch-test/3061/

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

+1

Just a question about whether the final status set by the LSCtl callback can be anything else than "active" (see the comment below).

review: Approve
354. By Björn Tillenius

Rename final_status to current_status.

355. By Björn Tillenius

Rename 'valid_status' to 'required_status'.

Revision history for this message
Björn Tillenius (bjornt) :
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: make ci-test
Result: Fail
Revno: 355
Branch: lp:~bjornt/landscape-charm/workload-status
Jenkins: https://ci.lscape.net/job/latch-test/3070/

review: Needs Fixing (test results)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: make ci-test
Result: Fail
Revno: 355
Branch: lp:~bjornt/landscape-charm/workload-status
Jenkins: https://ci.lscape.net/job/latch-test/3071/

review: Needs Fixing (test results)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: make ci-test
Result: Success
Revno: 355
Branch: lp:~bjornt/landscape-charm/workload-status
Jenkins: https://ci.lscape.net/job/latch-test/3072/

review: Approve (test results)
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
1=== modified file 'lib/action.py'
2--- lib/action.py 2015-06-05 12:00:05 +0000
3+++ lib/action.py 2015-12-15 12:32:20 +0000
4@@ -1,5 +1,3 @@
5-import os.path
6-
7 from charmhelpers.core import hookenv
8
9 from lib.error import CharmError
10@@ -8,7 +6,13 @@
11
12
13 class Action(Hook):
14- """Juju action abstraction, providing dependency injection for testing."""
15+ """Juju action abstraction, providing dependency injection for testing.
16+
17+ @ivar required_status: The status the unit has to be in when the action
18+ is executed.
19+ """
20+
21+ required_status = None
22
23 def __call__(self):
24 """
25@@ -17,6 +21,13 @@
26 If _run() returns a value, set it using action_set().
27 If _run() throws a CharmError, fail using action.fail().
28 """
29+ if self.required_status is not None:
30+ status, _ = self._hookenv.status_get()
31+ if status != self.required_status:
32+ self._hookenv.action_fail(
33+ "This action can only be called on a unit in status "
34+ "'{}'.".format(self.required_status))
35+ return
36 self._hookenv.log("Running action %s" % type(self).__name__)
37 try:
38 return_values = self._run()
39@@ -29,6 +40,8 @@
40 class MaintenanceAction(Action):
41 """Action that only runs when in maintenance mode."""
42
43+ required_status = "maintenance"
44+
45 def __init__(self, hookenv=hookenv, paths=default_paths):
46 """
47 @param hookenv: The charm-helpers C{hookenv} module, will be replaced
48@@ -37,14 +50,3 @@
49 """
50 self._hookenv = hookenv
51 self._paths = paths
52-
53- def __call__(self):
54- """Invoke the action.
55-
56- @return: An integer with the exit code for the hook.
57- """
58- if not os.path.exists(self._paths.maintenance_flag()):
59- self._hookenv.action_fail(
60- "This action can only be called on a unit in paused state.")
61- return
62- super(MaintenanceAction, self).__call__()
63
64=== modified file 'lib/callbacks/scripts.py'
65--- lib/callbacks/scripts.py 2015-09-29 10:26:00 +0000
66+++ lib/callbacks/scripts.py 2015-12-15 12:32:20 +0000
67@@ -69,6 +69,8 @@
68 self._hookenv = hookenv
69
70 def __call__(self, manager, service_name, event_name):
71+ current_status, current_status_message = self._hookenv.status_get()
72+ action_status_message = ""
73 action = event_name
74 if event_name == "start":
75 # XXX the 'start' event in the services framework is called after
76@@ -89,8 +91,18 @@
77 elif hook_name == "db-relation-changed":
78 if not self._need_restart_db_relation_changed(db_new, db_old):
79 return
80+ if action == "restart":
81+ action_status_message = "Restarting services."
82+ if current_status == "unknown":
83+ # If the status is unknown, it means that the services
84+ # have not been started yet.
85+ action_status_message = "Starting services."
86+ if current_status == "maintenance":
87+ return
88
89+ self._hookenv.status_set("maintenance", action_status_message)
90 self._run(LSCTL, (action,))
91+ self._hookenv.status_set("active", "")
92
93 def _need_restart_config_changed(self):
94 """Check whether we need to restart after a config change.
95
96=== modified file 'lib/callbacks/tests/test_scripts.py'
97--- lib/callbacks/tests/test_scripts.py 2015-09-29 10:26:00 +0000
98+++ lib/callbacks/tests/test_scripts.py 2015-12-15 12:32:20 +0000
99@@ -93,6 +93,53 @@
100 self.assertEqual(
101 ["/usr/bin/lsctl", "restart"], self.subprocess.calls[0][0])
102
103+ def test_start_unknown_status(self):
104+ """
105+ If the 'lsctl' script is invoked with the 'restart' action when
106+ the workload status is 'unknown', the status while restarting
107+ the services will way 'starting services' and the final status
108+ will be 'active'.
109+ """
110+ self.callback(self.manager, "landscape", "start")
111+ self.assertEqual(("active", ""), self.hookenv.status_get())
112+ self.assertEqual(
113+ [{"status": "unknown", "message": ""},
114+ {"status": "maintenance", "message": "Starting services."},
115+ {"status": "active", "message": ""}],
116+ self.hookenv.statuses)
117+
118+ def test_start_active_status(self):
119+ """
120+ If the 'lsctl' script is invoked with the 'restart' action when
121+ the workload status is 'active', the status while restarting
122+ the services will way 'restarting services' and the final status
123+ will be 'active' with no status message.
124+ """
125+ self.hookenv.statuses = [{"status": "active", "message": "Something."}]
126+ self.callback(self.manager, "landscape", "start")
127+ self.assertEqual(("active", ""), self.hookenv.status_get())
128+ self.assertEqual(
129+ [{"status": "active", "message": "Something."},
130+ {"status": "maintenance", "message": "Restarting services."},
131+ {"status": "active", "message": ""}],
132+ self.hookenv.statuses)
133+
134+ def test_start_maintenance_status(self):
135+ """
136+ If the 'lsctl' script is invoked with the 'restart' action when
137+ the workload status is 'maintenance', the services won't be
138+ restarted and the workload status won't be changed.
139+ """
140+ self.hookenv.statuses = [
141+ {"status": "maintenance", "message": "Doing maintenance."}]
142+ self.callback(self.manager, "landscape", "start")
143+ self.assertEqual(
144+ ("maintenance", "Doing maintenance."), self.hookenv.status_get())
145+ self.assertEqual(
146+ [{"status": "maintenance", "message": "Doing maintenance."}],
147+ self.hookenv.statuses)
148+ self.assertEqual([], self.subprocess.calls)
149+
150 def test_stop(self):
151 """
152 The 'lsctl' script is invoked with the 'stop' action if the event name
153
154=== modified file 'lib/pause.py'
155--- lib/pause.py 2015-06-03 08:55:54 +0000
156+++ lib/pause.py 2015-12-15 12:32:20 +0000
157@@ -14,4 +14,6 @@
158 self._subprocess = subprocess
159
160 def _run(self):
161+ self._hookenv.status_set("maintenance", "Stopping services.")
162 self._subprocess.check_call((LSCTL, "stop"))
163+ self._hookenv.status_set("maintenance", "Services stopped.")
164
165=== modified file 'lib/resume.py'
166--- lib/resume.py 2015-06-16 07:51:03 +0000
167+++ lib/resume.py 2015-12-15 12:32:20 +0000
168@@ -25,9 +25,15 @@
169 self._subprocess = subprocess
170
171 def _run(self):
172+ self._hookenv.status_set("maintenance", "Starting services.")
173 start_output = self._subprocess.check_output((LSCTL, "start"))
174 try:
175 self._subprocess.check_output((LSCTL, "status"))
176 except subprocess.CalledProcessError as status_error:
177+ self._hookenv.status_set("maintenance", "Stopping services.")
178 self._subprocess.call((LSCTL, "stop"))
179+ self._hookenv.status_set(
180+ "maintenance", "Services failed to start.")
181 raise ProcessesNotStartedError(start_output, status_error.output)
182+ else:
183+ self._hookenv.status_set("active", "")
184
185=== modified file 'lib/tests/stubs.py'
186--- lib/tests/stubs.py 2015-11-03 15:20:36 +0000
187+++ lib/tests/stubs.py 2015-12-15 12:32:20 +0000
188@@ -17,6 +17,7 @@
189 self.relations = {}
190 self.action_fails = []
191 self.action_sets = []
192+ self.statuses = [{"status": "unknown", "message": ""}]
193
194 # We should disable implicit saving since it runs at charm exit using
195 # globals :(
196@@ -95,6 +96,13 @@
197 self.action_gets.append(key)
198 return "%s-value" % key
199
200+ def status_get(self):
201+ current_status = self.statuses[-1]
202+ return current_status["status"], current_status["message"]
203+
204+ def status_set(self, workload_state, message):
205+ self.statuses.append({"status": workload_state, "message": message})
206+
207
208 class FetchStub(object):
209 """Provide a testable stub for C{charmhelpers.fetch}."""
210
211=== modified file 'lib/tests/test_action.py'
212--- lib/tests/test_action.py 2015-06-05 12:02:34 +0000
213+++ lib/tests/test_action.py 2015-12-15 12:32:20 +0000
214@@ -1,5 +1,3 @@
215-import os
216-
217 from lib.action import Action, MaintenanceAction
218 from lib.error import CharmError
219
220@@ -52,6 +50,31 @@
221 action()
222 self.assertEqual(["no go"], self.hookenv.action_fails)
223
224+ def test_run_required_status(self):
225+ """
226+ If required_status is set, the action will be executed if the
227+ current status is the same.
228+ """
229+ action = DummyAction(hookenv=self.hookenv)
230+ action.required_status = "active"
231+ self.hookenv.status_set("active", "")
232+ action()
233+ self.assertTrue(action.executed)
234+
235+ def test_run_invalid_status(self):
236+ """
237+ If required_status is set, the action won't be executed if the
238+ current status is different.
239+ """
240+ action = DummyAction(hookenv=self.hookenv)
241+ action.required_status = "active"
242+ self.hookenv.status_set("maintenance", "")
243+ action()
244+ self.assertFalse(action.executed)
245+ self.assertEqual(
246+ ["This action can only be called on a unit in status 'active'."],
247+ self.hookenv.action_fails)
248+
249
250 class DummyMaintenanceAction(MaintenanceAction):
251 executed = False
252@@ -69,25 +92,25 @@
253
254 def test_run(self):
255 """Calling a dummy hook runs only with maintenance flag set."""
256-
257- open(self.paths.maintenance_flag(), "w")
258- self.addCleanup(os.remove, self.paths.maintenance_flag())
259-
260+ self.hookenv.status_set("maintenance", "")
261 action = DummyMaintenanceAction(
262 hookenv=self.hookenv, paths=self.paths)
263
264 action()
265 self.assertTrue(action.executed)
266
267- def test_run_without_maintenance_flag(self):
268- """
269- When maintenance flag file is absent, maintenance hooks are no-ops.
270- """
271+ def test_run_without_maintenance_status(self):
272+ """
273+ If the workload status isn't 'maintenance', the maintenance
274+ action won't be executed.
275+ """
276+ self.hookenv.status_set("active", "")
277 action = DummyMaintenanceAction(
278 hookenv=self.hookenv, paths=self.paths)
279
280 action()
281 self.assertFalse(action.executed)
282 self.assertEqual(
283- ["This action can only be called on a unit in paused state."],
284+ ["This action can only be called on a unit in status "
285+ "'maintenance'."],
286 self.hookenv.action_fails)
287
288=== modified file 'lib/tests/test_migrate_schema.py'
289--- lib/tests/test_migrate_schema.py 2015-06-01 10:39:56 +0000
290+++ lib/tests/test_migrate_schema.py 2015-12-15 12:32:20 +0000
291@@ -1,5 +1,3 @@
292-import os
293-
294 from lib.migrate_schema import MigrateSchemaAction
295 from lib.paths import SCHEMA_SCRIPT
296 from lib.tests.helpers import HookenvTest
297@@ -20,8 +18,7 @@
298 """
299 The MigrateSchemaAction calls the schema script.
300 """
301- open(self.paths.maintenance_flag(), "w")
302- self.addCleanup(os.remove, self.paths.maintenance_flag())
303+ self.hookenv.status_set("maintenance", "")
304
305 action = MigrateSchemaAction(
306 hookenv=self.hookenv, paths=self.paths, subprocess=self.subprocess)
307@@ -30,8 +27,11 @@
308
309 def test_run_without_maintenance_flag(self):
310 """
311- The MigrateSchemaAction calls the schema script.
312+ The MigrateSchemaAction doesn't call the schema script if the
313+ unit is in an 'active' state.
314 """
315+ self.hookenv.status_set("active", "")
316+
317 action = MigrateSchemaAction(
318 hookenv=self.hookenv, paths=self.paths, subprocess=self.subprocess)
319 action()
320
321=== modified file 'lib/tests/test_pause.py'
322--- lib/tests/test_pause.py 2015-06-01 08:57:04 +0000
323+++ lib/tests/test_pause.py 2015-12-15 12:32:20 +0000
324@@ -21,3 +21,17 @@
325 self.action()
326 self.assertEqual(
327 [(("/usr/bin/lsctl", "stop"), {})], self.subprocess.calls)
328+
329+ def test_run_status(self):
330+ """
331+ The workload status is changed to 'maintenance' while stopping
332+ the services and after the services have been stopped.
333+ """
334+ self.action()
335+ self.assertEqual(
336+ ("maintenance", "Services stopped."), self.hookenv.status_get())
337+ self.assertEqual(
338+ [{"status": "unknown", "message": ""},
339+ {"status": "maintenance", "message": "Stopping services."},
340+ {"status": "maintenance", "message": "Services stopped."}],
341+ self.hookenv.statuses)
342
343=== modified file 'lib/tests/test_resume.py'
344--- lib/tests/test_resume.py 2015-06-16 07:51:03 +0000
345+++ lib/tests/test_resume.py 2015-12-15 12:32:20 +0000
346@@ -1,5 +1,3 @@
347-import os
348-
349 from lib.tests.helpers import HookenvTest
350 from lib.tests.rootdir import RootDir
351 from lib.tests.stubs import SubprocessStub
352@@ -23,8 +21,7 @@
353 enabling the cron service so that Landscape cron jobs can start
354 running again.
355 """
356- open(self.paths.maintenance_flag(), "w")
357- self.addCleanup(os.remove, self.paths.maintenance_flag())
358+ self.hookenv.status_set("maintenance", "")
359
360 action = ResumeAction(
361 hookenv=self.hookenv, subprocess=self.subprocess, paths=self.paths)
362@@ -34,10 +31,29 @@
363 (("/usr/bin/lsctl", "status"), {})],
364 self.subprocess.calls)
365
366+ def test_run_status(self):
367+ """
368+ The status is set to 'maintenance' while starting the services
369+ and is set to 'active' after the services have been started.
370+ """
371+ self.hookenv.status_set("maintenance", "")
372+
373+ action = ResumeAction(
374+ hookenv=self.hookenv, subprocess=self.subprocess, paths=self.paths)
375+ action()
376+ self.assertEqual(("active", ""), self.hookenv.status_get())
377+ self.assertEqual(
378+ [{"status": "unknown", "message": ""},
379+ {"status": "maintenance", "message": ""},
380+ {"status": "maintenance", "message": "Starting services."},
381+ {"status": "active", "message": ""}],
382+ self.hookenv.statuses)
383+
384 def test_run_without_maintenance_flag(self):
385 """
386 When no maintenance flag file is present, resume action is a no-op.
387 """
388+ self.hookenv.status_set("active", "")
389 action = ResumeAction(
390 hookenv=self.hookenv, subprocess=self.subprocess, paths=self.paths)
391 action()
392@@ -53,17 +69,58 @@
393 The unit is stopped again, to ensure that the 'resume' action
394 can run again after the problems have been addressed.
395 """
396- open(self.paths.maintenance_flag(), "w")
397- self.addCleanup(os.remove, self.paths.maintenance_flag())
398- self.subprocess.add_fake_executable(
399- LSCTL, args=["start"], stdout="start output")
400- self.subprocess.add_fake_executable(
401- LSCTL, args=["status"], stdout="status failure", return_code=3)
402- self.subprocess.add_fake_executable(LSCTL, args=["stop"])
403-
404- action = ResumeAction(
405- hookenv=self.hookenv, subprocess=self.subprocess, paths=self.paths)
406- action()
407+ self.hookenv.status_set("maintenance", "")
408+
409+ self.subprocess.add_fake_executable(
410+ LSCTL, args=["start"], stdout="start output")
411+ self.subprocess.add_fake_executable(
412+ LSCTL, args=["status"], stdout="status failure", return_code=3)
413+ self.subprocess.add_fake_executable(LSCTL, args=["stop"])
414+
415+ action = ResumeAction(
416+ hookenv=self.hookenv, subprocess=self.subprocess, paths=self.paths)
417+ action()
418+ self.assertEqual(
419+ ["Some services failed to start.\n\nstart output\n\n"
420+ "status failure"],
421+ self.hookenv.action_fails)
422+ self.assertEqual(
423+ [(("/usr/bin/lsctl", "start"), {}),
424+ (("/usr/bin/lsctl", "status"), {}),
425+ (("/usr/bin/lsctl", "stop"), {})],
426+ self.subprocess.calls)
427+
428+ def test_run_fail_status(self):
429+ """
430+ If the action fails, the final status is put back to
431+ maintenance, with a message saying that services failed to
432+ start.
433+
434+ XXX: The final status probably should be 'error', but it's not
435+ clear on how to recover from that.
436+ """
437+ self.hookenv.status_set("maintenance", "")
438+
439+ self.subprocess.add_fake_executable(
440+ LSCTL, args=["start"], stdout="start output")
441+ self.subprocess.add_fake_executable(
442+ LSCTL, args=["status"], stdout="status failure", return_code=3)
443+ self.subprocess.add_fake_executable(LSCTL, args=["stop"])
444+
445+ action = ResumeAction(
446+ hookenv=self.hookenv, subprocess=self.subprocess, paths=self.paths)
447+ action()
448+ self.assertEqual(
449+ ("maintenance", "Services failed to start."),
450+ self.hookenv.status_get())
451+ self.assertEqual(
452+ [{"status": "unknown", "message": ""},
453+ {"status": "maintenance", "message": ""},
454+ {"status": "maintenance", "message": "Starting services."},
455+ {"status": "maintenance", "message": "Stopping services."},
456+ {"status": "maintenance",
457+ "message": "Services failed to start."}],
458+ self.hookenv.statuses)
459 self.assertEqual(
460 ["Some services failed to start.\n\nstart output\n\n"
461 "status failure"],
462
463=== modified file 'lib/tests/test_upgrade.py'
464--- lib/tests/test_upgrade.py 2015-11-03 13:58:50 +0000
465+++ lib/tests/test_upgrade.py 2015-12-15 12:32:20 +0000
466@@ -1,5 +1,3 @@
467-import os
468-
469 from lib.tests.helpers import HookenvTest
470 from lib.tests.rootdir import RootDir
471 from lib.tests.stubs import FetchStub
472@@ -19,9 +17,7 @@
473 The UpgradeAction refreshes package indexes and upgrades
474 landscape-server package.
475 """
476-
477- open(self.paths.maintenance_flag(), "w")
478- self.addCleanup(os.remove, self.paths.maintenance_flag())
479+ self.hookenv.status_set("maintenance", "")
480
481 self.hookenv.config()["source"] = "ppa:my-ppa"
482 action = UpgradeAction(
483@@ -41,8 +37,10 @@
484
485 def test_run_without_maintenance_flag(self):
486 """
487- When maintenance flag file is absent, upgrade action is a no-op.
488+ If the unit is not in the 'maintenance' state, the upgrade
489+ action is a no-op.
490 """
491+ self.hookenv.status_set("active", "")
492
493 action = UpgradeAction(
494 hookenv=self.hookenv, fetch=self.fetch, paths=self.paths)
495
496=== modified file 'tests/basic/test_actions.py'
497--- tests/basic/test_actions.py 2015-07-09 11:17:22 +0000
498+++ tests/basic/test_actions.py 2015-12-15 12:32:20 +0000
499@@ -3,7 +3,7 @@
500 """
501
502 from helpers import IntegrationTest
503-from layers import OneLandscapeUnitLayer
504+from layers import OneLandscapeUnitLayer, TwoLandscapeUnitsLayer
505
506
507 class ActionsTest(IntegrationTest):
508@@ -60,3 +60,22 @@
509
510 # Logging in should now work.
511 self.environment.login("foo@bar", "bar")
512+
513+
514+class ActionsMultipleUnitsTest(IntegrationTest):
515+
516+ layer = TwoLandscapeUnitsLayer
517+
518+ def setUp(self):
519+ super(ActionsMultipleUnitsTest, self).setUp()
520+ [self.non_leader] = self.layer.non_leaders
521+
522+ def test_non_leader_pause_resume(self):
523+ """
524+ The non-leader unit can be paused and later resumed.
525+ """
526+ result = self.environment.pause_landscape(unit=self.non_leader)
527+ self.assertEqual("completed", result["status"])
528+
529+ result = self.environment.resume_landscape(unit=self.non_leader)
530+ self.assertEqual("completed", result["status"])

Subscribers

People subscribed via source and target branches