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