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
=== modified file 'juju/errors.py'
--- juju/errors.py 2012-03-17 14:32:43 +0000
+++ juju/errors.py 2012-04-04 20:07:25 +0000
@@ -132,6 +132,16 @@
132 """Machine constraints are inappropriate or incomprehensible"""132 """Machine constraints are inappropriate or incomprehensible"""
133133
134134
135class PreflightError(ConstraintError):
136 """Machine constraints cannot result in a viable instance"""
137
138 def __init__(self, problem):
139 self.problem = problem
140
141 def __str__(self):
142 return "Constraints preflight failed: %s" % self.problem
143
144
135class UnknownConstraintError(ConstraintError):145class UnknownConstraintError(ConstraintError):
136 """Constraint name not recognised"""146 """Constraint name not recognised"""
137147
138148
=== modified file 'juju/machine/constraints.py'
--- juju/machine/constraints.py 2012-03-30 08:41:02 +0000
+++ juju/machine/constraints.py 2012-04-04 20:07:25 +0000
@@ -2,7 +2,9 @@
2import operator2import operator
3from UserDict import DictMixin3from UserDict import DictMixin
44
5from juju.errors import ConstraintError, UnknownConstraintError5from twisted.internet.defer import fail, succeed
6
7from juju.errors import ConstraintError, PreflightError, UnknownConstraintError
68
7log = logging.getLogger("juju.machine.constraints")9log = logging.getLogger("juju.machine.constraints")
810
@@ -71,8 +73,9 @@
71 Individual providers can construct ConstraintSets which will be used to73 Individual providers can construct ConstraintSets which will be used to
72 construct Constraints objects directly relevant to that provider."""74 construct Constraints objects directly relevant to that provider."""
7375
74 def __init__(self, provider_type):76 def __init__(self, provider_type, validator=None):
75 self._provider_type = provider_type77 self._provider_type = provider_type
78 self._validator = validator
76 self._registry = {}79 self._registry = {}
77 self._conflicts = {}80 self._conflicts = {}
7881
@@ -81,6 +84,21 @@
81 self.register("ubuntu-series", visible=False)84 self.register("ubuntu-series", visible=False)
82 self.register("provider-type", visible=False)85 self.register("provider-type", visible=False)
8386
87 def preflight(self, constraints):
88 """Check constraints for sanity.
89
90 Always checks for completeness, and calls any validation function
91 that has been set.
92
93 Will raise PreflightError if constraints cannot be used to provision
94 a machine.
95 """
96 if not constraints.complete:
97 return fail(PreflightError("incomplete constraints"))
98 if self._validator is None:
99 return succeed(None)
100 return self._validator(constraints)
101
84 def register(self, name, default=None, converter=_dont_convert,102 def register(self, name, default=None, converter=_dont_convert,
85 comparer=operator.eq, visible=True):103 comparer=operator.eq, visible=True):
86 """Register a constraint to be handled by this ConstraintSet.104 """Register a constraint to be handled by this ConstraintSet.
@@ -254,6 +272,19 @@
254 return None not in (272 return None not in (
255 self.get("provider-type"), self.get("ubuntu-series"))273 self.get("provider-type"), self.get("ubuntu-series"))
256274
275 def preflight(self):
276 """Fails with ConstraintError if the constraints are not valid.
277
278 This allows us to detect invalid combinations of constraints that do
279 not otherwise conflict For example, on Amazon EC2, various instance
280 types can be launched with a choice of architectures, but:
281
282 instance-type=cc2.8xlarge arch=i386
283
284 ...can be guaranteed to fail.
285 """
286 return self._available.preflight(self)
287
257 @property288 @property
258 def data(self):289 def data(self):
259 """Return a dict suitable for serialisation and reconstruction.290 """Return a dict suitable for serialisation and reconstruction.
260291
=== modified file 'juju/machine/tests/test_constraints.py'
--- juju/machine/tests/test_constraints.py 2012-03-29 10:27:30 +0000
+++ juju/machine/tests/test_constraints.py 2012-04-04 20:07:25 +0000
@@ -1,30 +1,27 @@
1import operator1import operator
22
3from juju.errors import ConstraintError3from twisted.internet.defer import inlineCallbacks, fail, succeed
4
5from juju.errors import ConstraintError, PreflightError
4from juju.lib.testing import TestCase6from juju.lib.testing import TestCase
5from juju.machine.constraints import Constraints, ConstraintSet7from juju.machine.constraints import Constraints, ConstraintSet
68
9
10def _dummy_validate(constraints):
11 if constraints["cpu"] == 666.666:
12 return fail(PreflightError("disturbing symbolism"))
13 return succeed(None)
14
15
7# These objects exist for the convenience of other test files16# These objects exist for the convenience of other test files
8dummy_cs = ConstraintSet("dummy")17dummy_cs = ConstraintSet("dummy", _dummy_validate)
9dummy_cs.register_generics([])18dummy_cs.register_generics([])
10dummy_constraints = dummy_cs.parse([])19dummy_constraints = dummy_cs.parse([])
11series_constraints = dummy_constraints.with_series("series")20series_constraints = dummy_constraints.with_series("series")
1221
13generic_defaults = {22dummy_defaults = {
14 "arch": "amd64", "cpu": 1, "mem": 512,23 "arch": "amd64", "cpu": 1, "mem": 512,
15 "ubuntu-series": None, "provider-type": None}24 "ubuntu-series": None, "provider-type": "dummy"}
16dummy_defaults = dict(generic_defaults, **{
17 "provider-type": "dummy"})
18ec2_defaults = dict(generic_defaults, **{
19 "provider-type": "ec2",
20 "ec2-zone": None,
21 "instance-type": None})
22orchestra_defaults = {
23 "provider-type": "orchestra",
24 "ubuntu-series": None,
25 "orchestra-classes": None}
26
27all_providers = ["dummy", "ec2", "orchestra"]
2825
2926
30class ConstraintsTestCase(TestCase):27class ConstraintsTestCase(TestCase):
@@ -404,3 +401,27 @@
404 cs.register("foo", converter=blam)401 cs.register("foo", converter=blam)
405 e = self.assertRaises(ConstraintError, cs.load, {"foo": "bar"})402 e = self.assertRaises(ConstraintError, cs.load, {"foo": "bar"})
406 self.assertEquals(str(e), "Bad 'foo' constraint 'bar': bar")403 self.assertEquals(str(e), "Bad 'foo' constraint 'bar': bar")
404
405 @inlineCallbacks
406 def test_preflight(self):
407 self.allow_names("foo", "bar")
408
409 def validate(constraints):
410 if constraints["foo"] != constraints["bar"]:
411 return fail(PreflightError("BORKEN"))
412 return succeed(None)
413
414 cs = ConstraintSet("provider", validate)
415 cs.register("foo")
416 cs.register("bar")
417 constraints = cs.parse([])
418 e = yield self.assertFailure(constraints.preflight(), ConstraintError)
419 self.assertEquals(
420 str(e), "Constraints preflight failed: incomplete constraints")
421
422 constraints = constraints.with_series("series")
423 yield constraints.preflight()
424
425 constraints = cs.parse(["foo=1"]).with_series("series")
426 e = yield self.assertFailure(constraints.preflight(), ConstraintError)
427 self.assertEquals(str(e), "Constraints preflight failed: BORKEN")
407428
=== modified file 'juju/providers/common/base.py'
--- juju/providers/common/base.py 2012-03-29 01:37:57 +0000
+++ juju/providers/common/base.py 2012-04-04 20:07:25 +0000
@@ -38,6 +38,7 @@
38 * :meth:`get_legacy_config_keys`38 * :meth:`get_legacy_config_keys`
39 * :meth:`get_placement_policy`39 * :meth:`get_placement_policy`
40 * :meth:`get_constraint_set`40 * :meth:`get_constraint_set`
41 * :meth:`preflight_constraints`
4142
42 You probably shouldn't override anything else.43 You probably shouldn't override anything else.
43 """44 """
@@ -54,7 +55,19 @@
5455
55 def get_constraint_set(self):56 def get_constraint_set(self):
56 """Return the set of constraints that are valid for this provider."""57 """Return the set of constraints that are valid for this provider."""
57 return succeed(ConstraintSet(self.provider_type))58 return succeed(
59 ConstraintSet(self.provider_type, self.preflight_constraints))
60
61 def preflight_constraints(self, constraints):
62 """Check a complete Constraints object for any knowable problems.
63
64 Passing a preflight does not guarantee that the Constraints will result
65 in a viable instance, but it allows us to detect cases in which
66 interactions between nominally independent constraints will result in
67 nonsensical requests. This should raise PreflightError if problems are
68 detected.
69 """
70 return succeed(None)
5871
59 def get_legacy_config_keys(self):72 def get_legacy_config_keys(self):
60 """Return any deprecated config keys that are set."""73 """Return any deprecated config keys that are set."""
6174
=== modified file 'juju/providers/dummy.py'
--- juju/providers/dummy.py 2012-03-29 01:37:57 +0000
+++ juju/providers/dummy.py 2012-04-04 20:07:25 +0000
@@ -11,7 +11,7 @@
1111
1212
13from juju.machine import ProviderMachine13from juju.machine import ProviderMachine
14from juju.machine.constraints import ConstraintSet14from juju.machine.tests.test_constraints import dummy_cs
15from juju.state.placement import UNASSIGNED_POLICY15from juju.state.placement import UNASSIGNED_POLICY
16from juju.providers.common.files import FileStorage16from juju.providers.common.files import FileStorage
1717
@@ -46,9 +46,7 @@
46 return self.config.get("placement", UNASSIGNED_POLICY)46 return self.config.get("placement", UNASSIGNED_POLICY)
4747
48 def get_constraint_set(self):48 def get_constraint_set(self):
49 cs = ConstraintSet(self.provider_type)49 return succeed(dummy_cs)
50 cs.register_generics([])
51 return succeed(cs)
5250
53 @property51 @property
54 def provider_type(self):52 def provider_type(self):
5553
=== modified file 'juju/providers/ec2/__init__.py'
--- juju/providers/ec2/__init__.py 2012-03-28 19:48:24 +0000
+++ juju/providers/ec2/__init__.py 2012-04-04 20:07:25 +0000
@@ -7,7 +7,7 @@
7from txaws.service import AWSServiceRegion7from txaws.service import AWSServiceRegion
88
9from juju.errors import (9from juju.errors import (
10 MachinesNotFound, ProviderError, ProviderInteractionError)10 MachinesNotFound, ProviderError, ProviderInteractionError, PreflightError)
11from juju.providers.common.base import MachineProviderBase11from juju.providers.common.base import MachineProviderBase
1212
13from .files import FileStorage13from .files import FileStorage
@@ -16,7 +16,9 @@
16from .securitygroup import (16from .securitygroup import (
17 open_provider_port, close_provider_port, get_provider_opened_ports,17 open_provider_port, close_provider_port, get_provider_opened_ports,
18 remove_security_groups, destroy_environment_security_group)18 remove_security_groups, destroy_environment_security_group)
19from .utils import convert_zone, get_region_uri, DEFAULT_REGION, INSTANCE_TYPES19from .utils import (
20 convert_zone, get_region_uri, get_machine_spec,
21 DEFAULT_REGION, INSTANCE_TYPES)
2022
2123
22class MachineProvider(MachineProviderBase):24class MachineProvider(MachineProviderBase):
@@ -56,6 +58,22 @@
56 cs.register("ec2-zone", converter=convert_zone)58 cs.register("ec2-zone", converter=convert_zone)
57 returnValue(cs)59 returnValue(cs)
5860
61 @inlineCallbacks
62 def preflight_constraints(self, constraints):
63 """Check a complete Constraints object for any knowable problems.
64
65 Passing a preflight does not guarantee that the Constraints will result
66 in a viable instance, but it allows us to detect cases in which
67 interactions between nominally independent constraints will result in
68 nonsensical requests. This should raise PreflightError if problems are
69 detected.
70 """
71 yield super(MachineProvider, self).preflight_constraints(constraints)
72 try:
73 yield get_machine_spec(self.config, constraints)
74 except Exception as e:
75 raise PreflightError(e)
76
59 def get_legacy_config_keys(self):77 def get_legacy_config_keys(self):
60 """Return any deprecated config keys that are set"""78 """Return any deprecated config keys that are set"""
61 legacy = super(MachineProvider, self).get_legacy_config_keys()79 legacy = super(MachineProvider, self).get_legacy_config_keys()
6280
=== modified file 'juju/providers/ec2/tests/test_provider.py'
--- juju/providers/ec2/tests/test_provider.py 2012-03-28 19:48:24 +0000
+++ juju/providers/ec2/tests/test_provider.py 2012-04-04 20:07:25 +0000
@@ -205,6 +205,19 @@
205 "provider-type": "ec2",205 "provider-type": "ec2",
206 "ubuntu-series": None})206 "ubuntu-series": None})
207207
208 @inlineCallbacks
209 def test_preflight_constraints(self):
210 cs = yield self.constraint_set()
211 constraints = cs.parse(["instance-type=cc2.8xlarge", "arch=i386"])
212 e = yield self.assertFailure(constraints.preflight(), ConstraintError)
213 self.assertEquals(
214 str(e), "Constraints preflight failed: incomplete constraints")
215
216 constraints = constraints.with_series("series")
217 e = yield self.assertFailure(constraints.preflight(), ConstraintError)
218 self.assertTrue(str(e).startswith(
219 "Constraints preflight failed: No instance type satisfies {"))
220
208221
209class FailCreateTest(TestCase):222class FailCreateTest(TestCase):
210223
211224
=== modified file 'juju/providers/ec2/tests/test_utils.py'
--- juju/providers/ec2/tests/test_utils.py 2012-03-28 09:14:33 +0000
+++ juju/providers/ec2/tests/test_utils.py 2012-04-04 20:07:25 +0000
@@ -235,6 +235,8 @@
235 yield self.assert_no_instance_type(["mem=100G"])235 yield self.assert_no_instance_type(["mem=100G"])
236 yield self.assert_no_instance_type(["arch=i386", "mem=5G"])236 yield self.assert_no_instance_type(["arch=i386", "mem=5G"])
237 yield self.assert_no_instance_type(["arch=arm"])237 yield self.assert_no_instance_type(["arch=arm"])
238 yield self.assert_no_instance_type([
239 "arch=i386", "instance-type=m1.large"])
238240
239 @inlineCallbacks241 @inlineCallbacks
240 def test_config_override(self):242 def test_config_override(self):
241243
=== modified file 'juju/providers/ec2/utils.py'
--- juju/providers/ec2/utils.py 2012-03-28 19:48:24 +0000
+++ juju/providers/ec2/utils.py 2012-04-04 20:07:25 +0000
@@ -132,8 +132,9 @@
132 return instance_type132 return instance_type
133 instance_type = constraints["instance-type"]133 instance_type = constraints["instance-type"]
134 if instance_type is not None:134 if instance_type is not None:
135 return instance_type135 possible_types = [instance_type]
136 possible_types = list(INSTANCE_TYPES)136 else:
137 possible_types = list(INSTANCE_TYPES)
137 for f in _get_filters(constraints):138 for f in _get_filters(constraints):
138 possible_types = filter(f, possible_types)139 possible_types = filter(f, possible_types)
139 if not possible_types:140 if not possible_types:
140141
=== modified file 'juju/providers/orchestra/__init__.py'
--- juju/providers/orchestra/__init__.py 2012-03-28 02:00:16 +0000
+++ juju/providers/orchestra/__init__.py 2012-04-04 20:07:25 +0000
@@ -2,7 +2,7 @@
22
3from twisted.internet.defer import inlineCallbacks, returnValue, succeed3from twisted.internet.defer import inlineCallbacks, returnValue, succeed
44
5from juju.errors import ProviderError5from juju.errors import ProviderError, ConstraintError
6from juju.providers.common.base import MachineProviderBase6from juju.providers.common.base import MachineProviderBase
77
8from .cobbler import CobblerClient8from .cobbler import CobblerClient
@@ -13,6 +13,11 @@
13log = logging.getLogger("juju.orchestra")13log = logging.getLogger("juju.orchestra")
1414
1515
16def _compare_classes(candidate, benchmark):
17 """Constraint comparer for orchestra-classes"""
18 return set(candidate) >= set(benchmark)
19
20
16class MachineProvider(MachineProviderBase):21class MachineProvider(MachineProviderBase):
17 """MachineProvider for use in an Orchestra environment"""22 """MachineProvider for use in an Orchestra environment"""
1823
@@ -25,14 +30,28 @@
25 def provider_type(self):30 def provider_type(self):
26 return "orchestra"31 return "orchestra"
2732
33 def _convert_classes(self, s):
34 """Constraint converter for orchestra-classes"""
35 reserved = [
36 self.config["acquired-mgmt-class"],
37 self.config["available-mgmt-class"]]
38 mgmt_classes = filter(None, s.split(","))
39 for mgmt_class in mgmt_classes:
40 if mgmt_class in reserved:
41 raise ConstraintError(
42 "The management class %r is used internally and may not "
43 "be specified directly."
44 % mgmt_class)
45 return mgmt_classes or None
46
28 @inlineCallbacks47 @inlineCallbacks
29 def get_constraint_set(self):48 def get_constraint_set(self):
30 """Return the set of constraints that are valid for this provider."""49 """Return the set of constraints that are valid for this provider."""
31 cs = yield super(MachineProvider, self).get_constraint_set()50 cs = yield super(MachineProvider, self).get_constraint_set()
32 converter = lambda s: s.split(",")
33 comparer = lambda c, b: set(c) >= set(b)
34 cs.register(51 cs.register(
35 "orchestra-classes", converter=converter, comparer=comparer)52 "orchestra-classes",
53 converter=self._convert_classes,
54 comparer=_compare_classes)
36 returnValue(cs)55 returnValue(cs)
3756
38 def get_file_storage(self):57 def get_file_storage(self):
3958
=== modified file 'juju/providers/orchestra/tests/test_provider.py'
--- juju/providers/orchestra/tests/test_provider.py 2012-03-28 02:00:16 +0000
+++ juju/providers/orchestra/tests/test_provider.py 2012-04-04 20:07:25 +0000
@@ -1,6 +1,7 @@
1from twisted.internet.defer import inlineCallbacks1from twisted.internet.defer import inlineCallbacks
22
3from juju.environment.errors import EnvironmentsConfigError3from juju.environment.errors import EnvironmentsConfigError
4from juju.errors import ConstraintError
4from juju.lib.testing import TestCase5from juju.lib.testing import TestCase
5from juju.providers.orchestra import MachineProvider6from juju.providers.orchestra import MachineProvider
67
@@ -80,10 +81,13 @@
80 self.assertIn(81 self.assertIn(
81 "Firewalling is not yet implemented in Orchestra", log.getvalue())82 "Firewalling is not yet implemented in Orchestra", log.getvalue())
8283
84 def constraint_set(self):
85 provider = MachineProvider("some-orchestra-env", CONFIG)
86 return provider.get_constraint_set()
87
83 @inlineCallbacks88 @inlineCallbacks
84 def test_constraints(self):89 def test_constraints(self):
85 provider = MachineProvider("some-orchestra-env", CONFIG)90 cs = yield self.constraint_set()
86 cs = yield provider.get_constraint_set()
87 self.assertEquals(cs.parse([]), {91 self.assertEquals(cs.parse([]), {
88 "provider-type": "orchestra",92 "provider-type": "orchestra",
89 "ubuntu-series": None,93 "ubuntu-series": None,
@@ -92,7 +96,30 @@
92 "provider-type": "orchestra",96 "provider-type": "orchestra",
93 "ubuntu-series": None,97 "ubuntu-series": None,
94 "orchestra-classes": ["a", "b", "c"]})98 "orchestra-classes": ["a", "b", "c"]})
99 self.assertEquals(cs.parse(["orchestra-classes=a,"]), {
100 "provider-type": "orchestra",
101 "ubuntu-series": None,
102 "orchestra-classes": ["a"]})
103 self.assertEquals(cs.parse(["orchestra-classes=,"]), {
104 "provider-type": "orchestra",
105 "ubuntu-series": None,
106 "orchestra-classes": None})
107 e = self.assertRaises(
108 ConstraintError, cs.parse, ["orchestra-classes=foo,available"])
109 self.assertEquals(
110 str(e),
111 "The management class 'available' is used internally and may not "
112 "be specified directly.")
113 e = self.assertRaises(
114 ConstraintError, cs.parse, ["orchestra-classes=acquired,bar"])
115 self.assertEquals(
116 str(e),
117 "The management class 'acquired' is used internally and may not "
118 "be specified directly.")
95119
120 @inlineCallbacks
121 def test_satsify_constraints(self):
122 cs = yield self.constraint_set()
96 a = cs.parse(["orchestra-classes=a"]).with_series("series")123 a = cs.parse(["orchestra-classes=a"]).with_series("series")
97 ab = cs.parse(["orchestra-classes=a,b"]).with_series("series")124 ab = cs.parse(["orchestra-classes=a,b"]).with_series("series")
98 ac = cs.parse(["orchestra-classes=a,c"]).with_series("series")125 ac = cs.parse(["orchestra-classes=a,c"]).with_series("series")
99126
=== modified file 'juju/state/environment.py'
--- juju/state/environment.py 2012-03-30 08:26:21 +0000
+++ juju/state/environment.py 2012-04-04 20:07:25 +0000
@@ -71,6 +71,15 @@
71 data = {}71 data = {}
72 returnValue(constraint_set.load(data))72 returnValue(constraint_set.load(data))
7373
74 @inlineCallbacks
75 def get_final_constraints(self, override_data):
76 """Collapse a Constraints' data into environment constraints."""
77 constraint_set = yield self.get_constraint_set()
78 override_constraints = constraint_set.load(override_data)
79 final_constraints = yield self.get_constraints()
80 final_constraints.update(override_constraints)
81 returnValue(final_constraints)
82
74 # TODO The environment waiting/watching logic in the83 # TODO The environment waiting/watching logic in the
75 # provisioning agent should be moved here (#640726).84 # provisioning agent should be moved here (#640726).
7685
7786
=== modified file 'juju/state/machine.py'
--- juju/state/machine.py 2012-03-29 15:42:21 +0000
+++ juju/state/machine.py 2012-04-04 20:07:25 +0000
@@ -3,7 +3,6 @@
33
4from twisted.internet.defer import inlineCallbacks, returnValue4from twisted.internet.defer import inlineCallbacks, returnValue
55
6from juju.errors import ConstraintError
7from juju.state.agent import AgentStateMixin6from juju.state.agent import AgentStateMixin
8from juju.state.environment import EnvironmentStateManager7from juju.state.environment import EnvironmentStateManager
9from juju.state.errors import MachineStateNotFound, MachineStateInUse8from juju.state.errors import MachineStateNotFound, MachineStateInUse
@@ -20,9 +19,7 @@
2019
21 @return: MachineState for the created machine.20 @return: MachineState for the created machine.
22 """21 """
23 if not constraints.complete:22 yield constraints.preflight()
24 raise ConstraintError(
25 "Unprovisionable machine: incomplete constraints")
26 machine_data = {"constraints": constraints.data}23 machine_data = {"constraints": constraints.data}
27 path = yield self._client.create(24 path = yield self._client.create(
28 "/machines/machine-", yaml.dump(machine_data),25 "/machines/machine-", yaml.dump(machine_data),
2926
=== modified file 'juju/state/service.py'
--- juju/state/service.py 2012-03-30 10:13:09 +0000
+++ juju/state/service.py 2012-04-04 20:07:25 +0000
@@ -372,10 +372,8 @@
372 be set when changing service constraints).372 be set when changing service constraints).
373 """373 """
374 esm = EnvironmentStateManager(self._client)374 esm = EnvironmentStateManager(self._client)
375 constraints = yield esm.get_constraints()
376 constraint_set = yield esm.get_constraint_set()
377 service_data = yield self._get_node_value("constraints", {})375 service_data = yield self._get_node_value("constraints", {})
378 constraints.update(constraint_set.load(service_data))376 constraints = yield esm.get_final_constraints(service_data)
379 returnValue(constraints)377 returnValue(constraints)
380378
381 @inlineCallbacks379 @inlineCallbacks
@@ -412,6 +410,7 @@
412 apparently deployed on wildly inappropriate machines)).410 apparently deployed on wildly inappropriate machines)).
413 """411 """
414 constraints = yield self.get_constraints()412 constraints = yield self.get_constraints()
413 yield constraints.preflight()
415 charm_id = yield self.get_charm_id()414 charm_id = yield self.get_charm_id()
416 unit_data = {"charm": charm_id, "constraints": constraints.data}415 unit_data = {"charm": charm_id, "constraints": constraints.data}
417 path = yield self._client.create(416 path = yield self._client.create(
418417
=== modified file 'juju/state/tests/test_environment.py'
--- juju/state/tests/test_environment.py 2012-03-29 01:37:57 +0000
+++ juju/state/tests/test_environment.py 2012-04-04 20:07:25 +0000
@@ -106,16 +106,65 @@
106 "cpu": 10.0,106 "cpu": 10.0,
107 "mem": 512.0})107 "mem": 512.0})
108108
109 @inlineCallbacks
110 def test_get_constraints(self):
111 yield self.push_default_config()
112 constraints = yield self.environment_state_manager.get_constraints()
113 self.assertEquals(constraints, {
114 "ubuntu-series": None,
115 "provider-type": "dummy",
116 "arch": "amd64",
117 "cpu": 1.0,
118 "mem": 512.0})
119
120 @inlineCallbacks
121 def test_get_constraints_env_with_no_node(self):
122 yield self.push_default_config()
123 self.client.delete("/constraints")
124 constraints = yield self.environment_state_manager.get_constraints()
125 self.assertEquals(constraints, {
126 "ubuntu-series": None,
127 "provider-type": None,
128 "arch": "amd64",
129 "cpu": 1.0,
130 "mem": 512.0})
131
109 def test_get_constraints_no_env(self):132 def test_get_constraints_no_env(self):
110 d = self.environment_state_manager.get_constraints()133 d = self.environment_state_manager.get_constraints()
111 return self.assertFailure(d, EnvironmentStateNotFound)134 return self.assertFailure(d, EnvironmentStateNotFound)
112135
113 @inlineCallbacks136 def _get_final_constraints(self, data):
114 def test_get_constraints_env_with_no_node(self):137 return self.environment_state_manager.get_final_constraints(data)
138
139 @inlineCallbacks
140 def test_get_final_constraints(self):
141 yield self.push_default_config()
142 constraints = yield self._get_final_constraints({
143 "ubuntu-series": "series",
144 "arch": None,
145 "mem": "2G"})
146 self.assertEquals(constraints, {
147 "ubuntu-series": "series",
148 "provider-type": "dummy",
149 "arch": None,
150 "cpu": 1.0,
151 "mem": 2048.0})
152
153 @inlineCallbacks
154 def test_get_final_constraints_env_with_no_node(self):
115 yield self.push_default_config()155 yield self.push_default_config()
116 self.client.delete("/constraints")156 self.client.delete("/constraints")
117 constraints = yield self.environment_state_manager.get_constraints()157 constraints = yield self._get_final_constraints({"cpu": "512"})
118 self.assertEquals(constraints.data, {})158 self.assertEquals(constraints, {
159 "ubuntu-series": None,
160 "provider-type": None,
161 "arch": "amd64",
162 "cpu": 512.0,
163 "mem": 512.0})
164
165 def test_get_final_constraints_no_env(self):
166 d = self._get_final_constraints({})
167 return self.assertFailure(d, EnvironmentStateNotFound)
119168
120 @inlineCallbacks169 @inlineCallbacks
121 def test_set_constraints(self):170 def test_set_constraints(self):
122171
=== modified file 'juju/state/tests/test_machine.py'
--- juju/state/tests/test_machine.py 2012-03-28 00:38:16 +0000
+++ juju/state/tests/test_machine.py 2012-04-04 20:07:25 +0000
@@ -66,11 +66,11 @@
66 self.assertTrue(topology.has_machine("machine-0000000001"))66 self.assertTrue(topology.has_machine("machine-0000000001"))
6767
68 @inlineCallbacks68 @inlineCallbacks
69 def test_incomplete_constraints(self):69 def test_preflights_constraints(self):
70 e = yield self.assertFailure(70 e = yield self.assertFailure(
71 self.add_machine_state(dummy_constraints), ConstraintError)71 self.add_machine_state(dummy_constraints), ConstraintError)
72 self.assertEquals(72 self.assertEquals(
73 str(e), "Unprovisionable machine: incomplete constraints")73 str(e), "Constraints preflight failed: incomplete constraints")
7474
75 @inlineCallbacks75 @inlineCallbacks
76 def test_missing_constraints(self):76 def test_missing_constraints(self):
7777
=== modified file 'juju/state/tests/test_service.py'
--- juju/state/tests/test_service.py 2012-03-30 10:13:09 +0000
+++ juju/state/tests/test_service.py 2012-04-04 20:07:25 +0000
@@ -10,12 +10,14 @@
10from juju.charm.directory import CharmDirectory10from juju.charm.directory import CharmDirectory
11from juju.charm.tests import local_charm_id11from juju.charm.tests import local_charm_id
12from juju.charm.tests.test_metadata import test_repository_path12from juju.charm.tests.test_metadata import test_repository_path
13from juju.errors import ConstraintError
13from juju.machine.tests.test_constraints import (14from juju.machine.tests.test_constraints import (
14 dummy_constraints, dummy_cs, series_constraints)15 dummy_constraints, dummy_cs, series_constraints)
15from juju.lib.pick import pick_attr16from juju.lib.pick import pick_attr
16from juju.state.charm import CharmStateManager17from juju.state.charm import CharmStateManager
17from juju.charm.tests.test_repository import unbundled_repository18from juju.charm.tests.test_repository import unbundled_repository
18from juju.state.endpoint import RelationEndpoint19from juju.state.endpoint import RelationEndpoint
20from juju.state.environment import EnvironmentStateManager
19from juju.state.service import (21from juju.state.service import (
20 ServiceStateManager, NO_HOOKS, RETRY_HOOKS, parse_service_name)22 ServiceStateManager, NO_HOOKS, RETRY_HOOKS, parse_service_name)
21from juju.state.machine import MachineStateManager23from juju.state.machine import MachineStateManager
@@ -394,13 +396,29 @@
394 under it, and again the unit should offer attributes allowing396 under it, and again the unit should offer attributes allowing
395 its identification.397 its identification.
396 """398 """
399 arch_constraints = dummy_cs.parse(["arch=arm"])
397 service_state0 = yield self.service_state_manager.add_service_state(400 service_state0 = yield self.service_state_manager.add_service_state(
398 "wordpress", self.charm_state, dummy_constraints)401 "wordpress", self.charm_state, arch_constraints)
399 service_state1 = yield self.service_state_manager.add_service_state(402 service_state1 = yield self.service_state_manager.add_service_state(
400 "mysql", self.charm_state, dummy_constraints)403 "mysql", self.charm_state, dummy_constraints)
401404
405 # Temporarily break preflighting and try to add some states
406 esm = EnvironmentStateManager(self.client)
407 yield esm.set_constraints(dummy_cs.parse(["cpu=666.666"]))
408 for service_state in (service_state0, service_state1):
409 d = service_state.add_unit_state()
410 e = yield self.assertFailure(d, ConstraintError)
411 self.assertEquals(
412 str(e), "Constraints preflight failed: disturbing symbolism")
413 yield esm.set_constraints(dummy_cs.parse(["mem=2G"]))
414 # None of the above should have cause a unit node to be created.
415
402 unit_state0 = yield service_state0.add_unit_state()416 unit_state0 = yield service_state0.add_unit_state()
403 unit_state1 = yield service_state1.add_unit_state()417 unit_state1 = yield service_state1.add_unit_state()
418
419 # Change the effective service constraints so we can verify the unit
420 # constraints are creation-time snapshots of the service state.
421 yield esm.set_constraints(dummy_cs.parse(["mem=4G"]))
404 unit_state2 = yield service_state0.add_unit_state()422 unit_state2 = yield service_state0.add_unit_state()
405 unit_state3 = yield service_state1.add_unit_state()423 unit_state3 = yield service_state1.add_unit_state()
406424
@@ -412,18 +430,46 @@
412 self.assertEquals(unit_state0.service_name, "wordpress")430 self.assertEquals(unit_state0.service_name, "wordpress")
413 self.assertEquals(unit_state0.internal_id, "unit-0000000000")431 self.assertEquals(unit_state0.internal_id, "unit-0000000000")
414 self.assertEquals(unit_state0.unit_name, "wordpress/0")432 self.assertEquals(unit_state0.unit_name, "wordpress/0")
433 constraints0 = yield unit_state0.get_constraints()
434 self.assertEquals(constraints0, {
435 "provider-type": "dummy",
436 "ubuntu-series": "series",
437 "arch": "arm",
438 "cpu": 1.0,
439 "mem": 2048.0})
415440
416 self.assertEquals(unit_state1.service_name, "mysql")441 self.assertEquals(unit_state1.service_name, "mysql")
417 self.assertEquals(unit_state1.internal_id, "unit-0000000001")442 self.assertEquals(unit_state1.internal_id, "unit-0000000001")
418 self.assertEquals(unit_state1.unit_name, "mysql/0")443 self.assertEquals(unit_state1.unit_name, "mysql/0")
444 constraints1 = yield unit_state1.get_constraints()
445 self.assertEquals(constraints1, {
446 "provider-type": "dummy",
447 "ubuntu-series": "series",
448 "arch": "amd64",
449 "cpu": 1.0,
450 "mem": 2048.0})
419451
420 self.assertEquals(unit_state2.service_name, "wordpress")452 self.assertEquals(unit_state2.service_name, "wordpress")
421 self.assertEquals(unit_state2.internal_id, "unit-0000000002")453 self.assertEquals(unit_state2.internal_id, "unit-0000000002")
422 self.assertEquals(unit_state2.unit_name, "wordpress/1")454 self.assertEquals(unit_state2.unit_name, "wordpress/1")
455 constraints2 = yield unit_state2.get_constraints()
456 self.assertEquals(constraints2, {
457 "provider-type": "dummy",
458 "ubuntu-series": "series",
459 "arch": "arm",
460 "cpu": 1.0,
461 "mem": 4096.0})
423462
424 self.assertEquals(unit_state3.service_name, "mysql")463 self.assertEquals(unit_state3.service_name, "mysql")
425 self.assertEquals(unit_state3.internal_id, "unit-0000000003")464 self.assertEquals(unit_state3.internal_id, "unit-0000000003")
426 self.assertEquals(unit_state3.unit_name, "mysql/1")465 self.assertEquals(unit_state3.unit_name, "mysql/1")
466 constraints3 = yield unit_state3.get_constraints()
467 self.assertEquals(constraints3, {
468 "provider-type": "dummy",
469 "ubuntu-series": "series",
470 "arch": "amd64",
471 "cpu": 1.0,
472 "mem": 4096.0})
427473
428 topology = yield self.get_topology()474 topology = yield self.get_topology()
429475
430476
=== modified file 'juju/tests/test_errors.py'
--- juju/tests/test_errors.py 2012-03-17 14:32:43 +0000
+++ juju/tests/test_errors.py 2012-04-04 20:07:25 +0000
@@ -4,7 +4,7 @@
4 InvalidUser, ProviderError, CloudInitError, ProviderInteractionError,4 InvalidUser, ProviderError, CloudInitError, ProviderInteractionError,
5 CannotTerminateMachine, MachinesNotFound, EnvironmentPending,5 CannotTerminateMachine, MachinesNotFound, EnvironmentPending,
6 EnvironmentNotFound, IncompatibleVersion, InvalidPlacementPolicy,6 EnvironmentNotFound, IncompatibleVersion, InvalidPlacementPolicy,
7 ServiceError, ConstraintError, UnknownConstraintError)7 ServiceError, ConstraintError, UnknownConstraintError, PreflightError)
88
9from juju.lib.testing import TestCase9from juju.lib.testing import TestCase
1010
@@ -62,6 +62,11 @@
62 self.assertTrue(isinstance(error, ConstraintError))62 self.assertTrue(isinstance(error, ConstraintError))
63 self.assertEquals(str(error), "Unknown constraint: 'meatball'")63 self.assertEquals(str(error), "Unknown constraint: 'meatball'")
6464
65 def test_PreflightError(self):
66 error = PreflightError("cloudy")
67 self.assertTrue(isinstance(error, ConstraintError))
68 self.assertEquals(str(error), "Constraints preflight failed: cloudy")
69
65 def test_ProviderError(self):70 def test_ProviderError(self):
66 error = ProviderError("Invalid credentials")71 error = ProviderError("Invalid credentials")
67 self.assertIsJujuError(error)72 self.assertIsJujuError(error)

Subscribers

People subscribed via source and target branches

to status/vote changes: