Merge lp:~hazmat/pyjuju/cli-placement-restore into lp:pyjuju

Proposed by Kapil Thangavelu
Status: Work in progress
Proposed branch: lp:~hazmat/pyjuju/cli-placement-restore
Merge into: lp:pyjuju
Diff against target: 450 lines (+143/-56)
12 files modified
juju/control/add_unit.py (+13/-4)
juju/control/deploy.py (+10/-4)
juju/control/tests/test_add_unit.py (+36/-0)
juju/control/tests/test_deploy.py (+33/-1)
juju/providers/common/base.py (+4/-6)
juju/providers/common/tests/test_base.py (+2/-5)
juju/providers/dummy.py (+4/-6)
juju/providers/lxc/__init__.py (+2/-5)
juju/providers/lxc/tests/test_provider.py (+2/-7)
juju/providers/tests/test_dummy.py (+4/-7)
juju/state/placement.py (+19/-0)
juju/state/tests/test_placement.py (+14/-11)
To merge this branch: bzr merge lp:~hazmat/pyjuju/cli-placement-restore
Reviewer Review Type Date Requested Status
Gustavo Niemeyer Needs Fixing
Review via email: mp+77245@code.launchpad.net

Description of the change

Restore command line placement.

There are several folks who would like to see its restoration. Both for
flexibility down the road and more immediately as mechanism to conserve
machine resources. The alternative of modifying environment config between
deploys is suboptimal. Its being used for example in reducing the number of
machines required for openstack. Although future use of ideal policies (limited
number of machines) is suboptimal, there are other placement policies taking
into account machine resources or cross availability zone for which this option
would be ideal.

To post a comment you must log in.
Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

Why is the placement option in the environment suboptimal?

I'm not trying to make things difficult gratuitously, but I feel quite unhappy about the mechanism we're introducing here. This was introduced as a hack, and I'm now hearing people depending on the hack.. soon we'll see blog posts around the hack and people believing that this is the way Ensemble is supposed to work.

I understand folks want to have better control of placement. I surely want it too.. this is just not the way to do it in Ensemble, for several reasons that I'll be happy to discuss in a better forum.

If the placement option in the environment enable the Adam and Clint to do what they have to do, I'd prefer to keep it that way.

review: Needs Information
Revision history for this message
Adam Gandelman (gandelman-a) wrote :

Hi--

Moving "--placement=local" from argv to "placement: local" in environments.yaml seems trivial enough, however, my specific use case often has me dealing with many different formulas, some of which are destroyed and re-deployed many times in one sitting. Having to to parse and modify environments.yaml many times over between 'juju deploy' to switch to and from 'local' placement policy seems needlessly tedious. Having to put together my own wrappers around 'juju deploy' and environments.yaml to do this for me and emulate what '--placement=local' once provided seems even more suboptimal.

We understand its a hack but we're using it as a workaround to Bug #806241. Without it, Juju goes from something that might be useful "sometime in the future" given our hardware constraints to something that is extremely useful *now* and *central* to our QA and integration testing of specific software stacks. IMHO, making it more difficult to use due to user facing changes like this (especially this late in the cycle) would be unfortunate.

Revision history for this message
Clint Byrum (clint-fewbar) wrote :

I share your concern that --placement local will be abused if it is available. However, without this, testing scripts for hardware deployment or against limited size clouds become incredibly complex. I'd be comfortable with a WARNING: --placement on the command line is deprecated type of message once there is an alternative that allows making use of limited resources, but until then, I think we have to have at least *some* story other than "go buy more hardware".

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

Alright, so here is a review to try to get to some agreement:

[1]

Let's have the renamed the option to a boolean called

    --temporary-local-hack

With the following help message:

    help="Temporary placement hack for testing juju."

[2]

Let's not change the provider interface, or introduce pick_policy,
etc. The provider interface we have today looks nice and is going
into the direction we want.

Instead, we do something as simple as this:

    placement = provider.get_placement_policy()
    if options.temporary_local_hack:
        placement = "local"

[3]

Once we sort out making our system stable for 12.04, let's come back
and do placement correctly. It's definitely something that should be
high in our priority list.

review: Needs Fixing
Revision history for this message
Kapil Thangavelu (hazmat) wrote :

Excerpts from Gustavo Niemeyer's message of Thu Sep 29 20:48:30 UTC 2011:
> Review: Needs Fixing
>
> Alright, so here is a review to try to get to some agreement:
>
> [1]
>
> Let's have the renamed the option to a boolean called
>
> --temporary-local-hack
>
> With the following help message:
>
> help="Temporary placement hack for testing juju."

We can even hide it from the default cli help.

>
> [2]
>
> Let's not change the provider interface, or introduce pick_policy,
> etc. The provider interface we have today looks nice and is going
> into the direction we want.
>
> Instead, we do something as simple as this:
>
> placement = provider.get_placement_policy()
> if options.temporary_local_hack:
> placement = "local"
>
>
> [3]
>
> Once we sort out making our system stable for 12.04, let's come back
> and do placement correctly. It's definitely something that should be
> high in our priority list.

sounds reasonable.

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

i thought about this some more, and i think i'd rather just drop this and adopt clint's approach of giving users direct machine placement options.

Revision history for this message
Benjamin Saller (bcsaller) wrote :

I think thats just another policy, --placement machine:1

Those were always supposed to be strategy name or plans (implied by
the above) in my mind.

On Wed, Oct 5, 2011 at 7:46 AM, Kapil Thangavelu
<email address hidden> wrote:
> i thought about this some more, and i think i'd rather just drop this and adopt clint's approach of giving users direct machine placement options.
> --
> https://code.launchpad.net/~hazmat/juju/cli-placement-restore/+merge/77245
> You are subscribed to branch lp:juju.
>

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

interesting, thanks.

Excerpts from Benjamin Saller's message of Wed Oct 05 15:38:39 UTC 2011:
> I think thats just another policy, --placement machine:1
>
> Those were always supposed to be strategy name or plans (implied by
> the above) in my mind.
>
>
> On Wed, Oct 5, 2011 at 7:46 AM, Kapil Thangavelu
> <email address hidden> wrote:
> > i thought about this some more, and i think i'd rather just drop this and adopt clint's approach of giving users direct machine placement options.
> > --
> > https://code.launchpad.net/~hazmat/juju/cli-placement-restore/+merge/77245
> > You are subscribed to branch lp:juju.
> >
>

Unmerged revisions

397. By Kapil Thangavelu

restore placement cli

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'juju/control/add_unit.py'
2--- juju/control/add_unit.py 2011-09-27 02:14:56 +0000
3+++ juju/control/add_unit.py 2011-09-27 22:31:25 +0000
4@@ -3,7 +3,7 @@
5 from twisted.internet.defer import inlineCallbacks
6
7 from juju.control.utils import get_environment
8-from juju.state.placement import place_unit
9+from juju.state.placement import place_unit, pick_policy
10 from juju.state.service import ServiceStateManager
11
12
13@@ -16,6 +16,9 @@
14 sub_parser.add_argument(
15 "service_name",
16 help="Name of the service a unit should be added for")
17+ sub_parser.add_argument(
18+ "--placement",
19+ help="Placement policy used to place units of this service")
20
21 return sub_parser
22
23@@ -23,20 +26,26 @@
24 def command(options):
25 """Add a new service unit."""
26 environment = get_environment(options)
27+
28+ if not options.placement:
29+ options.placement = environment.placement
30+
31 return add_unit(
32 options.environments,
33 environment,
34 options.verbose,
35 options.log,
36- options.service_name)
37+ options.service_name,
38+ options.placement)
39
40
41 @inlineCallbacks
42-def add_unit(config, environment, verbose, log, service_name):
43+def add_unit(
44+ config, environment, verbose, log, service_name, placement_policy):
45 """Add a unit of a service to the environment.
46 """
47 provider = environment.get_machine_provider()
48- placement_policy = provider.get_placement_policy()
49+ placement_policy = pick_policy(placement_policy, provider)
50 client = yield provider.connect()
51
52 try:
53
54=== modified file 'juju/control/deploy.py'
55--- juju/control/deploy.py 2011-09-27 02:14:56 +0000
56+++ juju/control/deploy.py 2011-09-27 22:31:25 +0000
57@@ -12,7 +12,7 @@
58
59 from juju.state.endpoint import RelationEndpoint
60 from juju.state.environment import EnvironmentStateManager
61-from juju.state.placement import place_unit
62+from juju.state.placement import place_unit, pick_policy
63 from juju.state.relation import RelationStateManager
64 from juju.state.service import ServiceStateManager
65
66@@ -41,6 +41,10 @@
67 "--config",
68 help="YAML file containing service options")
69
70+ sub_parser.add_argument(
71+ "--placement",
72+ help="Placement policy used to place units of this service")
73+
74 return sub_parser
75
76
77@@ -56,7 +60,8 @@
78 options.charm,
79 options.service_name,
80 options.log,
81- options.config)
82+ options.config,
83+ options.placement)
84
85
86 def parse_config_options(config_file, service_name):
87@@ -77,7 +82,7 @@
88
89 @inlineCallbacks
90 def deploy(env_config, environment, repository_path, charm_name,
91- service_name, log, config_file=None):
92+ service_name, log, config_file=None, placement_policy=None):
93 """Deploy a charm within an environment.
94
95
96@@ -96,7 +101,7 @@
97 charm = repository.find(charm_name)
98
99 provider = environment.get_machine_provider()
100- placement_policy = provider.get_placement_policy()
101+ placement_policy = pick_policy(placement_policy, provider)
102 client = yield provider.connect()
103
104 try:
105@@ -133,6 +138,7 @@
106
107 # Create a service unit
108 unit_state = yield service_state.add_unit_state()
109+
110 yield place_unit(client, placement_policy, unit_state)
111
112 # Check if we have any peer relations to establish
113
114=== modified file 'juju/control/tests/test_add_unit.py'
115--- juju/control/tests/test_add_unit.py 2011-09-27 02:14:56 +0000
116+++ juju/control/tests/test_add_unit.py 2011-09-27 22:31:25 +0000
117@@ -87,6 +87,42 @@
118 yield self.assert_machine_assignments("mysql", [1, 2])
119
120 @inlineCallbacks
121+ def test_add_unit_with_local_policy(self):
122+ """
123+ 'juju add-unit <service_name>' will add a new service
124+ unit of the given service.
125+ """
126+ # rework setUp so that service_unit1 is on machine 0
127+ # to simulate local deployment
128+ ms0 = yield self.machine_state_manager.get_machine_state(0)
129+ yield self.service_unit1.unassign_from_machine()
130+ yield self.service_unit1.assign_to_machine(ms0)
131+
132+ unit_names = yield self.service_state1.get_unit_names()
133+ self.assertEqual(len(unit_names), 1)
134+
135+ finished = self.setup_cli_reactor()
136+ self.setup_exit(0)
137+ self.mocker.replay()
138+ main(["add-unit", "mysql", "--placement", "local"])
139+ yield finished
140+
141+ # verify the unit and its machine assignment.
142+ unit_names = yield self.service_state1.get_unit_names()
143+ self.assertEqual(len(unit_names), 2)
144+
145+ topology = yield self.get_topology()
146+ unit = yield self.service_state1.get_unit_state("mysql/1")
147+ machine_id = topology.get_service_unit_machine(
148+ self.service_state1.internal_id, unit.internal_id)
149+ self.assertNotEqual(machine_id, None)
150+ self.assertIn(
151+ "Unit 'mysql/1' added to service 'mysql'",
152+ self.output.getvalue())
153+ # adding a second unit still assigns to machine 0 with local policy
154+ yield self.assert_machine_assignments("mysql", [0, 0])
155+
156+ @inlineCallbacks
157 def test_policy_from_environment(self):
158 config = {
159 "environments": {"firstenv": {
160
161=== modified file 'juju/control/tests/test_deploy.py'
162--- juju/control/tests/test_deploy.py 2011-09-27 02:14:56 +0000
163+++ juju/control/tests/test_deploy.py 2011-09-27 22:31:25 +0000
164@@ -150,7 +150,7 @@
165 command(MATCH(match_config), MATCH(match_environment),
166 self.unbundled_repo_path, "sample", None,
167 MATCH(lambda x: isinstance(x, logging.Logger)),
168- None)
169+ None, None)
170 self.mocker.replay()
171 self.mocker.result(succeed(True))
172
173@@ -301,6 +301,38 @@
174 self.assertEqual(relations[0].relation_name, "ring")
175
176 @inlineCallbacks
177+ def test_deploy_with_local_policy(self):
178+ environment = self.config.get("firstenv")
179+ yield deploy.deploy(
180+ self.config, environment, self.unbundled_repo_path,
181+ "sample", "myblog", logging.getLogger("deploy"),
182+ placement_policy="local")
183+
184+ topology = yield self.get_topology()
185+ service_id = topology.find_service_with_name("myblog")
186+ self.assertEqual(service_id, "service-%010d" % 0)
187+ exists = yield self.client.exists("/services/%s" % service_id)
188+ self.assertTrue(exists)
189+
190+ machine_ids = topology.get_machines()
191+
192+ # There should just be the one machine with local deployment
193+ self.assertEqual(machine_ids, ["machine-%010d" % 0])
194+ exists = yield self.client.exists("/machines/%s" % machine_ids[0])
195+ self.assertTrue(exists)
196+
197+ # Verify the process still completes as expected
198+ unit_ids = topology.get_service_units(service_id)
199+ self.assertEqual(unit_ids, ["unit-%010d" % 0])
200+ exists = yield self.client.exists("/units/%s" % unit_ids[0])
201+ self.assertTrue(exists)
202+
203+ service = yield self.service_state_manager.get_service_state("myblog")
204+ unit_state = yield service.get_unit_state("myblog/0")
205+ machine_id = yield unit_state.get_assigned_machine_id()
206+ self.assertEqual(machine_id, 0)
207+
208+ @inlineCallbacks
209 def test_deploy_policy_from_environment(self):
210 config = {
211 "environments": {"firstenv": {
212
213=== modified file 'juju/providers/common/base.py'
214--- juju/providers/common/base.py 2011-09-27 02:14:56 +0000
215+++ juju/providers/common/base.py 2011-09-27 22:31:25 +0000
216@@ -4,7 +4,7 @@
217 from twisted.internet.defer import inlineCallbacks, returnValue
218
219 from juju.environment.errors import EnvironmentsConfigError
220-from juju.state.placement import UNASSIGNED_POLICY
221+from juju.state.placement import list_policies
222
223
224 from .bootstrap import Bootstrap
225@@ -48,12 +48,10 @@
226 self.environment_name = environment_name
227 self.config = config
228
229- def get_placement_policy(self):
230- """Get the unit placement policy for the provider.
231-
232- :param preference: A user specified plcaement policy preference
233+ def get_placement_policies(self):
234+ """Get the available unit placement policies for the provider.
235 """
236- return self.config.get("placement", UNASSIGNED_POLICY)
237+ return list_policies()
238
239 def get_serialization_data(self):
240 """Get provider configuration suitable for serialization."""
241
242=== modified file 'juju/providers/common/tests/test_base.py'
243--- juju/providers/common/tests/test_base.py 2011-09-27 02:14:56 +0000
244+++ juju/providers/common/tests/test_base.py 2011-09-27 22:31:25 +0000
245@@ -4,7 +4,7 @@
246 from juju.lib.testing import TestCase
247 from juju.machine import ProviderMachine
248 from juju.providers.common.base import MachineProviderBase
249-from juju.state.placement import UNASSIGNED_POLICY
250+from juju.state.placement import list_policies
251
252
253 class SomeError(Exception):
254@@ -58,10 +58,7 @@
255 def test_get_provider_placement(self):
256 provider = DummyProvider()
257 self.assertEqual(
258- provider.get_placement_policy(), UNASSIGNED_POLICY)
259- provider = DummyProvider({"placement": "local"})
260- self.assertEqual(
261- provider.get_placement_policy(), "local")
262+ provider.get_placement_policies(), list_policies())
263
264 def test_get_machine_error(self):
265 provider = DummyProvider()
266
267=== modified file 'juju/providers/dummy.py'
268--- juju/providers/dummy.py 2011-09-27 02:14:56 +0000
269+++ juju/providers/dummy.py 2011-09-27 22:31:25 +0000
270@@ -11,7 +11,7 @@
271
272
273 from juju.machine import ProviderMachine
274-from juju.state.placement import UNASSIGNED_POLICY
275+from juju.state.placement import list_policies
276 from juju.providers.common.files import FileStorage
277
278 log = logging.getLogger("juju.providers")
279@@ -34,12 +34,10 @@
280 self._state = None
281 self._storage = None
282
283- def get_placement_policy(self):
284- """Get the unit placement policy for the provider.
285-
286- :param preference: A user specified plcaement policy preference
287+ def get_placement_policies(self):
288+ """Get the available unit placement policies for the provider.
289 """
290- return self.config.get("placement", UNASSIGNED_POLICY)
291+ return list_policies()
292
293 @property
294 def provider_type(self):
295
296=== modified file 'juju/providers/lxc/__init__.py'
297--- juju/providers/lxc/__init__.py 2011-09-27 02:51:59 +0000
298+++ juju/providers/lxc/__init__.py 2011-09-27 22:31:25 +0000
299@@ -39,12 +39,9 @@
300 self._directory = os.path.join(
301 self.config["data-dir"], self._qualified_name)
302
303- def get_placement_policy(self):
304+ def get_placement_policies(self):
305 """Local dev supports only one unit placement policy."""
306- if self.config.get("placement", LOCAL_POLICY) != LOCAL_POLICY:
307- raise ProviderError(
308- "Unsupported placement policy for local provider")
309- return LOCAL_POLICY
310+ return [LOCAL_POLICY]
311
312 @property
313 def provider_type(self):
314
315=== modified file 'juju/providers/lxc/tests/test_provider.py'
316--- juju/providers/lxc/tests/test_provider.py 2011-09-27 02:51:59 +0000
317+++ juju/providers/lxc/tests/test_provider.py 2011-09-27 22:31:25 +0000
318@@ -41,15 +41,10 @@
319 user_name = pwd.getpwuid(os.getuid()).pw_name
320 return "%s-%s" % (user_name, self.provider.environment_name)
321
322- def test_get_placement_policy(self):
323+ def test_get_placement_policies(self):
324 """Lxc provider only supports local placement."""
325 self.assertEqual(
326- self.provider.get_placement_policy(), "local")
327- provider = MachineProvider(
328- "test", {"placement": "unassigned",
329- "data-dir": self.makeDir()})
330- self.assertRaises(
331- ProviderError, provider.get_placement_policy)
332+ self.provider.get_placement_policies(), ["local"])
333
334 def assertDir(self, *path_parts):
335 path = os.path.join(*path_parts)
336
337=== modified file 'juju/providers/tests/test_dummy.py'
338--- juju/providers/tests/test_dummy.py 2011-09-27 02:14:56 +0000
339+++ juju/providers/tests/test_dummy.py 2011-09-27 22:31:25 +0000
340@@ -7,7 +7,7 @@
341 from juju.errors import ProviderError
342 from juju.machine import ProviderMachine
343 from juju.providers.dummy import MachineProvider, DummyMachine
344-from juju.state.placement import UNASSIGNED_POLICY
345+from juju.state.placement import list_policies
346 from juju.lib.testing import TestCase
347
348
349@@ -24,12 +24,9 @@
350 def test_provider_type(self):
351 self.assertEqual(self.provider.provider_type, "dummy")
352
353- def test_get_placement_policy(self):
354- self.assertEqual(
355- self.provider.get_placement_policy(), UNASSIGNED_POLICY)
356- self.provider = MachineProvider("foo", {"placement": "local"})
357- self.assertEqual(
358- self.provider.get_placement_policy(), "local")
359+ def test_get_placement_policies(self):
360+ self.assertEqual(self.provider.get_placement_policies(),
361+ list_policies())
362
363 @inlineCallbacks
364 def test_bootstrap(self):
365
366=== modified file 'juju/state/placement.py'
367--- juju/state/placement.py 2011-09-27 02:02:42 +0000
368+++ juju/state/placement.py 2011-09-27 22:31:25 +0000
369@@ -55,10 +55,29 @@
370 }
371
372
373+def list_policies():
374+ """List all policies
375+ """
376+ # Ordering is important, and permissive providers use this
377+ # function to get the policy enumeratoin, the first item is the
378+ # default used in the absence of a user preference.
379+ return [UNASSIGNED_POLICY, LOCAL_POLICY]
380+
381+
382 def pick_policy(preference, provider):
383+ """Pick a provider appropriate placement policy.
384+
385+ :param preference: A user expressed placement policy preference.
386+ :param provider: A machine provider.
387+ """
388 policies = provider.get_placement_policies()
389+ # If no user policy expressed, check the environment config
390+ if not preference:
391+ preference = provider.config.get("placement")
392+ # If there's still no preference, chooce the provider's first policy.
393 if not preference:
394 return policies[0]
395+ # Validate the user policy preference.
396 if preference not in policies:
397 raise InvalidPlacementPolicy(
398 preference, provider.provider_type, policies)
399
400=== modified file 'juju/state/tests/test_placement.py'
401--- juju/state/tests/test_placement.py 2011-09-27 02:02:42 +0000
402+++ juju/state/tests/test_placement.py 2011-09-27 22:31:25 +0000
403@@ -4,6 +4,7 @@
404 from juju.errors import InvalidPlacementPolicy
405 from juju.state.placement import place_unit, pick_policy
406 from juju.state.tests.test_service import ServiceStateManagerTestBase
407+from juju.providers.dummy import MachineProvider
408
409
410 class TestPlacement(ServiceStateManagerTestBase):
411@@ -16,26 +17,28 @@
412 self.unit_state = yield self.service.add_unit_state()
413
414 def test_pick_policy(self):
415- mock_provider = self.mocker.mock()
416- mock_provider.get_placement_policies()
417- self.mocker.result(["unassigned", "local", "new"])
418- self.mocker.count(3)
419- mock_provider.provider_type
420- self.mocker.result("dummy")
421- self.mocker.replay()
422-
423+ provider = MachineProvider("foobar", {})
424 # No selection gets first listed provider policy
425 self.assertEqual(
426- pick_policy(None, mock_provider), "unassigned")
427+ pick_policy(None, provider), "unassigned")
428
429 # If the user selection doesn't match we get an error
430 self.assertRaises(
431 InvalidPlacementPolicy,
432- pick_policy, "smart", mock_provider)
433+ pick_policy, "smart", provider)
434
435 # The user choice is respected if its available
436 self.assertEqual(
437- pick_policy("new", mock_provider), "new")
438+ pick_policy("local", provider), "local")
439+
440+ # Invalid environment placement setting is an error
441+ provider = MachineProvider("foobar", {"placement": "smart"})
442+ self.assertRaises(
443+ InvalidPlacementPolicy, pick_policy, None, provider)
444+
445+ # User selection takes precedence over environment
446+ provider = MachineProvider("foobar", {"placement": "unassigned"})
447+ self.assertEqual(pick_policy("local", provider), "local")
448
449 @inlineCallbacks
450 def test_unassign_placement(self):

Subscribers

People subscribed via source and target branches

to status/vote changes: