Merge lp:~bcsaller/pyjuju/unit-placement-policy into lp:pyjuju

Proposed by Benjamin Saller
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
Reviewer Review Type Date Requested Status
Gustavo Niemeyer Approve
William Reade (community) Approve
Review via email: mp+70387@code.launchpad.net

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.

To post a comment you must log in.
Revision history for this message
William Reade (fwereade) 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.

[2]

+class InvalidPlacementPolicy(EnsembleError):

We don't seem to be placing exceptions in consistent places; I see that some things here are using ensemble.state.errors, but InvalidPlacementPolicy 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.

review: Approve
Revision history for this message
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 InvalidPlacementPolicy(EnsembleError):
>
> We don't seem to be placing exceptions in consistent places; I see that some
> things here are using ensemble.state.errors, but InvalidPlacementPolicy 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.

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(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.
>

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.

Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

Good stuff, +1!

[1]

+ env_defaults = environment.get_serialization_data()
+ options.placement = env_defaults[environment.name].get(
+ "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 NotImplementedError()
+ data = copy.deepcopy(self.config)
+ return data

These changes don't seem testsed in the orchestra API.

[3]

+class InvalidPlacementPolicy(EnsembleError):
+ 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_placement(client, machine_state_manager, unit_state):
+ machine = yield machine_state_manager.get_machine_state(0)
+ yield unit_state.assign_to_machine(machine)
+ returnValue(machine)

The overall organization feels simple. Thanks.

[5]

+def place_unit(client, policy_name, machine_state_manager, unit_state):
+ """Return machine state of unit_states assignment."""
+
+ placement = _PLACEMENT_LOOKUP.get(policy_name)

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.

review: Approve
Revision history for this message
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.get_serialization_data()
> +        options.placement = env_defaults[environment.name].get(
> +            "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 NotImplementedError()
> +        data = copy.deepcopy(self.config)
> +        return data
>
> These changes don't seem testsed in the orchestra API.
>

Added test

> [3]
>
> +class InvalidPlacementPolicy(EnsembleError):
> +    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_placement(client, machine_state_manager, unit_state):
> +    machine = yield machine_state_manager.get_machine_state(0)
> +    yield unit_state.assign_to_machine(machine)
> +    returnValue(machine)
>
> The overall organization feels simple. Thanks.
>
:)

> [5]
>
> +def place_unit(client, policy_name, machine_state_manager, unit_state):
> +    """Return machine state of unit_states assignment."""
> +
> +    placement = _PLACEMENT_LOOKUP.get(policy_name)
>
> 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://code.launchpad.net/~bcsaller/ensemble/unit-placement-policy/+merge/70387
> You are the owner of lp:~bcsaller/ensemble/unit-placement-policy.
>

302. By Benjamin Saller

review changes

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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")

Subscribers

People subscribed via source and target branches

to status/vote changes: