Merge lp:~frankban/juju-deployer/missing-units into lp:juju-deployer

Proposed by Francesco Banconi
Status: Merged
Approved by: David Britton
Approved revision: 142
Merged at revision: 140
Proposed branch: lp:~frankban/juju-deployer/missing-units
Merge into: lp:juju-deployer
Diff against target: 244 lines (+59/-43)
7 files modified
deployer/service.py (+4/-0)
deployer/tests/base.py (+2/-0)
deployer/tests/test_data/stack-placement-invalid-subordinate.yaml (+11/-0)
deployer/tests/test_deployment.py (+27/-20)
deployer/tests/test_diff.py (+4/-6)
deployer/tests/test_guiserver.py (+6/-8)
deployer/tests/test_importer.py (+5/-9)
To merge this branch: bzr merge lp:~frankban/juju-deployer/missing-units
Reviewer Review Type Date Requested Status
David Britton (community) Approve
Brad Crittenden (community) code Approve
Madison Scott-Clary (community) Approve
Review via email: mp+252935@code.launchpad.net

Commit message

Disallow deploying a bundle when a service unit placement points to a subordinate service.

Description of the change

Disallow deploying a bundle when a service unit placement points to a subordinate service.

Without this kind of validation, the bundle would still fail later with an unhelpful KeyError: 'units'.

The big assumption here is that we want the deployer to fail in such cases. Do we? If not, what's the correct behavior?

To post a comment you must log in.
Revision history for this message
Madison Scott-Clary (makyo) wrote :

LGTM, thanks for the fix. Per IRC, I believe that the placement directive should specify the unit on which the subordinate is deployed, rather than the subordinate unit itself, since we can't guarantee the order of subordinate deployments, thus can't guarantee which machine it would wind up on.

review: Approve
Revision history for this message
Madison Scott-Clary (makyo) wrote :

NB: this should land before https://code.launchpad.net/~makyo/juju-deployer/machines-and-placement/+merge/251857 and these changes made in that MP.

Revision history for this message
Brad Crittenden (bac) wrote :

LGTM. No QA

review: Approve (code)
Revision history for this message
David Britton (dpb) wrote :

Thanks -- I've marked as Approved, but put the MP back to WIP, please change back to 'needs review' when you have fixed up the one test issue, and I'll land it.

Code otherwise looks good.

Thanks for doing this, much friendlier error.

review: Approve
Revision history for this message
Francesco Banconi (frankban) wrote :

Thank you all for the reviews!

The fetching tests should now be properly skipped as suggested by David.
I realized the get_named_deployment is already defined in the Base test case, so I removed it from DeploymentTest. I also defined a helper skip decorator in Base to avoid repetition.
I also pushed some lint drive by fixes on the modified files.
Please take another look.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'deployer/service.py'
2--- deployer/service.py 2015-01-22 15:22:18 +0000
3+++ deployer/service.py 2015-03-17 10:41:55 +0000
4@@ -106,6 +106,10 @@
5 feedback.error(
6 "Nested placement not supported %s -> %s -> %s" % (
7 self.service.name, p, services[p].unit_placement))
8+ elif self.deployment.get_charm_for(p).is_subordinate():
9+ feedback.error(
10+ "Cannot place to a subordinate service: %s -> %s" % (
11+ self.service.name, p))
12 else:
13 feedback.error(
14 "Invalid service placement %s to %s" % (
15
16=== modified file 'deployer/tests/base.py'
17--- deployer/tests/base.py 2014-01-23 12:55:11 +0000
18+++ deployer/tests/base.py 2015-03-17 10:41:55 +0000
19@@ -11,6 +11,8 @@
20
21
22 TEST_OFFLINE = ("DEB_BUILD_ARCH" in os.environ or "TEST_OFFLINE" in os.environ)
23+skip_if_offline = unittest.skipIf(
24+ TEST_OFFLINE, 'Requires configured bzr launchpad id and network access')
25
26
27 class Base(unittest.TestCase):
28
29=== added file 'deployer/tests/test_data/stack-placement-invalid-subordinate.yaml'
30--- deployer/tests/test_data/stack-placement-invalid-subordinate.yaml 1970-01-01 00:00:00 +0000
31+++ deployer/tests/test_data/stack-placement-invalid-subordinate.yaml 2015-03-17 10:41:55 +0000
32@@ -0,0 +1,11 @@
33+stack:
34+ series: precise
35+ services:
36+ nova-compute:
37+ charm: cs:precise/nova-compute
38+ to: nrpe
39+ ceph:
40+ to: lxc:nrpe=0
41+ nrpe:
42+ charm: cs:precise/nrpe
43+ units: 2
44
45=== modified file 'deployer/tests/test_deployment.py'
46--- deployer/tests/test_deployment.py 2015-01-22 15:22:18 +0000
47+++ deployer/tests/test_deployment.py 2015-03-17 10:41:55 +0000
48@@ -1,14 +1,11 @@
49 import base64
50 import StringIO
51 import os
52-import unittest
53-
54-
55-from deployer.config import ConfigStack
56+
57 from deployer.deployment import Deployment
58 from deployer.utils import setup_logging, ErrorExit
59
60-from .base import Base, TEST_OFFLINE
61+from .base import Base, skip_if_offline
62
63
64 class DeploymentTest(Base):
65@@ -17,17 +14,19 @@
66 self.output = setup_logging(
67 debug=True, verbose=True, stream=StringIO.StringIO())
68
69- def get_named_deployment(self, file_name, stack_name):
70- return ConfigStack(
71- [os.path.join(
72- self.test_data_dir, file_name)]).get(stack_name)
73+ def get_named_deployment_and_fetch(self, file_name, stack_name):
74+ deployment = self.get_named_deployment(file_name, stack_name)
75+ # Fetch charms in order to allow proper late binding config and
76+ # placement validation.
77+ repo_path = self.mkdir()
78+ os.mkdir(os.path.join(repo_path, "precise"))
79+ deployment.repo_path = repo_path
80+ deployment.fetch_charms()
81+ return deployment
82
83- @unittest.skipIf(TEST_OFFLINE,
84- "Requires configured bzr launchpad id and network access")
85+ @skip_if_offline
86 def test_deployer(self):
87- d = ConfigStack(
88- [os.path.join(
89- self.test_data_dir, "blog.yaml")]).get('wordpress-prod')
90+ d = self.get_named_deployment_and_fetch('blog.yaml', 'wordpress-prod')
91 services = d.get_services()
92 self.assertTrue([s for s in services if s.name == "newrelic"])
93
94@@ -36,12 +35,6 @@
95 d.data['inherits'],
96 ['wordpress-stage', 'wordpress-base', 'metrics-base'])
97
98- # Fetch charms to verify late binding config values & validation.
99- t = self.mkdir()
100- os.mkdir(os.path.join(t, "precise"))
101- d.repo_path = t
102- d.fetch_charms()
103-
104 # Load up overrides and resolves
105 d.load_overrides(["key=abc"])
106 d.resolve()
107@@ -99,6 +92,20 @@
108 else:
109 self.fail("Should fail")
110
111+ @skip_if_offline
112+ def test_validate_invalid_placement_subordinate(self):
113+ # Placement validation fails if a subordinate charm is provided.
114+ deployment = self.get_named_deployment_and_fetch(
115+ 'stack-placement-invalid-subordinate.yaml', 'stack')
116+ with self.assertRaises(ErrorExit):
117+ deployment.validate_placement()
118+ output = self.output.getvalue()
119+ self.assertIn(
120+ 'Cannot place to a subordinate service: ceph -> nrpe\n', output)
121+ self.assertIn(
122+ 'Cannot place to a subordinate service: nova-compute -> nrpe\n',
123+ output)
124+
125 def test_get_unit_placement(self):
126 d = self.get_named_deployment("stack-placement.yaml", "stack")
127 status = {
128
129=== modified file 'deployer/tests/test_diff.py'
130--- deployer/tests/test_diff.py 2014-01-23 12:40:57 +0000
131+++ deployer/tests/test_diff.py 2015-03-17 10:41:55 +0000
132@@ -2,20 +2,18 @@
133 # pylint: disable=C0103
134 import StringIO
135 import os
136-import unittest
137 import shutil
138 import tempfile
139+
140+from deployer.config import ConfigStack
141 from deployer.env.mem import MemoryEnvironment
142-from deployer.config import ConfigStack
143 from deployer.utils import setup_logging
144
145-from .base import Base, TEST_OFFLINE
146+from .base import Base, skip_if_offline
147 from ..action.diff import Diff
148
149
150-# pylint: disable=C0111, R0904
151-@unittest.skipIf(TEST_OFFLINE,
152- "Requires configured bzr launchpad id and network access")
153+@skip_if_offline
154 class DiffTest(Base):
155
156 def setUp(self):
157
158=== modified file 'deployer/tests/test_guiserver.py'
159--- deployer/tests/test_guiserver.py 2015-02-03 12:06:59 +0000
160+++ deployer/tests/test_guiserver.py 2015-03-17 10:41:55 +0000
161@@ -11,7 +11,7 @@
162
163 from deployer import guiserver
164 from deployer.feedback import Feedback
165-from deployer.tests.base import TEST_OFFLINE
166+from deployer.tests.base import skip_if_offline
167
168
169 class TestGetDefaultGuiserverOptions(unittest.TestCase):
170@@ -26,9 +26,9 @@
171 'bootstrap', 'branch_only', 'configs', 'debug', 'deploy_delay',
172 'deployment', 'description', 'destroy_services', 'diff',
173 'find_service', 'ignore_errors', 'juju_env', 'list_deploys',
174- 'no_local_mods', 'no_relations', 'overrides', 'rel_wait', 'retry_count', 'series',
175- 'skip_unit_wait', 'terminate_machines', 'timeout', 'update_charms',
176- 'verbose', 'watch'
177+ 'no_local_mods', 'no_relations', 'overrides', 'rel_wait',
178+ 'retry_count', 'series', 'skip_unit_wait', 'terminate_machines',
179+ 'timeout', 'update_charms', 'verbose', 'watch'
180 ])
181 self.assertEqual(expected_keys, set(self.options.__dict__.keys()))
182
183@@ -172,8 +172,7 @@
184 mock_env_instance.close.assert_called_once_with()
185
186
187-@unittest.skipIf(TEST_OFFLINE,
188- "Requires configured bzr launchpad id and network access")
189+@skip_if_offline
190 @mock.patch('deployer.guiserver.GUIEnvironment')
191 class TestValidate(DeployerFunctionsTestMixin, unittest.TestCase):
192
193@@ -192,8 +191,7 @@
194 self.apiurl, self.username, self.password, self.bundle)
195
196
197-@unittest.skipIf(TEST_OFFLINE,
198- "Requires configured bzr launchpad id and network access")
199+@skip_if_offline
200 @mock.patch('deployer.guiserver.GUIEnvironment')
201 class TestImportBundle(DeployerFunctionsTestMixin, unittest.TestCase):
202
203
204=== modified file 'deployer/tests/test_importer.py'
205--- deployer/tests/test_importer.py 2014-10-01 10:18:36 +0000
206+++ deployer/tests/test_importer.py 2015-03-17 10:41:55 +0000
207@@ -1,14 +1,12 @@
208-import logging
209+import os
210+
211 import mock
212-import os
213-import sys
214-import unittest
215
216 from deployer.config import ConfigStack
217 from deployer.action.importer import Importer
218 from deployer.utils import yaml_dump, yaml_load
219
220-from base import Base, TEST_OFFLINE
221+from base import Base, skip_if_offline
222
223
224 class Options(dict):
225@@ -46,8 +44,7 @@
226 'verbose': True,
227 'watch': False})
228
229- @unittest.skipIf(TEST_OFFLINE,
230- "Requires configured bzr launchpad id and network access")
231+ @skip_if_offline
232 @mock.patch('deployer.action.importer.time')
233 def test_importer(self, mock_time):
234 # Trying to track down where this comes from http://pad.lv/1243827
235@@ -67,8 +64,7 @@
236 yaml_load(yaml_dump(yaml_load(yaml_dump(config)))),
237 config)
238
239- @unittest.skipIf(TEST_OFFLINE,
240- "Requires configured bzr launchpad id and network access")
241+ @skip_if_offline
242 @mock.patch('deployer.action.importer.time')
243 def test_importer_no_relations(self, mock_time):
244 self.options.no_relations = True

Subscribers

People subscribed via source and target branches