Merge lp:~fwereade/pyjuju/maas-name-constraint into lp:~fwereade/pyjuju/shadow-trunk-1204
- maas-name-constraint
- Merge into shadow-trunk-1204
Status: | Merged |
---|---|
Approved by: | William Reade |
Approved revision: | 514 |
Merged at revision: | 510 |
Proposed branch: | lp:~fwereade/pyjuju/maas-name-constraint |
Merge into: | lp:~fwereade/pyjuju/shadow-trunk-1204 |
Prerequisite: | lp:~fwereade/pyjuju/constraints-get |
Diff against target: |
388 lines (+169/-22) 11 files modified
juju/environment/config.py (+3/-1) juju/environment/tests/test_config.py (+8/-6) juju/machine/constraints.py (+3/-2) juju/machine/tests/test_constraints.py (+11/-0) juju/providers/maas/launch.py (+1/-4) juju/providers/maas/maas.py (+5/-1) juju/providers/maas/provider.py (+15/-0) juju/providers/maas/tests/test_launch.py (+38/-5) juju/providers/maas/tests/test_maas.py (+38/-1) juju/providers/maas/tests/test_provider.py (+39/-1) juju/providers/maas/tests/testing.py (+8/-1) |
To merge this branch: | bzr merge lp:~fwereade/pyjuju/maas-name-constraint |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Reade | Pending | ||
Review via email: mp+99746@code.launchpad.net |
Commit message
Description of the change
Implement maas-name constraint
Descended from lp:~julian-edwards/juju/name-constraints; but uses new-style
constraints registration.
Also uses a custom ubuntu-series constraint to forbid use of charms written
for systems we can't yet provision with MAAS.
William Reade (fwereade) wrote : | # |
Gavin Panella (allenap) wrote : | # |
On 2012/03/28 14:46:46, fwereade wrote:
> Please take a look.
Looks sensible to me.
Kapil Thangavelu (hazmat) wrote : | # |
LGTM
https:/
File juju/environmen
https:/
juju/environmen
deserves a comment regarding maas limitation in this regard.
https:/
File juju/environmen
https:/
juju/environmen
it would be nice to check this error message for its end user usability.
the nicely messaged value error in the provider doesn't really ever get
surfaced to the user, but this one does.
- 514. By William Reade
-
merge parent
William Reade (fwereade) wrote : | # |
https:/
File juju/environmen
https:/
juju/environmen
On 2012/03/28 18:10:47, hazmat wrote:
> deserves a comment regarding maas limitation in this regard.
Done.
https:/
File juju/environmen
https:/
juju/environmen
On 2012/03/28 18:10:47, hazmat wrote:
> it would be nice to check this error message for its end user
usability. the
> nicely messaged value error in the provider doesn't really ever get
surfaced to
> the user, but this one does.
Good catch, thank you. Done.
- 515. By William Reade
-
address review
- 516. By William Reade
-
merge parent
William Reade (fwereade) wrote : | # |
*** Submitted:
Implement maas-name constraint
Descended from lp:~julian-edwards/juju/name-constraints; but uses
new-style
constraints registration.
Also uses a custom ubuntu-series constraint to forbid use of charms
written
for systems we can't yet provision with MAAS.
R=allenap, hazmat
CC=
https:/
Preview Diff
1 | === modified file 'juju/environment/config.py' |
2 | --- juju/environment/config.py 2012-03-23 11:05:05 +0000 |
3 | +++ juju/environment/config.py 2012-03-28 19:04:19 +0000 |
4 | @@ -65,7 +65,9 @@ |
5 | "maas-oauth": OAuthString(), |
6 | "admin-secret": String(), |
7 | "placement": _EITHER_PLACEMENT, |
8 | - "default-series": String()}, |
9 | + # MAAS currently only provisions precise; any other default-series |
10 | + # would just lead to errors down the line. |
11 | + "default-series": Constant("precise")}, |
12 | optional=["placement"]), |
13 | "local": KeyDict({ |
14 | "admin-secret": String(), |
15 | |
16 | === modified file 'juju/environment/tests/test_config.py' |
17 | --- juju/environment/tests/test_config.py 2012-03-27 23:22:28 +0000 |
18 | +++ juju/environment/tests/test_config.py 2012-03-28 19:04:19 +0000 |
19 | @@ -46,7 +46,7 @@ |
20 | maas-server: somewhe.re |
21 | maas-oauth: foo:bar:baz |
22 | admin-secret: garden |
23 | - default-series: oneiric |
24 | + default-series: precise |
25 | """ |
26 | |
27 | SAMPLE_LOCAL = """ |
28 | @@ -674,14 +674,16 @@ |
29 | self.fail("Did not properly require %s when type == maas" |
30 | % require) |
31 | |
32 | - def test_maas_respects_default_series(self): |
33 | + def test_maas_default_series(self): |
34 | config = yaml.load(SAMPLE_MAAS) |
35 | config["environments"]["sample"]["default-series"] = "magnificent" |
36 | self.write_config(yaml.dump(config), other_path=True) |
37 | - self.config.load(self.other_path) |
38 | - |
39 | - provider = self.config.get_default().get_machine_provider() |
40 | - self.assertEqual(provider.config["default-series"], "magnificent") |
41 | + e = self.assertRaises( |
42 | + EnvironmentsConfigError, self.config.load, self.other_path) |
43 | + self.assertIn( |
44 | + "environments.sample.default-series: expected 'precise', got " |
45 | + "'magnificent'", |
46 | + str(e)) |
47 | |
48 | def test_maas_verifies_placement(self): |
49 | config = yaml.load(SAMPLE_MAAS) |
50 | |
51 | === modified file 'juju/machine/constraints.py' |
52 | --- juju/machine/constraints.py 2012-03-28 17:47:44 +0000 |
53 | +++ juju/machine/constraints.py 2012-03-28 19:04:19 +0000 |
54 | @@ -12,8 +12,9 @@ |
55 | ALL_NAMES = ( |
56 | "ubuntu-series", "provider-type", |
57 | "arch", "cpu", "mem", "instance-type", |
58 | + "ec2-zone", |
59 | + "maas-name", |
60 | "orchestra-classes", |
61 | - "ec2-zone", |
62 | ) |
63 | |
64 | |
65 | @@ -237,7 +238,7 @@ |
66 | """Return a Constraints with the "ubuntu-series" set to `series`""" |
67 | data = dict(self._data) |
68 | data["ubuntu-series"] = series |
69 | - return Constraints(self._available, data) |
70 | + return self._available.load(data) |
71 | |
72 | @property |
73 | def complete(self): |
74 | |
75 | === modified file 'juju/machine/tests/test_constraints.py' |
76 | --- juju/machine/tests/test_constraints.py 2012-03-28 08:43:23 +0000 |
77 | +++ juju/machine/tests/test_constraints.py 2012-03-28 19:04:19 +0000 |
78 | @@ -391,3 +391,14 @@ |
79 | self.assertEquals(c8["cpu"], 1.0) |
80 | self.assertEquals(c8["mem"], 512.0) |
81 | self.assertEquals(c8["instance-type"], None) |
82 | + |
83 | + def test_load_validates(self): |
84 | + self.allow_names("foo") |
85 | + cs = ConstraintSet("provider") |
86 | + |
87 | + def blam(s): |
88 | + raise ValueError(s) |
89 | + |
90 | + cs.register("foo", converter=blam) |
91 | + e = self.assertRaises(ConstraintError, cs.load, {"foo": "bar"}) |
92 | + self.assertEquals(str(e), "Bad 'foo' constraint 'bar': bar") |
93 | |
94 | === modified file 'juju/providers/maas/launch.py' |
95 | --- juju/providers/maas/launch.py 2012-03-23 08:34:09 +0000 |
96 | +++ juju/providers/maas/launch.py 2012-03-28 19:04:19 +0000 |
97 | @@ -35,11 +35,8 @@ |
98 | representing the newly-launched machine |
99 | :rtype: :class:`twisted.internet.defer.Deferred` |
100 | """ |
101 | - log.warning( |
102 | - "The maas provider does not yet support machine constraints; " |
103 | - "machine will be chosen abritrarily.") |
104 | maas_client = self._provider.maas_client |
105 | - instance_data = yield maas_client.acquire_node() |
106 | + instance_data = yield maas_client.acquire_node(self._constraints) |
107 | instance_uri = instance_data["resource_uri"] |
108 | # If anything goes wrong after the acquire and before the launch |
109 | # actually happens, we attempt to release the node. |
110 | |
111 | === modified file 'juju/providers/maas/maas.py' |
112 | --- juju/providers/maas/maas.py 2012-03-27 10:58:36 +0000 |
113 | +++ juju/providers/maas/maas.py 2012-03-28 19:04:19 +0000 |
114 | @@ -96,13 +96,17 @@ |
115 | for resource_uri in resource_uris) |
116 | return self.get("api/1.0/nodes/", params) |
117 | |
118 | - def acquire_node(self): |
119 | + def acquire_node(self, constraints=None): |
120 | """Ask MAAS to assign a node to us. |
121 | |
122 | :return: A Deferred whose value is the resource URI to the node |
123 | that was acquired. |
124 | """ |
125 | params = {"op": "acquire"} |
126 | + if constraints is not None: |
127 | + name = constraints["maas-name"] |
128 | + if name is not None: |
129 | + params["name"] = name |
130 | return self.post("api/1.0/nodes/", params) |
131 | |
132 | def start_node(self, resource_uri, user_data): |
133 | |
134 | === modified file 'juju/providers/maas/provider.py' |
135 | --- juju/providers/maas/provider.py 2012-03-28 07:04:10 +0000 |
136 | +++ juju/providers/maas/provider.py 2012-03-28 19:04:19 +0000 |
137 | @@ -33,6 +33,21 @@ |
138 | """Return a WebDAV-backed FileStorage abstraction.""" |
139 | return self._storage |
140 | |
141 | + @inlineCallbacks |
142 | + def get_constraint_set(self): |
143 | + """Return the set of constraints that are valid for this provider.""" |
144 | + cs = yield super(MachineProvider, self).get_constraint_set() |
145 | + |
146 | + def require_precise(s): |
147 | + if s != "precise": |
148 | + raise ValueError( |
149 | + "MAAS currently only provisions machines running precise") |
150 | + return s |
151 | + |
152 | + cs.register("ubuntu-series", converter=require_precise, visible=False) |
153 | + cs.register("maas-name") |
154 | + returnValue(cs) |
155 | + |
156 | def get_serialization_data(self): |
157 | """Get provider configuration suitable for serialization. |
158 | |
159 | |
160 | === modified file 'juju/providers/maas/tests/test_launch.py' |
161 | --- juju/providers/maas/tests/test_launch.py 2012-03-28 07:04:10 +0000 |
162 | +++ juju/providers/maas/tests/test_launch.py 2012-03-28 19:04:19 +0000 |
163 | @@ -42,7 +42,7 @@ |
164 | provider._storage = FakeStorage() |
165 | return provider |
166 | |
167 | - def test_bad_data(self): |
168 | + def test_no_machine_id(self): |
169 | provider = self._get_provider() |
170 | d = provider.start_machine({}) |
171 | self.assertFailure(d, ProviderError) |
172 | @@ -54,12 +54,25 @@ |
173 | d.addCallback(verify) |
174 | return d |
175 | |
176 | + def test_no_constraints(self): |
177 | + provider = self._get_provider() |
178 | + d = provider.start_machine({"machine-id": 99}) |
179 | + self.assertFailure(d, ProviderError) |
180 | + |
181 | + def verify(error): |
182 | + self.assertEqual( |
183 | + str(error), |
184 | + "Cannot launch a machine without specifying constraints") |
185 | + d.addCallback(verify) |
186 | + return d |
187 | + |
188 | def test_no_available_machines(self): |
189 | # Requesting a startup of an already-acquired machine should |
190 | # result in a Fault being returned. |
191 | provider = self._get_provider( |
192 | FakeMAASHTTPConnectionWithNoAvailableNodes) |
193 | - machine_data = {"machine-id": "foo", "constraints": {}} |
194 | + machine_data = { |
195 | + "machine-id": "foo", "constraints": {"maas-name": None}} |
196 | d = provider.start_machine(machine_data) |
197 | |
198 | # These arbitrary fake failure values come from |
199 | @@ -82,7 +95,8 @@ |
200 | |
201 | # Try to start up that node using the fake MAAS. |
202 | provider = self._get_provider(FakeMAASHTTPConnection) |
203 | - machine_data = {"machine-id": machine_id, "constraints": {}} |
204 | + machine_data = { |
205 | + "machine-id": machine_id, "constraints": {"maas-name": None}} |
206 | machine_list = yield provider.start_machine(machine_data) |
207 | |
208 | # Test that it returns a list containing a single MAASMachine |
209 | @@ -96,6 +110,25 @@ |
210 | "MAASMachine values of instance_id / dns_name don't match expected" |
211 | "values") |
212 | |
213 | + @inlineCallbacks |
214 | + def test_launch_with_name_constraint(self): |
215 | + # Try to launch a particular machine by its name using the |
216 | + # "maas-name" constraint. |
217 | + |
218 | + # Pick the "moon" node out of the test data. |
219 | + target_node = NODE_JSON[1] |
220 | + self.assertEqual("moon", target_node["hostname"]) |
221 | + machine_id = "foo" |
222 | + provider = self._get_provider(FakeMAASHTTPConnection) |
223 | + constraints = {"maas-name": "moon"} |
224 | + machine_data = {"machine-id": machine_id, "constraints": constraints} |
225 | + machine_list = yield provider.start_machine(machine_data) |
226 | + |
227 | + # Check that "moon" was started. |
228 | + [machine] = machine_list |
229 | + self.assertIsInstance(machine, MAASMachine) |
230 | + self.assertEqual(machine.dns_name, "moon") |
231 | + |
232 | def test_failed_launch(self): |
233 | """If an acquired node fails to start, it is released.""" |
234 | # Throw away logs. |
235 | @@ -116,7 +149,7 @@ |
236 | # The following operations happen in sequence. |
237 | mocker.order() |
238 | # First, the node is acquired. |
239 | - mock_client.acquire_node() |
240 | + mock_client.acquire_node({"maas-name": None}) |
241 | mocker.result({"resource_uri": "/node/123"}) |
242 | # Second, the node is started. We simulate a failure at this stage. |
243 | mock_client.start_node("/node/123", ANY) |
244 | @@ -127,5 +160,5 @@ |
245 | |
246 | mocker.replay() |
247 | return self.assertFailure( |
248 | - MAASLaunchMachine(mock_provider, {}).run("fred"), |
249 | + MAASLaunchMachine(mock_provider, {"maas-name": None}).run("fred"), |
250 | ZeroDivisionError) |
251 | |
252 | === modified file 'juju/providers/maas/tests/test_maas.py' |
253 | --- juju/providers/maas/tests/test_maas.py 2012-03-27 11:26:50 +0000 |
254 | +++ juju/providers/maas/tests/test_maas.py 2012-03-28 19:04:19 +0000 |
255 | @@ -116,13 +116,16 @@ |
256 | return succeed(json.dumps(request_url)) |
257 | |
258 | |
259 | -class TestMAASClientWithTwisted(TestCase): |
260 | +class TestMAASClientBase: |
261 | |
262 | def get_client(self): |
263 | """Return a MAASClient with a FakeMAASHTTPConnection factory.""" |
264 | log = self.setup_connection(MAASClient, FakeMAASHTTPConnection) |
265 | return MAASClient(CONFIG), log |
266 | |
267 | + |
268 | +class TestMAASClientWithTwisted(TestCase, TestMAASClientBase): |
269 | + |
270 | def assertPathEqual(self, expected_path, observed_url): |
271 | """Assert path of `observed_url` is equal to `expected_path`.""" |
272 | observed_path = urlparse(observed_url).path |
273 | @@ -261,3 +264,37 @@ |
274 | client, log = self.get_client() |
275 | returned_data = yield client.release_node(resource_uri) |
276 | self.assertEqual(returned_data, NODE_JSON[0]) |
277 | + |
278 | + |
279 | +class TestConstraints(TestCase, TestMAASClientBase): |
280 | + |
281 | + class fake_post: |
282 | + def __call__(self, uri, params): |
283 | + self.params_used = params |
284 | + |
285 | + def set_up_client_with_fake(self): |
286 | + fake = self.fake_post() |
287 | + client, log = self.get_client() |
288 | + self.patch(client, 'post', fake) |
289 | + return client |
290 | + |
291 | + def test_acquire_node_handles_name_constraint(self): |
292 | + # Ensure that the name constraint is passed through to the post |
293 | + # method. |
294 | + client = self.set_up_client_with_fake() |
295 | + constraints = {"maas-name": "gargleblaster"} |
296 | + client.acquire_node(constraints) |
297 | + |
298 | + name = client.post.params_used.get("name") |
299 | + self.assertEqual("gargleblaster", name) |
300 | + |
301 | + def test_acquire_node_ignores_unknown_constraints(self): |
302 | + # If an unknown constraint is passed it should be ignored. |
303 | + client = self.set_up_client_with_fake() |
304 | + constraints = {"maas-name": "zaphod", "guinness": "widget"} |
305 | + client.acquire_node(constraints) |
306 | + |
307 | + guinness = client.post.params_used.get("guinness") |
308 | + self.assertIs(None, guinness) |
309 | + name = client.post.params_used.get("name") |
310 | + self.assertEqual("zaphod", name) |
311 | |
312 | === modified file 'juju/providers/maas/tests/test_provider.py' |
313 | --- juju/providers/maas/tests/test_provider.py 2012-03-27 12:47:35 +0000 |
314 | +++ juju/providers/maas/tests/test_provider.py 2012-03-28 19:04:19 +0000 |
315 | @@ -5,7 +5,7 @@ |
316 | |
317 | from twisted.internet.defer import inlineCallbacks |
318 | |
319 | -from juju.errors import MachinesNotFound, ProviderError |
320 | +from juju.errors import ConstraintError, MachinesNotFound, ProviderError |
321 | from juju.lib.mocker import ANY |
322 | from juju.providers.maas import MachineProvider |
323 | from juju.providers.maas.maas import MAASClient |
324 | @@ -138,3 +138,41 @@ |
325 | self.assertEqual( |
326 | [machines[0].instance_id], |
327 | [machine.instance_id for machine in machines_terminated]) |
328 | + |
329 | + @inlineCallbacks |
330 | + def test_constraints(self): |
331 | + provider = MachineProvider("blah", CONFIG) |
332 | + cs = yield provider.get_constraint_set() |
333 | + self.assertEquals(cs.parse([]), { |
334 | + "provider-type": "maas", |
335 | + "ubuntu-series": None, |
336 | + "maas-name": None}) |
337 | + self.assertEquals(cs.parse(["maas-name=totoro"]), { |
338 | + "provider-type": "maas", |
339 | + "ubuntu-series": None, |
340 | + "maas-name": "totoro"}) |
341 | + |
342 | + bill = cs.parse(["maas-name=bill"]).with_series("precise") |
343 | + ben = cs.parse(["maas-name=ben"]).with_series("precise") |
344 | + nil = cs.parse([]).with_series("precise") |
345 | + self.assertTrue(bill.can_satisfy(bill)) |
346 | + self.assertTrue(bill.can_satisfy(nil)) |
347 | + self.assertTrue(ben.can_satisfy(ben)) |
348 | + self.assertTrue(ben.can_satisfy(nil)) |
349 | + self.assertTrue(nil.can_satisfy(nil)) |
350 | + self.assertFalse(nil.can_satisfy(bill)) |
351 | + self.assertFalse(nil.can_satisfy(ben)) |
352 | + self.assertFalse(ben.can_satisfy(bill)) |
353 | + self.assertFalse(bill.can_satisfy(ben)) |
354 | + |
355 | + @inlineCallbacks |
356 | + def test_precise_only_constraints(self): |
357 | + provider = MachineProvider("blah", CONFIG) |
358 | + cs = yield provider.get_constraint_set() |
359 | + e = self.assertRaises( |
360 | + ConstraintError, cs.parse([]).with_series, "not-precise") |
361 | + self.assertEquals( |
362 | + str(e), |
363 | + "Bad 'ubuntu-series' constraint 'not-precise': MAAS currently " |
364 | + "only provisions machines running precise") |
365 | + |
366 | |
367 | === modified file 'juju/providers/maas/tests/testing.py' |
368 | --- juju/providers/maas/tests/testing.py 2012-03-27 11:26:50 +0000 |
369 | +++ juju/providers/maas/tests/testing.py 2012-03-28 19:04:19 +0000 |
370 | @@ -142,10 +142,17 @@ |
371 | raise AssertionError("Unknown API method called") |
372 | |
373 | def start_node(self): |
374 | - # TODO: Simply returns the first node, ignoring the data. Fix. |
375 | + # Poor man's node selection. |
376 | + if NODE_JSON[1]["system_id"] in self.url: |
377 | + return defer.succeed(json.dumps(NODE_JSON[1])) |
378 | return defer.succeed(json.dumps(NODE_JSON[0])) |
379 | |
380 | def acquire_node(self): |
381 | + # Implement a poor man's name constraints. |
382 | + if "moon" in self.data: |
383 | + return defer.succeed(json.dumps(NODE_JSON[1])) |
384 | + elif "sun" in self.data: |
385 | + return defer.succeed(json.dumps(NODE_JSON[0])) |
386 | return defer.succeed(json.dumps(NODE_JSON[0])) |
387 | |
388 | def stop_node(self): |
Reviewers: mp+99746_ code.launchpad. net,
Message:
Please take a look.
Description:
Implement maas-name constraint
Descended from lp:~julian-edwards/juju/name-constraints; but uses
new-style
constraints registration.
Also uses a custom ubuntu-series constraint to forbid use of charms
written
for systems we can't yet provision with MAAS.
https:/ /code.launchpad .net/~fwereade/ juju/maas- name-constraint /+merge/ 99746
Requires: /code.launchpad .net/~fwereade/ juju/constraint s-get/+ merge/99704
https:/
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/5938050/
Affected files: t/config. py t/tests/ test_config. py constraints. py tests/test_ constraints. py maas/launch. py maas/maas. py maas/provider. py maas/tests/ test_launch. py maas/tests/ test_maas. py maas/tests/ test_provider. py maas/tests/ testing. py
A [revision details]
M juju/environmen
M juju/environmen
M juju/machine/
M juju/machine/
M juju/providers/
M juju/providers/
M juju/providers/
M juju/providers/
M juju/providers/
M juju/providers/
M juju/providers/