Merge lp:~fwereade/pyjuju/preflight-constraints-redux into lp:pyjuju

Proposed by William Reade
Status: Work in progress
Proposed branch: lp:~fwereade/pyjuju/preflight-constraints-redux
Merge into: lp:pyjuju
Diff against target: 718 lines (+306/-48)
18 files modified
juju/errors.py (+10/-0)
juju/machine/constraints.py (+33/-2)
juju/machine/tests/test_constraints.py (+37/-16)
juju/providers/common/base.py (+14/-1)
juju/providers/dummy.py (+2/-4)
juju/providers/ec2/__init__.py (+20/-2)
juju/providers/ec2/tests/test_provider.py (+13/-0)
juju/providers/ec2/tests/test_utils.py (+2/-0)
juju/providers/ec2/utils.py (+3/-2)
juju/providers/orchestra/__init__.py (+23/-4)
juju/providers/orchestra/tests/test_provider.py (+29/-2)
juju/state/environment.py (+9/-0)
juju/state/machine.py (+1/-4)
juju/state/service.py (+2/-3)
juju/state/tests/test_environment.py (+53/-4)
juju/state/tests/test_machine.py (+2/-2)
juju/state/tests/test_service.py (+47/-1)
juju/tests/test_errors.py (+6/-1)
To merge this branch: bzr merge lp:~fwereade/pyjuju/preflight-constraints-redux
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+100869@code.launchpad.net

Description of the change

Add ec2 preflighting; made orchestra constraint checking more effective

Providers can now register validation functions on their ConstraintSets, to
allow for more sophisticated checking than can be handled with the tools
otherwise available. Constraints are preflighted at unit-add and machine-add
time; these are the only constraints that directly translate to actual
machine specifications, and therefore the only ones that need to be
preflighted. (Environment and service constraints can vary independently and
changes to either can affect the service's viability; until someone tries to
deploy a unit that definitely cannot be deployed, IMO, it's premature to
preflight and complain.)

The EC2 provider actually makes use of the preflight mechanism; the
orchestra changes (disallowing direct use of acquired-mgmt-class/available-
mgmt-class) -- which were the original driver for this story -- have no
reason to be preflight-time changes and are implemented as normal
constraints.

https://codereview.appspot.com/5970084/

To post a comment you must log in.
Revision history for this message
William Reade (fwereade) wrote :

Reviewers: mp+100869_code.launchpad.net,

Message:
Please take a look.

Description:
Add ec2 preflighting; made orchestra constraint checking more effective

Providers can now register validation functions on their ConstraintSets,
to
allow for more sophisticated checking than can be handled with the tools
otherwise available. Constraints are preflighted at unit-add and
machine-add
time; these are the only constraints that directly translate to actual
machine specifications, and therefore the only ones that need to be
preflighted. (Environment and service constraints can vary independently
and
changes to either can affect the service's viability; until someone
tries to
deploy a unit that definitely cannot be deployed, IMO, it's premature to
preflight and complain.)

The EC2 provider actually makes use of the preflight mechanism; the
orchestra changes (disallowing direct use of
acquired-mgmt-class/available-
mgmt-class) -- which were the original driver for this story -- have no
reason to be preflight-time changes and are implemented as normal
constraints.

https://code.launchpad.net/~fwereade/juju/preflight-constraints-redux/+merge/100869

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/5970084/

Affected files:
   [revision details]
   juju/errors.py
   juju/machine/constraints.py
   juju/machine/tests/test_constraints.py
   juju/providers/common/base.py
   juju/providers/dummy.py
   juju/providers/ec2/__init__.py
   juju/providers/ec2/tests/test_provider.py
   juju/providers/ec2/tests/test_utils.py
   juju/providers/ec2/utils.py
   juju/providers/orchestra/__init__.py
   juju/providers/orchestra/tests/test_provider.py
   juju/state/environment.py
   juju/state/machine.py
   juju/state/service.py
   juju/state/tests/test_environment.py
   juju/state/tests/test_machine.py
   juju/state/tests/test_service.py
   juju/tests/test_errors.py

Unmerged revisions

513. By William Reade

copy over from weirded preflight-constraints branch

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'juju/errors.py'
2--- juju/errors.py 2012-03-17 14:32:43 +0000
3+++ juju/errors.py 2012-04-04 20:07:25 +0000
4@@ -132,6 +132,16 @@
5 """Machine constraints are inappropriate or incomprehensible"""
6
7
8+class PreflightError(ConstraintError):
9+ """Machine constraints cannot result in a viable instance"""
10+
11+ def __init__(self, problem):
12+ self.problem = problem
13+
14+ def __str__(self):
15+ return "Constraints preflight failed: %s" % self.problem
16+
17+
18 class UnknownConstraintError(ConstraintError):
19 """Constraint name not recognised"""
20
21
22=== modified file 'juju/machine/constraints.py'
23--- juju/machine/constraints.py 2012-03-30 08:41:02 +0000
24+++ juju/machine/constraints.py 2012-04-04 20:07:25 +0000
25@@ -2,7 +2,9 @@
26 import operator
27 from UserDict import DictMixin
28
29-from juju.errors import ConstraintError, UnknownConstraintError
30+from twisted.internet.defer import fail, succeed
31+
32+from juju.errors import ConstraintError, PreflightError, UnknownConstraintError
33
34 log = logging.getLogger("juju.machine.constraints")
35
36@@ -71,8 +73,9 @@
37 Individual providers can construct ConstraintSets which will be used to
38 construct Constraints objects directly relevant to that provider."""
39
40- def __init__(self, provider_type):
41+ def __init__(self, provider_type, validator=None):
42 self._provider_type = provider_type
43+ self._validator = validator
44 self._registry = {}
45 self._conflicts = {}
46
47@@ -81,6 +84,21 @@
48 self.register("ubuntu-series", visible=False)
49 self.register("provider-type", visible=False)
50
51+ def preflight(self, constraints):
52+ """Check constraints for sanity.
53+
54+ Always checks for completeness, and calls any validation function
55+ that has been set.
56+
57+ Will raise PreflightError if constraints cannot be used to provision
58+ a machine.
59+ """
60+ if not constraints.complete:
61+ return fail(PreflightError("incomplete constraints"))
62+ if self._validator is None:
63+ return succeed(None)
64+ return self._validator(constraints)
65+
66 def register(self, name, default=None, converter=_dont_convert,
67 comparer=operator.eq, visible=True):
68 """Register a constraint to be handled by this ConstraintSet.
69@@ -254,6 +272,19 @@
70 return None not in (
71 self.get("provider-type"), self.get("ubuntu-series"))
72
73+ def preflight(self):
74+ """Fails with ConstraintError if the constraints are not valid.
75+
76+ This allows us to detect invalid combinations of constraints that do
77+ not otherwise conflict For example, on Amazon EC2, various instance
78+ types can be launched with a choice of architectures, but:
79+
80+ instance-type=cc2.8xlarge arch=i386
81+
82+ ...can be guaranteed to fail.
83+ """
84+ return self._available.preflight(self)
85+
86 @property
87 def data(self):
88 """Return a dict suitable for serialisation and reconstruction.
89
90=== modified file 'juju/machine/tests/test_constraints.py'
91--- juju/machine/tests/test_constraints.py 2012-03-29 10:27:30 +0000
92+++ juju/machine/tests/test_constraints.py 2012-04-04 20:07:25 +0000
93@@ -1,30 +1,27 @@
94 import operator
95
96-from juju.errors import ConstraintError
97+from twisted.internet.defer import inlineCallbacks, fail, succeed
98+
99+from juju.errors import ConstraintError, PreflightError
100 from juju.lib.testing import TestCase
101 from juju.machine.constraints import Constraints, ConstraintSet
102
103+
104+def _dummy_validate(constraints):
105+ if constraints["cpu"] == 666.666:
106+ return fail(PreflightError("disturbing symbolism"))
107+ return succeed(None)
108+
109+
110 # These objects exist for the convenience of other test files
111-dummy_cs = ConstraintSet("dummy")
112+dummy_cs = ConstraintSet("dummy", _dummy_validate)
113 dummy_cs.register_generics([])
114 dummy_constraints = dummy_cs.parse([])
115 series_constraints = dummy_constraints.with_series("series")
116
117-generic_defaults = {
118+dummy_defaults = {
119 "arch": "amd64", "cpu": 1, "mem": 512,
120- "ubuntu-series": None, "provider-type": None}
121-dummy_defaults = dict(generic_defaults, **{
122- "provider-type": "dummy"})
123-ec2_defaults = dict(generic_defaults, **{
124- "provider-type": "ec2",
125- "ec2-zone": None,
126- "instance-type": None})
127-orchestra_defaults = {
128- "provider-type": "orchestra",
129- "ubuntu-series": None,
130- "orchestra-classes": None}
131-
132-all_providers = ["dummy", "ec2", "orchestra"]
133+ "ubuntu-series": None, "provider-type": "dummy"}
134
135
136 class ConstraintsTestCase(TestCase):
137@@ -404,3 +401,27 @@
138 cs.register("foo", converter=blam)
139 e = self.assertRaises(ConstraintError, cs.load, {"foo": "bar"})
140 self.assertEquals(str(e), "Bad 'foo' constraint 'bar': bar")
141+
142+ @inlineCallbacks
143+ def test_preflight(self):
144+ self.allow_names("foo", "bar")
145+
146+ def validate(constraints):
147+ if constraints["foo"] != constraints["bar"]:
148+ return fail(PreflightError("BORKEN"))
149+ return succeed(None)
150+
151+ cs = ConstraintSet("provider", validate)
152+ cs.register("foo")
153+ cs.register("bar")
154+ constraints = cs.parse([])
155+ e = yield self.assertFailure(constraints.preflight(), ConstraintError)
156+ self.assertEquals(
157+ str(e), "Constraints preflight failed: incomplete constraints")
158+
159+ constraints = constraints.with_series("series")
160+ yield constraints.preflight()
161+
162+ constraints = cs.parse(["foo=1"]).with_series("series")
163+ e = yield self.assertFailure(constraints.preflight(), ConstraintError)
164+ self.assertEquals(str(e), "Constraints preflight failed: BORKEN")
165
166=== modified file 'juju/providers/common/base.py'
167--- juju/providers/common/base.py 2012-03-29 01:37:57 +0000
168+++ juju/providers/common/base.py 2012-04-04 20:07:25 +0000
169@@ -38,6 +38,7 @@
170 * :meth:`get_legacy_config_keys`
171 * :meth:`get_placement_policy`
172 * :meth:`get_constraint_set`
173+ * :meth:`preflight_constraints`
174
175 You probably shouldn't override anything else.
176 """
177@@ -54,7 +55,19 @@
178
179 def get_constraint_set(self):
180 """Return the set of constraints that are valid for this provider."""
181- return succeed(ConstraintSet(self.provider_type))
182+ return succeed(
183+ ConstraintSet(self.provider_type, self.preflight_constraints))
184+
185+ def preflight_constraints(self, constraints):
186+ """Check a complete Constraints object for any knowable problems.
187+
188+ Passing a preflight does not guarantee that the Constraints will result
189+ in a viable instance, but it allows us to detect cases in which
190+ interactions between nominally independent constraints will result in
191+ nonsensical requests. This should raise PreflightError if problems are
192+ detected.
193+ """
194+ return succeed(None)
195
196 def get_legacy_config_keys(self):
197 """Return any deprecated config keys that are set."""
198
199=== modified file 'juju/providers/dummy.py'
200--- juju/providers/dummy.py 2012-03-29 01:37:57 +0000
201+++ juju/providers/dummy.py 2012-04-04 20:07:25 +0000
202@@ -11,7 +11,7 @@
203
204
205 from juju.machine import ProviderMachine
206-from juju.machine.constraints import ConstraintSet
207+from juju.machine.tests.test_constraints import dummy_cs
208 from juju.state.placement import UNASSIGNED_POLICY
209 from juju.providers.common.files import FileStorage
210
211@@ -46,9 +46,7 @@
212 return self.config.get("placement", UNASSIGNED_POLICY)
213
214 def get_constraint_set(self):
215- cs = ConstraintSet(self.provider_type)
216- cs.register_generics([])
217- return succeed(cs)
218+ return succeed(dummy_cs)
219
220 @property
221 def provider_type(self):
222
223=== modified file 'juju/providers/ec2/__init__.py'
224--- juju/providers/ec2/__init__.py 2012-03-28 19:48:24 +0000
225+++ juju/providers/ec2/__init__.py 2012-04-04 20:07:25 +0000
226@@ -7,7 +7,7 @@
227 from txaws.service import AWSServiceRegion
228
229 from juju.errors import (
230- MachinesNotFound, ProviderError, ProviderInteractionError)
231+ MachinesNotFound, ProviderError, ProviderInteractionError, PreflightError)
232 from juju.providers.common.base import MachineProviderBase
233
234 from .files import FileStorage
235@@ -16,7 +16,9 @@
236 from .securitygroup import (
237 open_provider_port, close_provider_port, get_provider_opened_ports,
238 remove_security_groups, destroy_environment_security_group)
239-from .utils import convert_zone, get_region_uri, DEFAULT_REGION, INSTANCE_TYPES
240+from .utils import (
241+ convert_zone, get_region_uri, get_machine_spec,
242+ DEFAULT_REGION, INSTANCE_TYPES)
243
244
245 class MachineProvider(MachineProviderBase):
246@@ -56,6 +58,22 @@
247 cs.register("ec2-zone", converter=convert_zone)
248 returnValue(cs)
249
250+ @inlineCallbacks
251+ def preflight_constraints(self, constraints):
252+ """Check a complete Constraints object for any knowable problems.
253+
254+ Passing a preflight does not guarantee that the Constraints will result
255+ in a viable instance, but it allows us to detect cases in which
256+ interactions between nominally independent constraints will result in
257+ nonsensical requests. This should raise PreflightError if problems are
258+ detected.
259+ """
260+ yield super(MachineProvider, self).preflight_constraints(constraints)
261+ try:
262+ yield get_machine_spec(self.config, constraints)
263+ except Exception as e:
264+ raise PreflightError(e)
265+
266 def get_legacy_config_keys(self):
267 """Return any deprecated config keys that are set"""
268 legacy = super(MachineProvider, self).get_legacy_config_keys()
269
270=== modified file 'juju/providers/ec2/tests/test_provider.py'
271--- juju/providers/ec2/tests/test_provider.py 2012-03-28 19:48:24 +0000
272+++ juju/providers/ec2/tests/test_provider.py 2012-04-04 20:07:25 +0000
273@@ -205,6 +205,19 @@
274 "provider-type": "ec2",
275 "ubuntu-series": None})
276
277+ @inlineCallbacks
278+ def test_preflight_constraints(self):
279+ cs = yield self.constraint_set()
280+ constraints = cs.parse(["instance-type=cc2.8xlarge", "arch=i386"])
281+ e = yield self.assertFailure(constraints.preflight(), ConstraintError)
282+ self.assertEquals(
283+ str(e), "Constraints preflight failed: incomplete constraints")
284+
285+ constraints = constraints.with_series("series")
286+ e = yield self.assertFailure(constraints.preflight(), ConstraintError)
287+ self.assertTrue(str(e).startswith(
288+ "Constraints preflight failed: No instance type satisfies {"))
289+
290
291 class FailCreateTest(TestCase):
292
293
294=== modified file 'juju/providers/ec2/tests/test_utils.py'
295--- juju/providers/ec2/tests/test_utils.py 2012-03-28 09:14:33 +0000
296+++ juju/providers/ec2/tests/test_utils.py 2012-04-04 20:07:25 +0000
297@@ -235,6 +235,8 @@
298 yield self.assert_no_instance_type(["mem=100G"])
299 yield self.assert_no_instance_type(["arch=i386", "mem=5G"])
300 yield self.assert_no_instance_type(["arch=arm"])
301+ yield self.assert_no_instance_type([
302+ "arch=i386", "instance-type=m1.large"])
303
304 @inlineCallbacks
305 def test_config_override(self):
306
307=== modified file 'juju/providers/ec2/utils.py'
308--- juju/providers/ec2/utils.py 2012-03-28 19:48:24 +0000
309+++ juju/providers/ec2/utils.py 2012-04-04 20:07:25 +0000
310@@ -132,8 +132,9 @@
311 return instance_type
312 instance_type = constraints["instance-type"]
313 if instance_type is not None:
314- return instance_type
315- possible_types = list(INSTANCE_TYPES)
316+ possible_types = [instance_type]
317+ else:
318+ possible_types = list(INSTANCE_TYPES)
319 for f in _get_filters(constraints):
320 possible_types = filter(f, possible_types)
321 if not possible_types:
322
323=== modified file 'juju/providers/orchestra/__init__.py'
324--- juju/providers/orchestra/__init__.py 2012-03-28 02:00:16 +0000
325+++ juju/providers/orchestra/__init__.py 2012-04-04 20:07:25 +0000
326@@ -2,7 +2,7 @@
327
328 from twisted.internet.defer import inlineCallbacks, returnValue, succeed
329
330-from juju.errors import ProviderError
331+from juju.errors import ProviderError, ConstraintError
332 from juju.providers.common.base import MachineProviderBase
333
334 from .cobbler import CobblerClient
335@@ -13,6 +13,11 @@
336 log = logging.getLogger("juju.orchestra")
337
338
339+def _compare_classes(candidate, benchmark):
340+ """Constraint comparer for orchestra-classes"""
341+ return set(candidate) >= set(benchmark)
342+
343+
344 class MachineProvider(MachineProviderBase):
345 """MachineProvider for use in an Orchestra environment"""
346
347@@ -25,14 +30,28 @@
348 def provider_type(self):
349 return "orchestra"
350
351+ def _convert_classes(self, s):
352+ """Constraint converter for orchestra-classes"""
353+ reserved = [
354+ self.config["acquired-mgmt-class"],
355+ self.config["available-mgmt-class"]]
356+ mgmt_classes = filter(None, s.split(","))
357+ for mgmt_class in mgmt_classes:
358+ if mgmt_class in reserved:
359+ raise ConstraintError(
360+ "The management class %r is used internally and may not "
361+ "be specified directly."
362+ % mgmt_class)
363+ return mgmt_classes or None
364+
365 @inlineCallbacks
366 def get_constraint_set(self):
367 """Return the set of constraints that are valid for this provider."""
368 cs = yield super(MachineProvider, self).get_constraint_set()
369- converter = lambda s: s.split(",")
370- comparer = lambda c, b: set(c) >= set(b)
371 cs.register(
372- "orchestra-classes", converter=converter, comparer=comparer)
373+ "orchestra-classes",
374+ converter=self._convert_classes,
375+ comparer=_compare_classes)
376 returnValue(cs)
377
378 def get_file_storage(self):
379
380=== modified file 'juju/providers/orchestra/tests/test_provider.py'
381--- juju/providers/orchestra/tests/test_provider.py 2012-03-28 02:00:16 +0000
382+++ juju/providers/orchestra/tests/test_provider.py 2012-04-04 20:07:25 +0000
383@@ -1,6 +1,7 @@
384 from twisted.internet.defer import inlineCallbacks
385
386 from juju.environment.errors import EnvironmentsConfigError
387+from juju.errors import ConstraintError
388 from juju.lib.testing import TestCase
389 from juju.providers.orchestra import MachineProvider
390
391@@ -80,10 +81,13 @@
392 self.assertIn(
393 "Firewalling is not yet implemented in Orchestra", log.getvalue())
394
395+ def constraint_set(self):
396+ provider = MachineProvider("some-orchestra-env", CONFIG)
397+ return provider.get_constraint_set()
398+
399 @inlineCallbacks
400 def test_constraints(self):
401- provider = MachineProvider("some-orchestra-env", CONFIG)
402- cs = yield provider.get_constraint_set()
403+ cs = yield self.constraint_set()
404 self.assertEquals(cs.parse([]), {
405 "provider-type": "orchestra",
406 "ubuntu-series": None,
407@@ -92,7 +96,30 @@
408 "provider-type": "orchestra",
409 "ubuntu-series": None,
410 "orchestra-classes": ["a", "b", "c"]})
411+ self.assertEquals(cs.parse(["orchestra-classes=a,"]), {
412+ "provider-type": "orchestra",
413+ "ubuntu-series": None,
414+ "orchestra-classes": ["a"]})
415+ self.assertEquals(cs.parse(["orchestra-classes=,"]), {
416+ "provider-type": "orchestra",
417+ "ubuntu-series": None,
418+ "orchestra-classes": None})
419+ e = self.assertRaises(
420+ ConstraintError, cs.parse, ["orchestra-classes=foo,available"])
421+ self.assertEquals(
422+ str(e),
423+ "The management class 'available' is used internally and may not "
424+ "be specified directly.")
425+ e = self.assertRaises(
426+ ConstraintError, cs.parse, ["orchestra-classes=acquired,bar"])
427+ self.assertEquals(
428+ str(e),
429+ "The management class 'acquired' is used internally and may not "
430+ "be specified directly.")
431
432+ @inlineCallbacks
433+ def test_satsify_constraints(self):
434+ cs = yield self.constraint_set()
435 a = cs.parse(["orchestra-classes=a"]).with_series("series")
436 ab = cs.parse(["orchestra-classes=a,b"]).with_series("series")
437 ac = cs.parse(["orchestra-classes=a,c"]).with_series("series")
438
439=== modified file 'juju/state/environment.py'
440--- juju/state/environment.py 2012-03-30 08:26:21 +0000
441+++ juju/state/environment.py 2012-04-04 20:07:25 +0000
442@@ -71,6 +71,15 @@
443 data = {}
444 returnValue(constraint_set.load(data))
445
446+ @inlineCallbacks
447+ def get_final_constraints(self, override_data):
448+ """Collapse a Constraints' data into environment constraints."""
449+ constraint_set = yield self.get_constraint_set()
450+ override_constraints = constraint_set.load(override_data)
451+ final_constraints = yield self.get_constraints()
452+ final_constraints.update(override_constraints)
453+ returnValue(final_constraints)
454+
455 # TODO The environment waiting/watching logic in the
456 # provisioning agent should be moved here (#640726).
457
458
459=== modified file 'juju/state/machine.py'
460--- juju/state/machine.py 2012-03-29 15:42:21 +0000
461+++ juju/state/machine.py 2012-04-04 20:07:25 +0000
462@@ -3,7 +3,6 @@
463
464 from twisted.internet.defer import inlineCallbacks, returnValue
465
466-from juju.errors import ConstraintError
467 from juju.state.agent import AgentStateMixin
468 from juju.state.environment import EnvironmentStateManager
469 from juju.state.errors import MachineStateNotFound, MachineStateInUse
470@@ -20,9 +19,7 @@
471
472 @return: MachineState for the created machine.
473 """
474- if not constraints.complete:
475- raise ConstraintError(
476- "Unprovisionable machine: incomplete constraints")
477+ yield constraints.preflight()
478 machine_data = {"constraints": constraints.data}
479 path = yield self._client.create(
480 "/machines/machine-", yaml.dump(machine_data),
481
482=== modified file 'juju/state/service.py'
483--- juju/state/service.py 2012-03-30 10:13:09 +0000
484+++ juju/state/service.py 2012-04-04 20:07:25 +0000
485@@ -372,10 +372,8 @@
486 be set when changing service constraints).
487 """
488 esm = EnvironmentStateManager(self._client)
489- constraints = yield esm.get_constraints()
490- constraint_set = yield esm.get_constraint_set()
491 service_data = yield self._get_node_value("constraints", {})
492- constraints.update(constraint_set.load(service_data))
493+ constraints = yield esm.get_final_constraints(service_data)
494 returnValue(constraints)
495
496 @inlineCallbacks
497@@ -412,6 +410,7 @@
498 apparently deployed on wildly inappropriate machines)).
499 """
500 constraints = yield self.get_constraints()
501+ yield constraints.preflight()
502 charm_id = yield self.get_charm_id()
503 unit_data = {"charm": charm_id, "constraints": constraints.data}
504 path = yield self._client.create(
505
506=== modified file 'juju/state/tests/test_environment.py'
507--- juju/state/tests/test_environment.py 2012-03-29 01:37:57 +0000
508+++ juju/state/tests/test_environment.py 2012-04-04 20:07:25 +0000
509@@ -106,16 +106,65 @@
510 "cpu": 10.0,
511 "mem": 512.0})
512
513+ @inlineCallbacks
514+ def test_get_constraints(self):
515+ yield self.push_default_config()
516+ constraints = yield self.environment_state_manager.get_constraints()
517+ self.assertEquals(constraints, {
518+ "ubuntu-series": None,
519+ "provider-type": "dummy",
520+ "arch": "amd64",
521+ "cpu": 1.0,
522+ "mem": 512.0})
523+
524+ @inlineCallbacks
525+ def test_get_constraints_env_with_no_node(self):
526+ yield self.push_default_config()
527+ self.client.delete("/constraints")
528+ constraints = yield self.environment_state_manager.get_constraints()
529+ self.assertEquals(constraints, {
530+ "ubuntu-series": None,
531+ "provider-type": None,
532+ "arch": "amd64",
533+ "cpu": 1.0,
534+ "mem": 512.0})
535+
536 def test_get_constraints_no_env(self):
537 d = self.environment_state_manager.get_constraints()
538 return self.assertFailure(d, EnvironmentStateNotFound)
539
540- @inlineCallbacks
541- def test_get_constraints_env_with_no_node(self):
542+ def _get_final_constraints(self, data):
543+ return self.environment_state_manager.get_final_constraints(data)
544+
545+ @inlineCallbacks
546+ def test_get_final_constraints(self):
547+ yield self.push_default_config()
548+ constraints = yield self._get_final_constraints({
549+ "ubuntu-series": "series",
550+ "arch": None,
551+ "mem": "2G"})
552+ self.assertEquals(constraints, {
553+ "ubuntu-series": "series",
554+ "provider-type": "dummy",
555+ "arch": None,
556+ "cpu": 1.0,
557+ "mem": 2048.0})
558+
559+ @inlineCallbacks
560+ def test_get_final_constraints_env_with_no_node(self):
561 yield self.push_default_config()
562 self.client.delete("/constraints")
563- constraints = yield self.environment_state_manager.get_constraints()
564- self.assertEquals(constraints.data, {})
565+ constraints = yield self._get_final_constraints({"cpu": "512"})
566+ self.assertEquals(constraints, {
567+ "ubuntu-series": None,
568+ "provider-type": None,
569+ "arch": "amd64",
570+ "cpu": 512.0,
571+ "mem": 512.0})
572+
573+ def test_get_final_constraints_no_env(self):
574+ d = self._get_final_constraints({})
575+ return self.assertFailure(d, EnvironmentStateNotFound)
576
577 @inlineCallbacks
578 def test_set_constraints(self):
579
580=== modified file 'juju/state/tests/test_machine.py'
581--- juju/state/tests/test_machine.py 2012-03-28 00:38:16 +0000
582+++ juju/state/tests/test_machine.py 2012-04-04 20:07:25 +0000
583@@ -66,11 +66,11 @@
584 self.assertTrue(topology.has_machine("machine-0000000001"))
585
586 @inlineCallbacks
587- def test_incomplete_constraints(self):
588+ def test_preflights_constraints(self):
589 e = yield self.assertFailure(
590 self.add_machine_state(dummy_constraints), ConstraintError)
591 self.assertEquals(
592- str(e), "Unprovisionable machine: incomplete constraints")
593+ str(e), "Constraints preflight failed: incomplete constraints")
594
595 @inlineCallbacks
596 def test_missing_constraints(self):
597
598=== modified file 'juju/state/tests/test_service.py'
599--- juju/state/tests/test_service.py 2012-03-30 10:13:09 +0000
600+++ juju/state/tests/test_service.py 2012-04-04 20:07:25 +0000
601@@ -10,12 +10,14 @@
602 from juju.charm.directory import CharmDirectory
603 from juju.charm.tests import local_charm_id
604 from juju.charm.tests.test_metadata import test_repository_path
605+from juju.errors import ConstraintError
606 from juju.machine.tests.test_constraints import (
607 dummy_constraints, dummy_cs, series_constraints)
608 from juju.lib.pick import pick_attr
609 from juju.state.charm import CharmStateManager
610 from juju.charm.tests.test_repository import unbundled_repository
611 from juju.state.endpoint import RelationEndpoint
612+from juju.state.environment import EnvironmentStateManager
613 from juju.state.service import (
614 ServiceStateManager, NO_HOOKS, RETRY_HOOKS, parse_service_name)
615 from juju.state.machine import MachineStateManager
616@@ -394,13 +396,29 @@
617 under it, and again the unit should offer attributes allowing
618 its identification.
619 """
620+ arch_constraints = dummy_cs.parse(["arch=arm"])
621 service_state0 = yield self.service_state_manager.add_service_state(
622- "wordpress", self.charm_state, dummy_constraints)
623+ "wordpress", self.charm_state, arch_constraints)
624 service_state1 = yield self.service_state_manager.add_service_state(
625 "mysql", self.charm_state, dummy_constraints)
626
627+ # Temporarily break preflighting and try to add some states
628+ esm = EnvironmentStateManager(self.client)
629+ yield esm.set_constraints(dummy_cs.parse(["cpu=666.666"]))
630+ for service_state in (service_state0, service_state1):
631+ d = service_state.add_unit_state()
632+ e = yield self.assertFailure(d, ConstraintError)
633+ self.assertEquals(
634+ str(e), "Constraints preflight failed: disturbing symbolism")
635+ yield esm.set_constraints(dummy_cs.parse(["mem=2G"]))
636+ # None of the above should have cause a unit node to be created.
637+
638 unit_state0 = yield service_state0.add_unit_state()
639 unit_state1 = yield service_state1.add_unit_state()
640+
641+ # Change the effective service constraints so we can verify the unit
642+ # constraints are creation-time snapshots of the service state.
643+ yield esm.set_constraints(dummy_cs.parse(["mem=4G"]))
644 unit_state2 = yield service_state0.add_unit_state()
645 unit_state3 = yield service_state1.add_unit_state()
646
647@@ -412,18 +430,46 @@
648 self.assertEquals(unit_state0.service_name, "wordpress")
649 self.assertEquals(unit_state0.internal_id, "unit-0000000000")
650 self.assertEquals(unit_state0.unit_name, "wordpress/0")
651+ constraints0 = yield unit_state0.get_constraints()
652+ self.assertEquals(constraints0, {
653+ "provider-type": "dummy",
654+ "ubuntu-series": "series",
655+ "arch": "arm",
656+ "cpu": 1.0,
657+ "mem": 2048.0})
658
659 self.assertEquals(unit_state1.service_name, "mysql")
660 self.assertEquals(unit_state1.internal_id, "unit-0000000001")
661 self.assertEquals(unit_state1.unit_name, "mysql/0")
662+ constraints1 = yield unit_state1.get_constraints()
663+ self.assertEquals(constraints1, {
664+ "provider-type": "dummy",
665+ "ubuntu-series": "series",
666+ "arch": "amd64",
667+ "cpu": 1.0,
668+ "mem": 2048.0})
669
670 self.assertEquals(unit_state2.service_name, "wordpress")
671 self.assertEquals(unit_state2.internal_id, "unit-0000000002")
672 self.assertEquals(unit_state2.unit_name, "wordpress/1")
673+ constraints2 = yield unit_state2.get_constraints()
674+ self.assertEquals(constraints2, {
675+ "provider-type": "dummy",
676+ "ubuntu-series": "series",
677+ "arch": "arm",
678+ "cpu": 1.0,
679+ "mem": 4096.0})
680
681 self.assertEquals(unit_state3.service_name, "mysql")
682 self.assertEquals(unit_state3.internal_id, "unit-0000000003")
683 self.assertEquals(unit_state3.unit_name, "mysql/1")
684+ constraints3 = yield unit_state3.get_constraints()
685+ self.assertEquals(constraints3, {
686+ "provider-type": "dummy",
687+ "ubuntu-series": "series",
688+ "arch": "amd64",
689+ "cpu": 1.0,
690+ "mem": 4096.0})
691
692 topology = yield self.get_topology()
693
694
695=== modified file 'juju/tests/test_errors.py'
696--- juju/tests/test_errors.py 2012-03-17 14:32:43 +0000
697+++ juju/tests/test_errors.py 2012-04-04 20:07:25 +0000
698@@ -4,7 +4,7 @@
699 InvalidUser, ProviderError, CloudInitError, ProviderInteractionError,
700 CannotTerminateMachine, MachinesNotFound, EnvironmentPending,
701 EnvironmentNotFound, IncompatibleVersion, InvalidPlacementPolicy,
702- ServiceError, ConstraintError, UnknownConstraintError)
703+ ServiceError, ConstraintError, UnknownConstraintError, PreflightError)
704
705 from juju.lib.testing import TestCase
706
707@@ -62,6 +62,11 @@
708 self.assertTrue(isinstance(error, ConstraintError))
709 self.assertEquals(str(error), "Unknown constraint: 'meatball'")
710
711+ def test_PreflightError(self):
712+ error = PreflightError("cloudy")
713+ self.assertTrue(isinstance(error, ConstraintError))
714+ self.assertEquals(str(error), "Constraints preflight failed: cloudy")
715+
716 def test_ProviderError(self):
717 error = ProviderError("Invalid credentials")
718 self.assertIsJujuError(error)

Subscribers

People subscribed via source and target branches

to status/vote changes: