Merge lp:~bcsaller/pyjuju/unit-placement-policy into lp:pyjuju
- unit-placement-policy
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Gustavo Niemeyer | ||||
Approved revision: | 301 | ||||
Merge reported by: | Benjamin Saller | ||||
Merged at revision: | not available | ||||
Proposed branch: | lp:~bcsaller/pyjuju/unit-placement-policy | ||||
Merge into: | lp:pyjuju | ||||
Diff against target: |
637 lines (+378/-32) (has conflicts) 13 files modified
ensemble/control/add_unit.py (+17/-10) ensemble/control/deploy.py (+18/-15) ensemble/control/tests/test_add_unit.py (+72/-1) ensemble/control/tests/test_deploy.py (+61/-3) ensemble/environment/config.py (+11/-2) ensemble/environment/environment.py (+5/-0) ensemble/environment/tests/test_config.py (+36/-0) ensemble/environment/tests/test_environment.py (+1/-0) ensemble/providers/orchestra/__init__.py (+8/-1) ensemble/providers/orchestra/tests/test_provider.py (+24/-0) ensemble/state/errors.py (+10/-0) ensemble/state/placement.py (+58/-0) ensemble/state/tests/test_placement.py (+57/-0) Text conflict in ensemble/environment/config.py Text conflict in ensemble/providers/orchestra/__init__.py Text conflict in ensemble/providers/orchestra/tests/test_provider.py |
||||
To merge this branch: | bzr merge lp:~bcsaller/pyjuju/unit-placement-policy | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gustavo Niemeyer | Approve | ||
William Reade (community) | Approve | ||
Review via email: mp+70387@code.launchpad.net |
Commit message
Description of the change
This branch allows for selecting a policy when deploying and adding units via the ensemble cli
ensemble deploy mysql myblog [--placement local]
ensemble add-unit myblog [--placement local]
if the environment.yaml file has a `placement: <policyname>`
That string will be used to resolve policy when placement isn't provided on the cmd line.
Benjamin Saller (bcsaller) wrote : | # |
> Generally all looks good to me; please address the following minor issues
> before you merge.
>
> [1]
>
> It would be good to keep the config requirements in sync between orchestra and
> ec2, so please (1) merge latest trunk and (2) add handling and tests for
> placement in the orchestra config schema.
Merged and resolved.
>
> [2]
>
> +class InvalidPlacemen
>
> We don't seem to be placing exceptions in consistent places; I see that some
> things here are using ensemble.
> ensemble.
> ensemble.errors rather than (say) ensemble.
> be good to rationalise this a bit; I don't strongly favour any particular
> scheme, though.
moved to state/errors.py
>
> [3]
>
> + self._policy_name = policy_name
>
> I have a slight bias towards making exception attributes public, because I
> don't see the value of hiding their information: it might be useful to read it
> from code at some point, without having to change the exception definition.
> Conversely, anyone crazy enough to actually modify a caught exception isn't
> going to be put off by a mere leading _ ;-). Just a suggestion, not a
> condition.
>
> [4]
>
> +def local_placement
>
> +def unassigned_
>
> +placement_lookup = {
>
> It would be good to make these module-_private, and to rename placement_lookup
> to _PLACEMENT_LOOKUP, just to make its intent a little clearer at first
> glance.
>
Done
> [5]
>
> The python string "unassigned" appears in enough places (in the code, not just
> the tests) that I think I'd prefer it to be named, even if we don't expect it
> to change all that much (and if you do that, you may as well name "local" as
> well).
>
defined.
> [6]
>
> There are a couple of little pep8 issues that could use a look.
removed some E303 too many blank lines errors. There are a few line too long errors but only in tests.
Gustavo Niemeyer (niemeyer) wrote : | # |
Good stuff, +1!
[1]
+ env_defaults = environment.
+ options.placement = env_defaults[
+ "placement", UNASSIGNED_POLICY)
We need a proper API for this rather than poking at the serialization
data, and the default should live in a single place (several call sites
currently carry the default-decision logic).
[2]
- raise NotImplementedE
+ data = copy.deepcopy(
+ return data
These changes don't seem testsed in the orchestra API.
[3]
+class InvalidPlacemen
+ def __init__(self, policy_name):
Some testing for __str__ please. I know it's trivial, but it's been
wrong before, and the test is also trivial.
[4]
+def _local_
+ machine = yield machine_
+ yield unit_state.
+ returnValue(
The overall organization feels simple. Thanks.
[5]
+def place_unit(client, policy_name, machine_
+ """Return machine state of unit_states assignment."""
+
+ placement = _PLACEMENT_
I suggest handling the default here. Move policy_name to the end, default to None, and if None assign the default.
[6]
+ # These shouldn't be used with local (but should be available
+ # to prove a different policy is at work)
Nice testing there.
Benjamin Saller (bcsaller) wrote : | # |
On Mon, Aug 8, 2011 at 4:49 PM, Gustavo Niemeyer <email address hidden> wrote:
> Review: Approve
> Good stuff, +1!
>
>
> [1]
>
> + env_defaults = environment.
> + options.placement = env_defaults[
> + "placement", UNASSIGNED_POLICY)
>
> We need a proper API for this rather than poking at the serialization
> data, and the default should live in a single place (several call sites
> currently carry the default-decision logic).
Added as a property and modified existing test to assert.
>
> [2]
>
> - raise NotImplementedE
> + data = copy.deepcopy(
> + return data
>
> These changes don't seem testsed in the orchestra API.
>
Added test
> [3]
>
> +class InvalidPlacemen
> + def __init__(self, policy_name):
>
> Some testing for __str__ please. I know it's trivial, but it's been
> wrong before, and the test is also trivial.
>
Added test
> [4]
>
> +def _local_
> + machine = yield machine_
> + yield unit_state.
> + returnValue(
>
> The overall organization feels simple. Thanks.
>
:)
> [5]
>
> +def place_unit(client, policy_name, machine_
> + """Return machine state of unit_states assignment."""
> +
> + placement = _PLACEMENT_
>
> I suggest handling the default here. Move policy_name to the end, default to None, and if None assign the default.
changed.
>
> [6]
>
> + # These shouldn't be used with local (but should be available
> + # to prove a different policy is at work)
>
> Nice testing there.
>
> --
> https:/
> You are the owner of lp:~bcsaller/ensemble/unit-placement-policy.
>
- 302. By Benjamin Saller
-
review changes
Preview Diff
1 | === modified file 'ensemble/control/add_unit.py' |
2 | --- ensemble/control/add_unit.py 2011-02-15 03:06:33 +0000 |
3 | +++ ensemble/control/add_unit.py 2011-08-09 01:43:25 +0000 |
4 | @@ -2,10 +2,10 @@ |
5 | |
6 | from twisted.internet.defer import inlineCallbacks |
7 | |
8 | -from ensemble.state.errors import NoUnusedMachines |
9 | +from ensemble.control.utils import get_environment |
10 | +from ensemble.state.machine import MachineStateManager |
11 | +from ensemble.state.placement import place_unit, UNASSIGNED_POLICY |
12 | from ensemble.state.service import ServiceStateManager |
13 | -from ensemble.state.machine import MachineStateManager |
14 | -from ensemble.control.utils import get_environment |
15 | |
16 | |
17 | def configure_subparser(subparsers): |
18 | @@ -17,22 +17,32 @@ |
19 | sub_parser.add_argument( |
20 | "service_name", |
21 | help="Name of the service a unit should be added for") |
22 | + sub_parser.add_argument( |
23 | + "--placement", |
24 | + help="Placement policy used to place units of this service") |
25 | + |
26 | return sub_parser |
27 | |
28 | |
29 | def command(options): |
30 | """Add a new service unit.""" |
31 | environment = get_environment(options) |
32 | + if not options.placement: |
33 | + env_defaults = environment.get_serialization_data() |
34 | + options.placement = env_defaults[environment.name].get( |
35 | + "placement", UNASSIGNED_POLICY) |
36 | + |
37 | return add_unit( |
38 | options.environments, |
39 | environment, |
40 | options.verbose, |
41 | options.log, |
42 | - options.service_name) |
43 | + options.service_name, |
44 | + options.placement) |
45 | |
46 | |
47 | @inlineCallbacks |
48 | -def add_unit(config, environment, verbose, log, service_name): |
49 | +def add_unit(config, environment, verbose, log, service_name, policy_name): |
50 | provider = environment.get_machine_provider() |
51 | client = yield provider.connect() |
52 | try: |
53 | @@ -41,11 +51,8 @@ |
54 | |
55 | service_state = yield service_manager.get_service_state(service_name) |
56 | unit_state = yield service_state.add_unit_state() |
57 | - try: |
58 | - yield unit_state.assign_to_unused_machine() |
59 | - except NoUnusedMachines: |
60 | - machine_state = yield machine_manager.add_machine_state() |
61 | - yield unit_state.assign_to_machine(machine_state) |
62 | + |
63 | + yield place_unit(client, policy_name, machine_manager, unit_state) |
64 | log.info("Unit %r added to service %r", |
65 | unit_state.unit_name, service_state.service_name) |
66 | finally: |
67 | |
68 | === modified file 'ensemble/control/deploy.py' |
69 | --- ensemble/control/deploy.py 2011-07-24 21:26:27 +0000 |
70 | +++ ensemble/control/deploy.py 2011-08-09 01:43:25 +0000 |
71 | @@ -1,4 +1,3 @@ |
72 | -import argparse |
73 | import os |
74 | |
75 | import yaml |
76 | @@ -13,8 +12,8 @@ |
77 | |
78 | from ensemble.state.endpoint import RelationEndpoint |
79 | from ensemble.state.environment import EnvironmentStateManager |
80 | -from ensemble.state.errors import NoUnusedMachines |
81 | from ensemble.state.machine import MachineStateManager |
82 | +from ensemble.state.placement import place_unit, UNASSIGNED_POLICY |
83 | from ensemble.state.relation import RelationStateManager |
84 | from ensemble.state.service import ServiceStateManager |
85 | |
86 | @@ -43,7 +42,9 @@ |
87 | "--config", |
88 | help="YAML file containing service options") |
89 | |
90 | - |
91 | + sub_parser.add_argument( |
92 | + "--placement", |
93 | + help="Placement policy used to place units of this service") |
94 | |
95 | return sub_parser |
96 | |
97 | @@ -53,6 +54,12 @@ |
98 | Deploy a formula to Ensemble! |
99 | """ |
100 | environment = get_environment(options) |
101 | + |
102 | + if not options.placement: |
103 | + env_defaults = environment.get_serialization_data() |
104 | + options.placement = env_defaults[environment.name].get( |
105 | + "placement", UNASSIGNED_POLICY) |
106 | + |
107 | return deploy( |
108 | options.environments, |
109 | environment, |
110 | @@ -60,12 +67,15 @@ |
111 | options.formula, |
112 | options.service_name, |
113 | options.log, |
114 | - options.config) |
115 | + options.config, |
116 | + options.placement) |
117 | + |
118 | |
119 | def parse_config_options(config_file, service_name): |
120 | if not os.path.exists(config_file) or \ |
121 | not os.access(config_file, os.R_OK): |
122 | - raise ServiceConfigError("Config file '%s' not accessible." % config_file) |
123 | + raise ServiceConfigError("Config file '%s' not accessible." % |
124 | + config_file) |
125 | |
126 | options = yaml.load(open(config_file, "r").read()) |
127 | if not options or not isinstance(options, dict) or \ |
128 | @@ -77,10 +87,10 @@ |
129 | return options[service_name] |
130 | |
131 | |
132 | - |
133 | @inlineCallbacks |
134 | def deploy(env_config, environment, repository_path, formula_name, |
135 | - service_name, log, config_file=None): |
136 | + service_name, log, config_file=None, |
137 | + placement_policy=UNASSIGNED_POLICY): |
138 | """Deploy a formula within an environment. |
139 | |
140 | This will publish the formula to the environment, creating |
141 | @@ -130,14 +140,7 @@ |
142 | # Create a service unit |
143 | unit_state = yield service_state.add_unit_state() |
144 | |
145 | - try: |
146 | - # Attempt to assign to an unused machine |
147 | - yield unit_state.assign_to_unused_machine() |
148 | - except NoUnusedMachines: |
149 | - # Otherwise create the machine state |
150 | - machine_state = yield machine_manager.add_machine_state() |
151 | - # Associate the unit to the machine |
152 | - yield unit_state.assign_to_machine(machine_state) |
153 | + yield place_unit(client, placement_policy, machine_manager, unit_state) |
154 | |
155 | # Check if we have any peer relations to establish |
156 | if formula.metadata.peers: |
157 | |
158 | === modified file 'ensemble/control/tests/test_add_unit.py' |
159 | --- ensemble/control/tests/test_add_unit.py 2011-02-15 21:07:22 +0000 |
160 | +++ ensemble/control/tests/test_add_unit.py 2011-08-09 01:43:25 +0000 |
161 | @@ -86,4 +86,75 @@ |
162 | self.output.getvalue()) |
163 | yield self.assert_machine_assignments("wordpress", [None]) |
164 | yield self.assert_machine_assignments("mysql", [1, 2]) |
165 | - |
166 | + |
167 | + @inlineCallbacks |
168 | + def test_add_unit_with_local_policy(self): |
169 | + """ |
170 | + 'ensemble add-unit <service_name>' will add a new service |
171 | + unit of the given service. |
172 | + """ |
173 | + # rework setUp so that service_unit1 is on machine 0 |
174 | + # to simulate local deployment |
175 | + ms0 = yield self.machine_state_manager.get_machine_state(0) |
176 | + yield self.service_unit1.unassign_from_machine() |
177 | + yield self.service_unit1.assign_to_machine(ms0) |
178 | + |
179 | + unit_names = yield self.service_state1.get_unit_names() |
180 | + self.assertEqual(len(unit_names), 1) |
181 | + |
182 | + finished = self.setup_cli_reactor() |
183 | + self.setup_exit(0) |
184 | + self.mocker.replay() |
185 | + main(["add-unit", "mysql", "--placement", "local"]) |
186 | + yield finished |
187 | + |
188 | + # verify the unit and its machine assignment. |
189 | + unit_names = yield self.service_state1.get_unit_names() |
190 | + self.assertEqual(len(unit_names), 2) |
191 | + |
192 | + topology = yield self.get_topology() |
193 | + unit = yield self.service_state1.get_unit_state("mysql/1") |
194 | + machine_id = topology.get_service_unit_machine( |
195 | + self.service_state1.internal_id, unit.internal_id) |
196 | + self.assertNotEqual(machine_id, None) |
197 | + self.assertIn( |
198 | + "Unit 'mysql/1' added to service 'mysql'", |
199 | + self.output.getvalue()) |
200 | + # adding a second unit still assigns to machine 0 with local policy |
201 | + yield self.assert_machine_assignments("mysql", [0, 0]) |
202 | + |
203 | + @inlineCallbacks |
204 | + def test_policy_from_environment(self): |
205 | + config = { |
206 | + "ensemble": "environments", |
207 | + "environments": {"firstenv": { |
208 | + "placement": "local", |
209 | + "type": "dummy"}}} |
210 | + |
211 | + self.write_config(dump(config)) |
212 | + self.config.load() |
213 | + |
214 | + ms0 = yield self.machine_state_manager.get_machine_state(0) |
215 | + yield self.service_unit1.unassign_from_machine() |
216 | + yield self.service_unit1.assign_to_machine(ms0) |
217 | + |
218 | + unit_names = yield self.service_state1.get_unit_names() |
219 | + self.assertEqual(len(unit_names), 1) |
220 | + |
221 | + finished = self.setup_cli_reactor() |
222 | + self.setup_exit(0) |
223 | + self.mocker.replay() |
224 | + main(["add-unit", "mysql"]) |
225 | + yield finished |
226 | + |
227 | + # Verify the local policy was used |
228 | + topology = yield self.get_topology() |
229 | + unit = yield self.service_state1.get_unit_state("mysql/1") |
230 | + machine_id = topology.get_service_unit_machine( |
231 | + self.service_state1.internal_id, unit.internal_id) |
232 | + self.assertNotEqual(machine_id, None) |
233 | + self.assertIn( |
234 | + "Unit 'mysql/1' added to service 'mysql'", |
235 | + self.output.getvalue()) |
236 | + # adding a second unit still assigns to machine 0 with local policy |
237 | + yield self.assert_machine_assignments("mysql", [0, 0]) |
238 | |
239 | === modified file 'ensemble/control/tests/test_deploy.py' |
240 | --- ensemble/control/tests/test_deploy.py 2011-07-25 01:28:41 +0000 |
241 | +++ ensemble/control/tests/test_deploy.py 2011-08-09 01:43:25 +0000 |
242 | @@ -29,7 +29,9 @@ |
243 | "ensemble": "environments", |
244 | "environments": { |
245 | "firstenv": { |
246 | - "type": "dummy", "admin-secret": "homer"}}} |
247 | + "type": "dummy", |
248 | + "admin-secret": "homer", |
249 | + "placement": "local"}}} |
250 | self.write_config(yaml.dump(config)) |
251 | self.config.load() |
252 | |
253 | @@ -149,7 +151,7 @@ |
254 | command(MATCH(match_config), MATCH(match_environment), |
255 | self.unbundled_repo_path, "sample", None, |
256 | MATCH(lambda x: isinstance(x, logging.Logger)), |
257 | - None) |
258 | + None, "unassigned") |
259 | self.mocker.replay() |
260 | self.mocker.result(succeed(True)) |
261 | |
262 | @@ -239,7 +241,6 @@ |
263 | error = yield self.assertFailure(failure, ServiceConfigError) |
264 | self.assertIn("Expected a YAML dict with service name ('myblog').", str(error)) |
265 | |
266 | - |
267 | @inlineCallbacks |
268 | def test_deploy_with_config(self): |
269 | """Valid config options should be available to the deployed |
270 | @@ -271,3 +272,60 @@ |
271 | service_state) |
272 | self.assertEqual(len(relations), 1) |
273 | self.assertEqual(relations[0].relation_name, "ring") |
274 | + |
275 | + @inlineCallbacks |
276 | + def test_deploy_with_local_policy(self): |
277 | + environment = self.config.get("firstenv") |
278 | + yield deploy.deploy(self.config, environment, self.unbundled_repo_path, |
279 | + "sample", "myblog", |
280 | + logging.getLogger("deploy"), |
281 | + placement_policy="local") |
282 | + |
283 | + topology = yield self.get_topology() |
284 | + service_id = topology.find_service_with_name("myblog") |
285 | + self.assertEqual(service_id, "service-%010d" % 0) |
286 | + exists = yield self.client.exists("/services/%s" % service_id) |
287 | + self.assertTrue(exists) |
288 | + |
289 | + machine_ids = topology.get_machines() |
290 | + # There should just be the one machine with local deployment |
291 | + self.assertEqual(machine_ids, ["machine-%010d" % 0]) |
292 | + exists = yield self.client.exists("/machines/%s" % machine_ids[0]) |
293 | + self.assertTrue(exists) |
294 | + |
295 | + # Verify the process still completes as expected |
296 | + unit_ids = topology.get_service_units(service_id) |
297 | + self.assertEqual(unit_ids, ["unit-%010d" % 0]) |
298 | + exists = yield self.client.exists("/units/%s" % unit_ids[0]) |
299 | + self.assertTrue(exists) |
300 | + |
301 | + service = yield self.service_state_manager.get_service_state("myblog") |
302 | + unit_state = yield service.get_unit_state("myblog/0") |
303 | + machine_id = yield unit_state.get_assigned_machine_id() |
304 | + self.assertEqual(machine_id, 0) |
305 | + |
306 | + @inlineCallbacks |
307 | + def test_deploy_policy_from_environment(self): |
308 | + config = { |
309 | + "ensemble": "environments", |
310 | + "environments": {"firstenv": { |
311 | + "placement": "local", |
312 | + "type": "dummy"}}} |
313 | + |
314 | + self.write_config(yaml.dump(config)) |
315 | + self.config.load() |
316 | + |
317 | + finished = self.setup_cli_reactor() |
318 | + self.setup_exit(0) |
319 | + self.mocker.replay() |
320 | + main(["deploy", "--environment", "firstenv", |
321 | + "--repository", self.unbundled_repo_path, |
322 | + "sample", "beekeeper"]) |
323 | + yield finished |
324 | + |
325 | + # and verify its placed on node 0 (as per local policy) |
326 | + service = yield self.service_state_manager.get_service_state("beekeeper") |
327 | + units = yield service.get_all_unit_states() |
328 | + unit = units[0] |
329 | + machine_id = yield unit.get_assigned_machine_id() |
330 | + self.assertEqual(machine_id, 0) |
331 | |
332 | === modified file 'ensemble/environment/config.py' |
333 | --- ensemble/environment/config.py 2011-08-02 08:12:16 +0000 |
334 | +++ ensemble/environment/config.py 2011-08-09 01:43:25 +0000 |
335 | @@ -41,18 +41,27 @@ |
336 | "default-instance-type": String(), |
337 | "default-ami": String(), |
338 | "ec2-uri": String(), |
339 | - "s3-uri": String()}, |
340 | + "s3-uri": String(), |
341 | + "placement": OneOf( |
342 | + Constant("unassigned"), |
343 | + Constant("local"))}, |
344 | optional=["access-key", "secret-key", |
345 | "default-instance-type", "default-ami", |
346 | - "region", "ec2-uri", "s3-uri"]), |
347 | + "region", "ec2-uri", "s3-uri", "placement"]), |
348 | "orchestra": KeyDict({"orchestra-server": String(), |
349 | "orchestra-user": String(), |
350 | "orchestra-pass": String(), |
351 | "admin-secret": String(), |
352 | "acquired-mgmt-class": String(), |
353 | +<<<<<<< TREE |
354 | "available-mgmt-class": String(), |
355 | "storage-url": String()}, |
356 | optional=["storage-url"]), |
357 | +======= |
358 | + "available-mgmt-class": String(), |
359 | + "placement": String()}, |
360 | + optional=["placement"]), |
361 | +>>>>>>> MERGE-SOURCE |
362 | "dummy": KeyDict({})}))}, |
363 | optional=["default"]) |
364 | |
365 | |
366 | === modified file 'ensemble/environment/environment.py' |
367 | --- ensemble/environment/environment.py 2010-10-15 04:51:17 +0000 |
368 | +++ ensemble/environment/environment.py 2011-08-09 01:43:25 +0000 |
369 | @@ -37,3 +37,8 @@ |
370 | self._machine_provider = MachineProvider( |
371 | self._name, self._environment_config) |
372 | return self._machine_provider |
373 | + |
374 | + @property |
375 | + def placement(self): |
376 | + """Unit placement policy name or None""" |
377 | + return self._environment_config.get("placement") |
378 | |
379 | === modified file 'ensemble/environment/tests/test_config.py' |
380 | --- ensemble/environment/tests/test_config.py 2011-08-05 07:17:27 +0000 |
381 | +++ ensemble/environment/tests/test_config.py 2011-08-09 01:43:25 +0000 |
382 | @@ -553,6 +553,31 @@ |
383 | self.fail("Did not properly require " |
384 | "control-buckets when type == 'ec2'") |
385 | |
386 | + def test_ec2_verifies_placement(self): |
387 | + self.config.write_sample() |
388 | + with open(self.default_path) as file: |
389 | + config = yaml.load(file.read()) |
390 | + config["environments"]["sample"]["placement"] = "random" |
391 | + |
392 | + self.write_config(yaml.dump(config), other_path=True) |
393 | + |
394 | + e = self.assertRaises(EnvironmentsConfigError, |
395 | + self.config.load, |
396 | + self.other_path) |
397 | + self.assertIn("expected 'unassigned', got 'random'", |
398 | + str(e)) |
399 | + |
400 | + with open(self.default_path) as file: |
401 | + config = yaml.load(file.read()) |
402 | + # Authorized keys are required for environment serialization. |
403 | + config["environments"]["sample"]["authorized-keys"] = "mickey" |
404 | + config["environments"]["sample"]["placement"] = "local" |
405 | + self.write_config(yaml.dump(config), other_path=True) |
406 | + |
407 | + self.config.load(self.other_path) |
408 | + data = self.config.get_default().get_serialization_data() |
409 | + self.assertEqual(data["sample"]["placement"], "local") |
410 | + |
411 | def test_orchestra_schema_requires(self): |
412 | requires = ( |
413 | "type orchestra-server orchestra-user orchestra-pass " |
414 | @@ -573,3 +598,14 @@ |
415 | else: |
416 | self.fail("Did not properly require %s when type == orchestra" |
417 | % require) |
418 | + |
419 | + def test_orchestra_verifies_placement(self): |
420 | + self.config.write_sample() |
421 | + config = yaml.load(SAMPLE_ORCHESTRA) |
422 | + |
423 | + config["environments"]["sample"]["placement"] = "local" |
424 | + self.write_config(yaml.dump(config), other_path=True) |
425 | + self.config.load(self.other_path) |
426 | + |
427 | + data = self.config.get_default().placement |
428 | + self.assertEqual(data, "local") |
429 | |
430 | === modified file 'ensemble/environment/tests/test_environment.py' |
431 | --- ensemble/environment/tests/test_environment.py 2010-10-08 23:23:48 +0000 |
432 | +++ ensemble/environment/tests/test_environment.py 2011-08-09 01:43:25 +0000 |
433 | @@ -78,3 +78,4 @@ |
434 | self.mocker.throw(SyntaxError()) |
435 | self.mocker.replay() |
436 | self.assertRaises(SyntaxError, env.get_serialization_data) |
437 | + |
438 | |
439 | === modified file 'ensemble/providers/orchestra/__init__.py' |
440 | --- ensemble/providers/orchestra/__init__.py 2011-08-05 07:17:27 +0000 |
441 | +++ ensemble/providers/orchestra/__init__.py 2011-08-09 01:43:25 +0000 |
442 | @@ -1,4 +1,10 @@ |
443 | +<<<<<<< TREE |
444 | from twisted.internet.defer import fail |
445 | +======= |
446 | +import copy |
447 | + |
448 | +from twisted.internet.defer import fail, succeed |
449 | +>>>>>>> MERGE-SOURCE |
450 | |
451 | from ensemble.errors import EnvironmentNotFound |
452 | from ensemble.providers.common.bootstrap import Bootstrap |
453 | @@ -99,7 +105,8 @@ |
454 | |
455 | Additionally this extracts credential information from the environment. |
456 | """ |
457 | - raise NotImplementedError() |
458 | + data = copy.deepcopy(self.config) |
459 | + return data |
460 | |
461 | def open_port(self, machine, port, protocol="tcp"): |
462 | """Expose port to the environment. |
463 | |
464 | === modified file 'ensemble/providers/orchestra/tests/test_provider.py' |
465 | --- ensemble/providers/orchestra/tests/test_provider.py 2011-07-31 00:40:19 +0000 |
466 | +++ ensemble/providers/orchestra/tests/test_provider.py 2011-08-09 01:43:25 +0000 |
467 | @@ -15,3 +15,27 @@ |
468 | "tetrascape", CONFIG) |
469 | self.assertEquals(provider.environment_name, "tetrascape") |
470 | self.assertEquals(provider.config, CONFIG) |
471 | +<<<<<<< TREE |
472 | +======= |
473 | + |
474 | + def test_config_serialization(self): |
475 | + """ |
476 | + The provider configuration can be serialized to yaml. |
477 | + """ |
478 | + keys_path = self.makeFile("my-keys") |
479 | + |
480 | + config = { "orchestra-user": "chaosmonkey", |
481 | + "orchestra-pass": "crash", |
482 | + "acquired-mgmt-class": "madeup", |
483 | + "available-mgmt-class": "madeup", |
484 | + "orchestra-server": "127.0.0.01", |
485 | + "authorized-keys-path": keys_path, |
486 | + } |
487 | + |
488 | + expected_serialization = config.copy() |
489 | + |
490 | + provider = MachineProvider("tetrascape", config) |
491 | + serialized = provider.get_serialization_data() |
492 | + self.assertEqual(serialized, expected_serialization) |
493 | + |
494 | +>>>>>>> MERGE-SOURCE |
495 | |
496 | === modified file 'ensemble/state/errors.py' |
497 | --- ensemble/state/errors.py 2011-07-25 19:36:48 +0000 |
498 | +++ ensemble/state/errors.py 2011-08-09 01:43:25 +0000 |
499 | @@ -298,3 +298,13 @@ |
500 | |
501 | def __str__(self): |
502 | return "Ambiguous endpoints: %r" % (self.endpoints,) |
503 | + |
504 | +class InvalidPlacementPolicy(EnsembleError): |
505 | + def __init__(self, policy_name): |
506 | + self._policy_name = policy_name |
507 | + |
508 | + def __str__(self): |
509 | + return "Invalid unit placement policy: %s" % self._policy_name |
510 | + |
511 | + |
512 | + |
513 | |
514 | === added file 'ensemble/state/placement.py' |
515 | --- ensemble/state/placement.py 1970-01-01 00:00:00 +0000 |
516 | +++ ensemble/state/placement.py 2011-08-09 01:43:25 +0000 |
517 | @@ -0,0 +1,58 @@ |
518 | +"""Various unit placement strategies for use in deploy and add-unit. |
519 | + |
520 | +The API used by the `place_unit` method is: |
521 | + |
522 | +machine_state = yield placement_strategy(zk_client, |
523 | + machine_state_manager, |
524 | + unit_state) |
525 | + |
526 | +The placement strategy is passed the machine manager for the |
527 | +deployment and the unit_state it is attempting to place. According to |
528 | +its policy it should yield back the machine_state for where it placed |
529 | +the unit. |
530 | +""" |
531 | + |
532 | +from twisted.internet.defer import inlineCallbacks, returnValue |
533 | + |
534 | +from ensemble.state.errors import NoUnusedMachines, InvalidPlacementPolicy |
535 | + |
536 | +LOCAL_POLICY = "local" |
537 | +UNASSIGNED_POLICY = "unassigned" |
538 | + |
539 | + |
540 | +@inlineCallbacks |
541 | +def _local_placement(client, machine_state_manager, unit_state): |
542 | + machine = yield machine_state_manager.get_machine_state(0) |
543 | + yield unit_state.assign_to_machine(machine) |
544 | + returnValue(machine) |
545 | + |
546 | + |
547 | +@inlineCallbacks |
548 | +def _unassigned_placement(client, machine_state_manager, unit_state): |
549 | + try: |
550 | + machine_state = yield unit_state.assign_to_unused_machine() |
551 | + except NoUnusedMachines: |
552 | + machine_state = yield machine_state_manager.add_machine_state() |
553 | + yield unit_state.assign_to_unused_machine() |
554 | + |
555 | + returnValue(machine_state) |
556 | + |
557 | + |
558 | +_PLACEMENT_LOOKUP = { |
559 | + LOCAL_POLICY: _local_placement, |
560 | + UNASSIGNED_POLICY: _unassigned_placement, |
561 | + } |
562 | + |
563 | + |
564 | +def place_unit(client, policy_name, machine_state_manager, unit_state): |
565 | + """Return machine state of unit_states assignment.""" |
566 | + |
567 | + # default policy handling |
568 | + if policy_name is None: |
569 | + placement = _unassigned_placement |
570 | + else: |
571 | + placement = _PLACEMENT_LOOKUP.get(policy_name) |
572 | + if placement is None: |
573 | + raise InvalidPlacementPolicy(policy_name) |
574 | + |
575 | + return placement(client, machine_state_manager, unit_state) |
576 | |
577 | === added file 'ensemble/state/tests/test_placement.py' |
578 | --- ensemble/state/tests/test_placement.py 1970-01-01 00:00:00 +0000 |
579 | +++ ensemble/state/tests/test_placement.py 2011-08-09 01:43:25 +0000 |
580 | @@ -0,0 +1,57 @@ |
581 | + |
582 | +from twisted.internet.defer import inlineCallbacks |
583 | + |
584 | +from ensemble.state.errors import InvalidPlacementPolicy |
585 | +from ensemble.state.placement import place_unit |
586 | +from ensemble.state.tests.test_service import ServiceStateManagerTestBase |
587 | + |
588 | + |
589 | +class TestPlacement(ServiceStateManagerTestBase): |
590 | + |
591 | + @inlineCallbacks |
592 | + def setUp(self): |
593 | + yield super(TestPlacement, self).setUp() |
594 | + |
595 | + self.service = yield self.add_service_from_formula("mysql") |
596 | + self.unit_state = yield self.service.add_unit_state() |
597 | + |
598 | + @inlineCallbacks |
599 | + def test_unassign_placement(self): |
600 | + machine1 = yield self.machine_state_manager.add_machine_state() |
601 | + machine2 = yield self.machine_state_manager.add_machine_state() |
602 | + |
603 | + unit2 = yield self.service.add_unit_state() |
604 | + |
605 | + # Take machine 1 manually |
606 | + yield self.unit_state.assign_to_machine(machine1) |
607 | + |
608 | + ms2 = yield place_unit(self.client, "unassigned", self.machine_state_manager, unit2) |
609 | + self.assertEqual(ms2.id, machine2.id) |
610 | + |
611 | + # and placing a new unit creates a new machine state |
612 | + unit3 = yield self.service.add_unit_state() |
613 | + ms3 = yield place_unit(self.client, "unassigned", self.machine_state_manager, unit3) |
614 | + self.assertEqual(ms3.id, machine2.id + 1) |
615 | + |
616 | + @inlineCallbacks |
617 | + def test_local_placement(self): |
618 | + ms0 = yield self.machine_state_manager.add_machine_state() |
619 | + self.assertEqual(ms0.id, 0) |
620 | + |
621 | + # These shouldn't be used with local (but should be available |
622 | + # to prove a different policy is at work) |
623 | + yield self.machine_state_manager.add_machine_state() |
624 | + yield self.machine_state_manager.add_machine_state() |
625 | + unit2 = yield self.service.add_unit_state() |
626 | + |
627 | + ms1 = yield place_unit(self.client, "local", self.machine_state_manager, self.unit_state) |
628 | + ms2 = yield place_unit(self.client, "local", self.machine_state_manager, unit2) |
629 | + |
630 | + # Everything should end up on machine 0 with local placement |
631 | + # even though other machines are available |
632 | + self.assertEqual(ms0.id, ms1.id) |
633 | + self.assertEqual(ms0.id, ms2.id) |
634 | + |
635 | + def test_invalid_placement_policy_exception(self): |
636 | + ex = InvalidPlacementPolicy("foo") |
637 | + self.assertEqual(str(ex), "Invalid unit placement policy: foo") |
Generally all looks good to me; please address the following minor issues before you merge.
[1]
It would be good to keep the config requirements in sync between orchestra and ec2, so please (1) merge latest trunk and (2) add handling and tests for placement in the orchestra config schema.
[2]
+class InvalidPlacemen tPolicy( EnsembleError) :
We don't seem to be placing exceptions in consistent places; I see that some things here are using ensemble. state.errors, but InvalidPlacemen tPolicy is in ensemble. state.placement ; meanwhile, several provider-specific errors are in ensemble.errors rather than (say) ensemble. providers. errors . I feel it would be good to rationalise this a bit; I don't strongly favour any particular scheme, though.
[3]
+ self._policy_name = policy_name
I have a slight bias towards making exception attributes public, because I don't see the value of hiding their information: it might be useful to read it from code at some point, without having to change the exception definition. Conversely, anyone crazy enough to actually modify a caught exception isn't going to be put off by a mere leading _ ;-). Just a suggestion, not a condition.
[4]
+def local_placement (client, machine_ state_manager, unit_state):
+def unassigned_ placement( client, machine_ state_manager, unit_state):
+placement_lookup = {
It would be good to make these module-_private, and to rename placement_lookup to _PLACEMENT_LOOKUP, just to make its intent a little clearer at first glance.
[5]
The python string "unassigned" appears in enough places (in the code, not just the tests) that I think I'd prefer it to be named, even if we don't expect it to change all that much (and if you do that, you may as well name "local" as well).
[6]
There are a couple of little pep8 issues that could use a look.