Merge lp:~fwereade/pyjuju/shadow-trunk-1204 into lp:pyjuju

Proposed by William Reade
Status: Merged
Merge reported by: Kapil Thangavelu
Merged at revision: not available
Proposed branch: lp:~fwereade/pyjuju/shadow-trunk-1204
Merge into: lp:pyjuju
Diff against target: 7373 lines (+2733/-1538)
97 files modified
docs/source/internals/constraints-notes.rst (+78/-0)
juju/agents/provision.py (+3/-2)
juju/agents/tests/common.py (+3/-19)
juju/agents/tests/test_machine.py (+6/-14)
juju/agents/tests/test_provision.py (+33/-48)
juju/control/__init__.py (+2/-0)
juju/control/add_unit.py (+6/-1)
juju/control/bootstrap.py (+16/-2)
juju/control/constraints_get.py (+75/-0)
juju/control/constraints_set.py (+41/-28)
juju/control/deploy.py (+13/-18)
juju/control/initialize.py (+7/-1)
juju/control/legacy.py (+41/-0)
juju/control/terminate_machine.py (+2/-0)
juju/control/tests/test_add_relation.py (+0/-7)
juju/control/tests/test_add_unit.py (+44/-9)
juju/control/tests/test_bootstrap.py (+33/-14)
juju/control/tests/test_config_get.py (+0/-4)
juju/control/tests/test_config_set.py (+0/-4)
juju/control/tests/test_constraints_get.py (+116/-0)
juju/control/tests/test_constraints_set.py (+28/-17)
juju/control/tests/test_control.py (+5/-10)
juju/control/tests/test_debug_hooks.py (+0/-9)
juju/control/tests/test_debug_log.py (+1/-7)
juju/control/tests/test_deploy.py (+66/-7)
juju/control/tests/test_destroy_service.py (+0/-5)
juju/control/tests/test_initialize.py (+26/-3)
juju/control/tests/test_remove_unit.py (+0/-6)
juju/control/tests/test_resolved.py (+0/-6)
juju/control/tests/test_scp.py (+2/-20)
juju/control/tests/test_ssh.py (+0/-16)
juju/control/tests/test_status.py (+0/-8)
juju/control/tests/test_terminate_machine.py (+9/-7)
juju/control/tests/test_upgrade_charm.py (+0/-10)
juju/control/tests/test_utils.py (+0/-6)
juju/control/utils.py (+27/-0)
juju/environment/config.py (+49/-55)
juju/environment/tests/test_config.py (+93/-12)
juju/hooks/tests/test_invoker.py (+7/-2)
juju/machine/constraints.py (+241/-252)
juju/machine/tests/test_constraints.py (+284/-243)
juju/machine/unit.py (+0/-1)
juju/providers/common/base.py (+17/-8)
juju/providers/common/bootstrap.py (+8/-2)
juju/providers/common/cloudinit.py (+15/-2)
juju/providers/common/launch.py (+23/-3)
juju/providers/common/tests/data/cloud_init_bootstrap (+2/-2)
juju/providers/common/tests/data/cloud_init_bootstrap_zookeepers (+2/-2)
juju/providers/common/tests/data/cloud_init_distro (+4/-3)
juju/providers/common/tests/data/cloud_init_ppa (+4/-3)
juju/providers/common/tests/test_base.py (+4/-0)
juju/providers/common/tests/test_bootstrap.py (+27/-12)
juju/providers/common/tests/test_cloudinit.py (+4/-1)
juju/providers/common/tests/test_launch.py (+56/-35)
juju/providers/dummy.py (+10/-1)
juju/providers/ec2/__init__.py (+28/-9)
juju/providers/ec2/launch.py (+10/-6)
juju/providers/ec2/tests/common.py (+15/-11)
juju/providers/ec2/tests/data/bootstrap_cloud_init (+4/-4)
juju/providers/ec2/tests/test_bootstrap.py (+32/-36)
juju/providers/ec2/tests/test_launch.py (+103/-79)
juju/providers/ec2/tests/test_provider.py (+89/-1)
juju/providers/ec2/tests/test_utils.py (+173/-70)
juju/providers/ec2/utils.py (+122/-47)
juju/providers/local/__init__.py (+3/-2)
juju/providers/local/tests/test_provider.py (+7/-4)
juju/providers/maas/launch.py (+1/-1)
juju/providers/maas/maas.py (+5/-1)
juju/providers/maas/provider.py (+17/-7)
juju/providers/maas/tests/test_launch.py (+38/-5)
juju/providers/maas/tests/test_maas.py (+38/-1)
juju/providers/maas/tests/test_provider.py (+39/-1)
juju/providers/maas/tests/testing.py (+8/-1)
juju/providers/orchestra/__init__.py (+12/-6)
juju/providers/orchestra/cobbler.py (+17/-7)
juju/providers/orchestra/launch.py (+2/-1)
juju/providers/orchestra/tests/common.py (+7/-4)
juju/providers/orchestra/tests/data/bootstrap_user_data (+3/-2)
juju/providers/orchestra/tests/test_bootstrap.py (+23/-14)
juju/providers/orchestra/tests/test_cobbler.py (+22/-19)
juju/providers/orchestra/tests/test_launch.py (+29/-10)
juju/providers/orchestra/tests/test_provider.py (+32/-0)
juju/providers/tests/test_dummy.py (+3/-3)
juju/state/environment.py (+41/-18)
juju/state/initialize.py (+19/-24)
juju/state/machine.py (+9/-5)
juju/state/service.py (+55/-19)
juju/state/tests/common.py (+2/-2)
juju/state/tests/test_environment.py (+49/-44)
juju/state/tests/test_firewall.py (+5/-23)
juju/state/tests/test_initialize.py (+35/-16)
juju/state/tests/test_machine.py (+17/-4)
juju/state/tests/test_placement.py (+9/-10)
juju/state/tests/test_relation.py (+5/-8)
juju/state/tests/test_service.py (+65/-20)
juju/unit/tests/test_charm.py (+2/-26)
juju/unit/tests/test_deploy.py (+5/-20)
To merge this branch: bzr merge lp:~fwereade/pyjuju/shadow-trunk-1204
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+100195@code.launchpad.net

Description of the change

Constraints feature

End result of many branches merged into lp:~fwereade/juju/shadow-trunk-1204
over the last couple of weeks. Includes provider-specific constraint
registration (not global), provision for legacy deployments, and environment
constraints.

https://codereview.appspot.com/5971047/

To post a comment you must log in.
Revision history for this message
William Reade (fwereade) wrote :

Please take a look.

Revision history for this message
William Reade (fwereade) wrote :
Download full text (4.4 KiB)

Reviewers: mp+100195_code.launchpad.net,

Message:
Please take a look.

Description:
Constraints feature

End result of many branches merged into
lp:~fwereade/juju/shadow-trunk-1204
over the last couple of weeks. Includes provider-specific constraint
registration (not global), provision for legacy deployments, and
environment
constraints.

https://code.launchpad.net/~fwereade/juju/shadow-trunk-1204/+merge/100195

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/5971047/

Affected files:
   A [revision details]
   A docs/source/internals/constraints-notes.rst
   M juju/agents/provision.py
   M juju/agents/tests/common.py
   M juju/agents/tests/test_machine.py
   M juju/agents/tests/test_provision.py
   M juju/control/__init__.py
   M juju/control/add_unit.py
   M juju/control/bootstrap.py
   A juju/control/constraints_get.py
   M juju/control/constraints_set.py
   M juju/control/deploy.py
   M juju/control/initialize.py
   A juju/control/legacy.py
   M juju/control/terminate_machine.py
   M juju/control/tests/test_add_relation.py
   M juju/control/tests/test_add_unit.py
   M juju/control/tests/test_bootstrap.py
   M juju/control/tests/test_config_get.py
   M juju/control/tests/test_config_set.py
   A juju/control/tests/test_constraints_get.py
   M juju/control/tests/test_constraints_set.py
   M juju/control/tests/test_control.py
   M juju/control/tests/test_debug_hooks.py
   M juju/control/tests/test_debug_log.py
   M juju/control/tests/test_deploy.py
   M juju/control/tests/test_destroy_service.py
   M juju/control/tests/test_initialize.py
   M juju/control/tests/test_remove_unit.py
   M juju/control/tests/test_resolved.py
   M juju/control/tests/test_scp.py
   M juju/control/tests/test_ssh.py
   M juju/control/tests/test_status.py
   M juju/control/tests/test_terminate_machine.py
   M juju/control/tests/test_upgrade_charm.py
   M juju/control/tests/test_utils.py
   M juju/control/utils.py
   M juju/environment/config.py
   M juju/environment/tests/test_config.py
   M juju/hooks/tests/test_invoker.py
   M juju/machine/constraints.py
   M juju/machine/tests/test_constraints.py
   M juju/machine/unit.py
   M juju/providers/common/base.py
   M juju/providers/common/bootstrap.py
   M juju/providers/common/cloudinit.py
   M juju/providers/common/launch.py
   M juju/providers/common/tests/data/cloud_init_bootstrap
   M juju/providers/common/tests/data/cloud_init_bootstrap_zookeepers
   M juju/providers/common/tests/data/cloud_init_distro
   M juju/providers/common/tests/data/cloud_init_ppa
   M juju/providers/common/tests/test_base.py
   M juju/providers/common/tests/test_bootstrap.py
   M juju/providers/common/tests/test_cloudinit.py
   M juju/providers/common/tests/test_launch.py
   M juju/providers/dummy.py
   M juju/providers/ec2/__init__.py
   M juju/providers/ec2/launch.py
   M juju/providers/ec2/tests/common.py
   M juju/providers/ec2/tests/data/bootstrap_cloud_init
   M juju/providers/ec2/tests/test_bootstrap.py
   M juju/providers/ec2/tests/test_launch.py
   M juju/providers/ec2/tests/test_provider.py
   M juju/providers/ec2/tests/test_utils.py
   M juju/providers/ec2/utils.py
   M juju...

Read more...

Revision history for this message
William Reade (fwereade) wrote :

*** Submitted:

Constraints feature

End result of many branches merged into
lp:~fwereade/juju/shadow-trunk-1204
over the last couple of weeks. Includes provider-specific constraint
registration (not global), provision for legacy deployments, and
environment
constraints.

R=
CC=
https://codereview.appspot.com/5971047

https://codereview.appspot.com/5971047/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'docs/source/internals/constraints-notes.rst'
--- docs/source/internals/constraints-notes.rst 1970-01-01 00:00:00 +0000
+++ docs/source/internals/constraints-notes.rst 2012-03-30 16:56:32 +0000
@@ -0,0 +1,78 @@
1Notes on the current state of machine constraints
2=================================================
3
4Summary
5-------
6
7EC2 constraints and generic constraints can each be used to provision EC2 machines as intended. Orchestra support is not implemented, and should remain on hold until we get some clarity about MaaS. Environment constraints are not implemented; machine reuse is limited but predictable. Unit constraints, and a get-constraints command, should probably be added despite not being in the initial spec.
8
9We should prepare the ground for, and implement, environment constraints above all else. Adding a --constraints argument to add-unit will be very easy, and adding a get-constraints command won't be much harder, and the feature will feel much more complete with these capabilities, so they should come next. After that we should, with care and discretion, start tweaking the unit/machine matching algorithms such that we do the Right Thing more often without running the risk of doing a surprising and/or costly Wrong Thing.
10
11
12Code overview
13-------------
14
15`juju.machine.constraints` contains a `Constraints` type and a `_Constraint` type (which holds the configuration for a given constraint). The actual configuration is set up in `juju.machine.constraints_info` by means of calls to `register_constraint` and `register_conflicts`. Note that `ubuntu-series` and `provider-type` are themselves modelled as constraints (largely for ease/ consitency of representation) but are not exposed via `from_strs`, thereby preventing users from specifying things that should only be set by the code.
16
17The juju defaults are contained in the individual configured `_Constraint` objects; empty values are converted to defaults at `from_strs` time (when a provider type and a list of strings are used to construct a `Constraints` object). Therefore, the juju defaults are not themselves a separate `Constraints` object.
18
19Once created, `Constraints` objects store everything in their `_layer_data` dict, which contains keys for (1) every constraint specified explicitly and (2) every constraint conflicting with one specified explicitly (these are set to None). When storing constraints in ZK, all you need to do is serialise this dict (exposed as `layer_data`); `Constraints` objects can then be reconstructed directly from such dicts.
20
21This layer data does not necessarily include all possible constraints; so, when a `Constraints` is being used as a dict, we ensure we return default values for every key not in `_layer_data` (we also convert values from their textual representations to more-easily-consumable ones; so, eg, a mem of "4G" becomes 4096.0). However, we don't have any reason to store unset constraints at any stage; doing so would make it far harder to update one `Constraints` object with another. This is because `_layer_data` is effectively a mask: by updating A with B, the conflict Nones in B will overwrite any values set in A; while any keys not overridden will retain their values or conflict Nones, which ensures that we don't accidentally insert defaults due to missing values. Consider:
22
23A = "orchestra-name=jimbob" (masks arch, cpu, mem, orchestra-classes)
24B = "arch=arm orchestra-classes=ultralightweight" (masks orchestra-name)
25
26Now, if we A.update(B): orchestra-name gets masked out; cpu and mem remain masked out from before; the only remaining (user-visible) constraints are arch and orchestra-classes. Because the "ultralightweight" orchestra-class could easily be referring to a set of machines with very low cpu values, we *don't* want to reapply the default cpu constraint: we want to keep it masked as None, so we don't end up with a surprisingly unfulfillable request.
27
28It's a bit tricker on EC2: if arch overrides ec2-instance-type, this masks out cpu and mem as before and will therefore, in the absence of other constraints, deliver a t1.micro. IMO this is acceptable because: if someone's already thinking in instance-types, that's what they'll use in practice, so it's an edge case anyway; the results are actually not unreasonable; and the cost of incorrect behaviour is very low. The alternative -- of treating ec2-instance-type constraints as implicit arch/cpu/mem constraints, (so the update would end up with: a blank ec2-instance type; arch from the strings; and cpu/mem from the previous layer's ec2-instance-type) -- seems to me to be more likely to lead to surprising and potentially costly behaviour. This behaviour could be bolted on with little effort if deemed helpful, though.
29
30There are a couple of extra notes on EC2 in the source, both in `constraints_info` and in the ec2 provider code itself. (Note that ec2 constraints are defined in `constraints_info`, not the provider itself, because I want to be sure (without excessive magic or circular imports) that all constraints have been registered *before* any `Constraints` object is constructed, lest one be constructed with a incorrect mask. (It would be quick and easy to notify `_Constraint` once a `Constraints` is constructed, and refuse to register further constraints and conflicts, to ensure nobody does this accidentally; I only just thought if this though.) Most of the code for interpreting constraints is in the ec2 provider, though; this is because it only comes into play at provisioning time.
31
32
33Usage
34-----
35
36Actual use is pretty trivial, but involved an irritating weight of API changes: service states, unit states, and machine states all now require constraints, and it's tricky to figure out what the right "default" would be. (Well, we could grab provider-type from `GlobalSettingsStateManager` and construct it from an empty string list, but I have a disinclination to access such things without a very good reason; and since now, I think, all the "real" uses (as opposed to "test") can be expected to have constraints readily available, the inconvenience of changing all the tests doesn't seem like a valid argument against explicit specification. Does pollute the diffs a bit, though; sorry.)
37
38Basically, constraints are set on a service via `deploy` or `set-constraints`; then, when a unit is added, they're copied from the service to the unit (note: will be trivial to combine with env constraints and save the combined constraints... that is, when we actually have some env constraints); then, when a machine is added for a unit, the constraints are matched against existing machines' states, or used to construct a new one onto which the unit will be placed; and finally, those machine constraints are eventually passed into the provider by the provisioning agent.
39
40Bootstrap remains less than ideal -- but since we know the ubuntu-series and the provider-type, both on the client and on the bootstrap node, we can construct a full `Constraints` object and just use the default values for the other constraints; and, we can do so both when provisioning the initial machine (on the client) and when creating the bootstrap node's machine state (on the bootstrap node itself). When we have env constraints, we'll need to use them for provisioning *and* feed them through to the initialize command on the bootstrap node, so we can set up the correct machine state (in addition to the environment constraint state we'll need to set up).
41
42
43Merge pipeline
44--------------
45
46* placement-spec (Just the spec. Despite the MaaS-related inaccuracies, worth merging, IMO.)
47
48* constraint-types (Add `juju.machine.constraints` and `juju.machine.constraints_info`. Oh, and `juju.errors.ConstraintError`.)
49
50* set-service-constraints (Add `set-constraints` command; add param to `deploy` command; store constraints in service state.)
51
52* apply-machine-constraints (Constraints are set on the unit state by the service state, and on the machine state by (or from) the unit state; `initialize` command sets default constraints on machine/0.)
53
54* pa-start-machine-constraints (When launching machines, the provisioning agent passes "constraints" in machine_data, which are now actually used by the ec2 provider (I couldn't break this up further: once the constraints were passed in, ec2 had to change). Also removed "default-instance-type" and "default-ami" from ec2 environment config, and set bootstrap to provision a machine from juju defaults. Note: appears to actually work as expected.)
55
56
57Known problems
58--------------
59
60* Most horrifyingly: MaaS plans imply that we shouldn't depend on being able to specify `arch`, `cpu`, `mem`, or `orchestra-classes`, and I'm more than somewhat blindsided by this... certainly, with those (er) constraints, the eventual implementation cannot match the spec as currently written.
61
62* No environment constraints are implemented; it's not hard per se (add handling to `bootstrap` and `set-constraints`; store them somewhere sensible; get and combine them at unit-add time) but (as discussed) it would be a good idea to make the environment settings behaviour saner *before* we add constraints to the mix. Once that's done, we need to remember to actually use the information at bootstrap time, both for provisioning the bootstrap node and constructing its ZK state.
63
64 * I think it's fine for people to specify non-connection info inside their environments.yaml~s; but I think all such settings should be in a sub-dict called "initial" (or "bootstrap", or possibly even "default", as discussed at P-rally; I'm not sure which word carries the most appropriate semantic payload, but I'm not sure "default" is quite right).
65
66* Unused machines are not reused very sensibly: we just grab the first one we see that fulfils the relevant service unit's constraints. It would be trivial to build up a list of matching machines; it's not immediately clear just how we should go about picking the most appropriate machine from such a list.
67
68 * Well, ok, it's pretty clear -- we can "just" define a cost method on `Constraints`, and pick the cheapest. (But should we really reuse a cc1.4xlarge for a job that only demands an m1.small? Even if that's an easy question, what about orchestra? If we're interested in picking cheap machines -- and even assuming we can come up with a universally appropriate cost function -- we still need to consider the unused machines as well if we want to get a good answer.)
69
70 I remain convinced that it'll have to take the provider into consideration somehow -- and, tangentially, that we'll need to be a bit more sophisticated about matching `ec2-instance-type` constraints against generic constraints and vice versa -- and so I'm getting a little concerned that the "single type for Constraints" preference expressed by you and Gustavo is not going to be workable in the long term (but it's set up to require a provider-type to create a `Constraints` anyway, so it should be trivial to factoryise it by provider-type anyway).
71
72* We should probably bulk out machine constraints with actual data from the provider, once they've been provisioned (so we can tell, for example, what ec2-zone we ended up in (if it was left unspecified)). This affects how well we can match existing machines with new units; but what with MaaS, it remains an open question how much of this we'll even be able to do on orchestra(-without-landscape, anyway).
73
74 Note that this functionality -- and other upgrades in machine-reuse sophistication, like the previous bullet -- should IMO be added slowly and incrementally; I think we'll inevitably make mistakes here, at least until we've properly figured out the usage patterns, and I think for our users' sake it's more important that the rules be *clear* than that they be clever.
75
76* No `get-constraints` command exists. This command makes sense for all of env, service, unit, and machine; and on reflection, IMO, it should display the full constraints-as-dict in all cases (not just the layer data) so there's no chance of ambiguity; however, we should consider the implications of exporting explicit constraints as they were set, or as they "are" -- ie if we output mem as "4G" it will be irritating for machines but convenient for humans, while 4096.0 will be better suited for machine consumption (and also gives visibility into exactly what params will be passed to the provider itself). Not specced, but hard to justify leaving out.
77
78* No explicit unit constraints are implemented; the code as it stands would make it *very* easy to set them at add-unit time, and some constraints (well, orchestra-name in particular) only really make sense at the unit level. Nonetheless, it was deliberately left out of the spec, and can be worked around by users where necesssary, so it's not really a priority; but IMO the benefit outwieghs the very small cost of implementing it.
079
=== modified file 'juju/agents/provision.py'
--- juju/agents/provision.py 2012-02-01 15:39:08 +0000
+++ juju/agents/provision.py 2012-03-30 16:56:32 +0000
@@ -6,7 +6,7 @@
6from juju.environment.config import EnvironmentsConfig6from juju.environment.config import EnvironmentsConfig
7from juju.errors import ProviderError7from juju.errors import ProviderError
8from juju.lib.twistutils import concurrent_execution_guard8from juju.lib.twistutils import concurrent_execution_guard
9from juju.state.errors import MachineStateNotFound, StateChanged, StopWatcher9from juju.state.errors import MachineStateNotFound, StopWatcher
10from juju.state.firewall import FirewallManager10from juju.state.firewall import FirewallManager
11from juju.state.machine import MachineStateManager11from juju.state.machine import MachineStateManager
12from juju.state.service import ServiceStateManager12from juju.state.service import ServiceStateManager
@@ -223,8 +223,9 @@
223 # Verify a machine id has state and is running, else launch it.223 # Verify a machine id has state and is running, else launch it.
224 if instance_id is None or not instance_id in provider_machine_map:224 if instance_id is None or not instance_id in provider_machine_map:
225 log.info("Starting machine id:%s ...", machine_state.id)225 log.info("Starting machine id:%s ...", machine_state.id)
226 constraints = yield machine_state.get_constraints()
226 machines = yield self.provider.start_machine(227 machines = yield self.provider.start_machine(
227 {"machine-id": machine_state.id})228 {"machine-id": machine_state.id, "constraints": constraints})
228 instance_id = machines[0].instance_id229 instance_id = machines[0].instance_id
229 yield machine_state.set_instance_id(instance_id)230 yield machine_state.set_instance_id(instance_id)
230231
231232
=== modified file 'juju/agents/tests/common.py'
--- juju/agents/tests/common.py 2011-11-28 12:28:57 +0000
+++ juju/agents/tests/common.py 2012-03-30 16:56:32 +0000
@@ -5,30 +5,23 @@
5from txzookeeper.tests.utils import deleteTree5from txzookeeper.tests.utils import deleteTree
66
7from juju.agents.base import TwistedOptionNamespace7from juju.agents.base import TwistedOptionNamespace
8from juju.environment.config import EnvironmentsConfig
9from juju.state.tests.common import StateTestBase8from juju.state.tests.common import StateTestBase
10from juju.tests.common import get_test_zookeeper_address9from juju.tests.common import get_test_zookeeper_address
1110
1211
13SAMPLE_ENV = """\
14environments:
15 myfirstenv:
16 type: dummy
17 foo: bar
18 storage-directory: %s
19"""
20
21
22class AgentTestBase(StateTestBase):12class AgentTestBase(StateTestBase):
2313
24 agent_class = None14 agent_class = None
25 juju_directory = None15 juju_directory = None
16 setup_environment = True
2617
27 @inlineCallbacks18 @inlineCallbacks
28 def setUp(self):19 def setUp(self):
29 self.juju_directory = self.makeDir()20 self.juju_directory = self.makeDir()
30 yield super(AgentTestBase, self).setUp()21 yield super(AgentTestBase, self).setUp()
31 assert self.agent_class, "Agent Class must be specified on test"22 assert self.agent_class, "Agent Class must be specified on test"
23 if self.setup_environment:
24 yield self.push_default_config()
32 self.agent = self.agent_class()25 self.agent = self.agent_class()
33 self.options = yield self.get_agent_config()26 self.options = yield self.get_agent_config()
34 self.agent.configure(self.options)27 self.agent.configure(self.options)
@@ -42,15 +35,6 @@
42 deleteTree("/", self.client.handle)35 deleteTree("/", self.client.handle)
43 self.client.close()36 self.client.close()
4437
45 def get_test_environment_config(self):
46 sample_config = SAMPLE_ENV % self.makeDir()
47 config = EnvironmentsConfig()
48 config.parse(sample_config)
49 return config
50
51 def get_test_environment(self):
52 return self.get_test_environment_config().get_default()
53
54 def get_agent_config(self):38 def get_agent_config(self):
55 options = TwistedOptionNamespace()39 options = TwistedOptionNamespace()
56 options["juju_directory"] = self.juju_directory40 options["juju_directory"] = self.juju_directory
5741
=== modified file 'juju/agents/tests/test_machine.py'
--- juju/agents/tests/test_machine.py 2012-03-16 16:54:43 +0000
+++ juju/agents/tests/test_machine.py 2012-03-30 16:56:32 +0000
@@ -2,8 +2,7 @@
2import logging2import logging
3import os3import os
44
5from twisted.internet.defer import (5from twisted.internet.defer import inlineCallbacks, returnValue, fail, Deferred
6 inlineCallbacks, returnValue, succeed, fail, Deferred)
76
8from juju.agents.base import TwistedOptionNamespace7from juju.agents.base import TwistedOptionNamespace
9from juju.agents.machine import MachineAgent8from juju.agents.machine import MachineAgent
@@ -14,8 +13,8 @@
14from juju.charm.tests import local_charm_id13from juju.charm.tests import local_charm_id
15from juju.charm.tests.test_repository import RepositoryTestBase14from juju.charm.tests.test_repository import RepositoryTestBase
16from juju.lib.mocker import MATCH15from juju.lib.mocker import MATCH
17from juju.machine.constraints import Constraints16from juju.machine.tests.test_constraints import (
18from juju.state.environment import EnvironmentStateManager17 dummy_constraints, series_constraints)
19from juju.state.machine import MachineStateManager, MachineState18from juju.state.machine import MachineStateManager, MachineState
20from juju.state.service import ServiceStateManager19from juju.state.service import ServiceStateManager
21from juju.tests.common import get_test_zookeeper_address20from juju.tests.common import get_test_zookeeper_address
@@ -36,13 +35,7 @@
36 yield super(MachineAgentTest, self).setUp()35 yield super(MachineAgentTest, self).setUp()
3736
38 self.output = self.capture_logging(level=logging.DEBUG)37 self.output = self.capture_logging(level=logging.DEBUG)
3938 environment = self.config.get_default()
40 config = self.get_test_environment_config()
41 environment = config.get_default()
42
43 # Store the environment to zookeeper
44 environment_state_manager = EnvironmentStateManager(self.client)
45 yield environment_state_manager.set_config_state(config, "myfirstenv")
4639
47 # Load the environment with the charm state and charm binary40 # Load the environment with the charm state and charm binary
48 self.provider = environment.get_machine_provider()41 self.provider = environment.get_machine_provider()
@@ -58,8 +51,7 @@
58 # the machine.51 # the machine.
59 self.service_state_manager = ServiceStateManager(self.client)52 self.service_state_manager = ServiceStateManager(self.client)
60 self.service = yield self.service_state_manager.add_service_state(53 self.service = yield self.service_state_manager.add_service_state(
61 "fatality-blog", self.charm_state,54 "fatality-blog", self.charm_state, dummy_constraints)
62 Constraints.from_strs("dummy", []))
6355
64 @inlineCallbacks56 @inlineCallbacks
65 def get_agent_config(self):57 def get_agent_config(self):
@@ -68,7 +60,7 @@
68 machine_state_manager = MachineStateManager(self.client)60 machine_state_manager = MachineStateManager(self.client)
6961
70 self.machine_state = yield machine_state_manager.add_machine_state(62 self.machine_state = yield machine_state_manager.add_machine_state(
71 Constraints.from_strs("dummy", []).with_series("series"))63 series_constraints)
7264
73 self.change_environment(65 self.change_environment(
74 JUJU_MACHINE_ID="0",66 JUJU_MACHINE_ID="0",
7567
=== modified file 'juju/agents/tests/test_provision.py'
--- juju/agents/tests/test_provision.py 2012-01-09 16:57:13 +0000
+++ juju/agents/tests/test_provision.py 2012-03-30 16:56:32 +0000
@@ -1,18 +1,12 @@
1import logging1import logging
22
3import zookeeper
4
5from twisted.internet.defer import inlineCallbacks, fail, succeed3from twisted.internet.defer import inlineCallbacks, fail, succeed
6from twisted.internet import reactor4from twisted.internet import reactor
75
8from txzookeeper.client import ZOO_OPEN_ACL_UNSAFE
9
10from juju.agents.provision import ProvisioningAgent6from juju.agents.provision import ProvisioningAgent
11from juju.environment.environment import Environment7from juju.environment.environment import Environment
12from juju.environment.config import EnvironmentsConfig
13from juju.environment.errors import EnvironmentsConfigError8from juju.environment.errors import EnvironmentsConfigError
14from juju.environment.tests.test_config import SAMPLE_ENV9from juju.machine.tests.test_constraints import dummy_cs, series_constraints
15from juju.machine.constraints import Constraints
16from juju.errors import ProviderInteractionError10from juju.errors import ProviderInteractionError
17from juju.lib.mocker import MATCH11from juju.lib.mocker import MATCH
18from juju.providers.dummy import DummyMachine12from juju.providers.dummy import DummyMachine
@@ -37,23 +31,21 @@
37 yield super(ProvisioningTestBase, self).setUp()31 yield super(ProvisioningTestBase, self).setUp()
38 self.machine_manager = MachineStateManager(self.client)32 self.machine_manager = MachineStateManager(self.client)
3933
40 def add_machine_state(self):34 def add_machine_state(self, constraints=None):
41 return self.machine_manager.add_machine_state(35 return self.machine_manager.add_machine_state(
42 Constraints.from_strs("dummy", []).with_series("series"))36 constraints or series_constraints)
43
44 def get_serialized_environment(self):
45 config = EnvironmentsConfig()
46 config.parse(SAMPLE_ENV)
47 return config.serialize("myfirstenv")
4837
4938
50class ProvisioningAgentStartupTest(ProvisioningTestBase):39class ProvisioningAgentStartupTest(ProvisioningTestBase):
5140
41 setup_environment = False
42
52 @inlineCallbacks43 @inlineCallbacks
53 def setUp(self):44 def setUp(self):
54 yield super(ProvisioningAgentStartupTest, self).setUp()45 yield super(ProvisioningAgentStartupTest, self).setUp()
55 yield self.agent.connect()46 yield self.agent.connect()
5647
48 @inlineCallbacks
57 def test_agent_waits_for_environment(self):49 def test_agent_waits_for_environment(self):
58 """50 """
59 When the agent starts it waits for the /environment node to exist.51 When the agent starts it waits for the /environment node to exist.
@@ -61,31 +53,20 @@
61 deserialize it into an environment object.53 deserialize it into an environment object.
62 """54 """
63 env_loaded_deferred = self.agent.configure_environment()55 env_loaded_deferred = self.agent.configure_environment()
6456 reactor.callLater(
65 def verify_environment(result):57 0.3, self.push_default_config, with_constraints=False)
66 self.assertTrue(isinstance(result, Environment))58 result = yield env_loaded_deferred
67 self.assertEqual(result.name, "myfirstenv")59 self.assertTrue(isinstance(result, Environment))
6860 self.assertEqual(result.name, "firstenv")
69 env_loaded_deferred.addCallback(verify_environment)
70
71 def create_environment_node():
72 self.assertFalse(env_loaded_deferred.called)
73 return self.client.create(
74 "/environment", self.get_serialized_environment())
75
76 reactor.callLater(0.3, create_environment_node)
77 return env_loaded_deferred
7861
79 @inlineCallbacks62 @inlineCallbacks
80 def test_agent_with_existing_environment(self):63 def test_agent_with_existing_environment(self):
81 """An agent should load an existing environment to configure itself."""64 """An agent should load an existing environment to configure itself."""
8265 yield self.push_default_config()
83 yield self.client.create(
84 "/environment", self.get_serialized_environment())
8566
86 def verify_environment(result):67 def verify_environment(result):
87 self.assertTrue(isinstance(result, Environment))68 self.assertTrue(isinstance(result, Environment))
88 self.assertEqual(result.name, "myfirstenv")69 self.assertEqual(result.name, "firstenv")
8970
90 d = self.agent.configure_environment()71 d = self.agent.configure_environment()
91 d.addCallback(verify_environment)72 d.addCallback(verify_environment)
@@ -103,15 +84,13 @@
103 while the agent is processing the NoNodeException, it should detect84 while the agent is processing the NoNodeException, it should detect
104 this and configure normally.85 this and configure normally.
105 """86 """
106 data = self.get_serialized_environment()
107 exists_and_watch = self.agent.client.exists_and_watch87 exists_and_watch = self.agent.client.exists_and_watch
10888
109 mock_client = self.mocker.patch(self.agent.client)89 mock_client = self.mocker.patch(self.agent.client)
110 mock_client.exists_and_watch("/environment")90 mock_client.exists_and_watch("/environment")
11191
112 def inject_creation(path):92 def inject_creation(path):
113 zookeeper.create(93 self.push_default_config(with_constraints=False)
114 self.agent.client.handle, path, data, [ZOO_OPEN_ACL_UNSAFE])
115 return exists_and_watch(path)94 return exists_and_watch(path)
11695
117 self.mocker.call(inject_creation)96 self.mocker.call(inject_creation)
@@ -131,8 +110,6 @@
131 @inlineCallbacks110 @inlineCallbacks
132 def setUp(self):111 def setUp(self):
133 yield super(ProvisioningAgentTest, self).setUp()112 yield super(ProvisioningAgentTest, self).setUp()
134 yield self.client.create(
135 "/environment", self.get_serialized_environment())
136 self.agent.set_watch_enabled(False)113 self.agent.set_watch_enabled(False)
137 yield self.agent.startService()114 yield self.agent.startService()
138 self.output = self.capture_logging("juju.agents.provision",115 self.output = self.capture_logging("juju.agents.provision",
@@ -268,8 +245,7 @@
268 If the environment changes the agent reconfigures itself245 If the environment changes the agent reconfigures itself
269 """246 """
270 provider = self.agent.provider247 provider = self.agent.provider
271 data = self.get_serialized_environment()248 yield self.push_default_config()
272 yield self.client.set("/environment", data)
273 yield self.sleep(0.2)249 yield self.sleep(0.2)
274 self.assertNotIdentical(provider, self.agent.provider)250 self.assertNotIdentical(provider, self.agent.provider)
275251
@@ -314,14 +290,22 @@
314 If there's an error when processing changes, the agent should log290 If there's an error when processing changes, the agent should log
315 the error and continue.291 the error and continue.
316 """292 """
317 machine_state0 = yield self.add_machine_state()293 machine_state0 = yield self.add_machine_state(
318 machine_state1 = yield self.add_machine_state()294 dummy_cs.parse(["cpu=10"]).with_series("series"))
295 machine_state1 = yield self.add_machine_state(
296 dummy_cs.parse(["cpu=20"]).with_series("series"))
319297
320 mock_provider = self.mocker.patch(self.agent.provider)298 mock_provider = self.mocker.patch(self.agent.provider)
321 mock_provider.start_machine({"machine-id": 0})299 mock_provider.start_machine({
300 "machine-id": 0, "constraints": {
301 "arch": "amd64", "cpu": 10, "mem": 512,
302 "provider-type": "dummy", "ubuntu-series": "series"}})
322 self.mocker.result(fail(ProviderInteractionError()))303 self.mocker.result(fail(ProviderInteractionError()))
323304
324 mock_provider.start_machine({"machine-id": 1})305 mock_provider.start_machine({
306 "machine-id": 1, "constraints": {
307 "arch": "amd64", "cpu": 20, "mem": 512,
308 "provider-type": "dummy", "ubuntu-series": "series"}})
325 self.mocker.passthrough()309 self.mocker.passthrough()
326 self.mocker.replay()310 self.mocker.replay()
327311
@@ -476,8 +460,6 @@
476 @inlineCallbacks460 @inlineCallbacks
477 def setUp(self):461 def setUp(self):
478 yield super(FirewallManagerTest, self).setUp()462 yield super(FirewallManagerTest, self).setUp()
479 yield self.client.create(
480 "/environment", self.get_serialized_environment())
481 self.agent.set_watch_enabled(False)463 self.agent.set_watch_enabled(False)
482 yield self.agent.startService()464 yield self.agent.startService()
483465
@@ -509,11 +491,14 @@
509 # Modify services, while subsequently poking to ensure service491 # Modify services, while subsequently poking to ensure service
510 # watch is processed on each modification492 # watch is processed on each modification
511 yield self.add_service("wordpress")493 yield self.add_service("wordpress")
512 yield self.poke_zk()494 while len(seen) < 1:
495 yield self.poke_zk()
513 mysql = yield self.add_service("mysql")496 mysql = yield self.add_service("mysql")
514 yield self.poke_zk()497 while len(seen) < 2:
498 yield self.poke_zk()
515 yield self.service_state_manager.remove_service_state(mysql)499 yield self.service_state_manager.remove_service_state(mysql)
516 yield self.poke_zk()500 while len(seen) < 3:
501 yield self.poke_zk()
517502
518 self.assertEqual(503 self.assertEqual(
519 seen,504 seen,
520505
=== modified file 'juju/control/__init__.py'
--- juju/control/__init__.py 2011-12-15 17:12:26 +0000
+++ juju/control/__init__.py 2012-03-30 16:56:32 +0000
@@ -12,6 +12,7 @@
12import bootstrap12import bootstrap
13import config_get13import config_get
14import config_set14import config_set
15import constraints_get
15import constraints_set16import constraints_set
16import debug_hooks17import debug_hooks
17import debug_log18import debug_log
@@ -39,6 +40,7 @@
39 bootstrap,40 bootstrap,
40 config_get,41 config_get,
41 config_set,42 config_set,
43 constraints_get,
42 constraints_set,44 constraints_set,
43 debug_log,45 debug_log,
44 debug_hooks,46 debug_hooks,
4547
=== modified file 'juju/control/add_unit.py'
--- juju/control/add_unit.py 2011-11-29 22:50:38 +0000
+++ juju/control/add_unit.py 2012-03-30 16:56:32 +0000
@@ -2,7 +2,8 @@
22
3from twisted.internet.defer import inlineCallbacks3from twisted.internet.defer import inlineCallbacks
44
5from juju.control.utils import get_environment5from juju.control import legacy
6from juju.control.utils import get_environment, sync_environment_state
6from juju.state.placement import place_unit7from juju.state.placement import place_unit
7from juju.state.service import ServiceStateManager8from juju.state.service import ServiceStateManager
89
@@ -44,6 +45,10 @@
44 client = yield provider.connect()45 client = yield provider.connect()
4546
46 try:47 try:
48 yield legacy.check_environment(
49 client, provider.get_legacy_config_keys())
50 yield sync_environment_state(client, config, environment.name)
51
47 service_manager = ServiceStateManager(client)52 service_manager = ServiceStateManager(client)
48 service_state = yield service_manager.get_service_state(service_name)53 service_state = yield service_manager.get_service_state(service_name)
49 for i in range(num_units):54 for i in range(num_units):
5055
=== modified file 'juju/control/bootstrap.py'
--- juju/control/bootstrap.py 2011-09-22 13:23:00 +0000
+++ juju/control/bootstrap.py 2012-03-30 16:56:32 +0000
@@ -1,6 +1,7 @@
1from twisted.internet.defer import inlineCallbacks1from twisted.internet.defer import inlineCallbacks
22
3from juju.control.utils import get_environment3from juju.control import legacy
4from juju.control.utils import expand_constraints, get_environment
45
56
6def configure_subparser(subparsers):7def configure_subparser(subparsers):
@@ -9,6 +10,11 @@
9 sub_parser.add_argument(10 sub_parser.add_argument(
10 "--environment", "-e",11 "--environment", "-e",
11 help="juju environment to operate in.")12 help="juju environment to operate in.")
13 sub_parser.add_argument(
14 "--constraints",
15 help="default hardware constraints for this environment.",
16 default=[],
17 type=expand_constraints)
12 return sub_parser18 return sub_parser
1319
1420
@@ -19,6 +25,14 @@
19 """25 """
20 environment = get_environment(options)26 environment = get_environment(options)
21 provider = environment.get_machine_provider()27 provider = environment.get_machine_provider()
28 legacy_keys = provider.get_legacy_config_keys()
29 if legacy_keys:
30 legacy.error(legacy_keys)
31
32 constraint_set = yield provider.get_constraint_set()
33 constraints = constraint_set.parse(options.constraints)
34 constraints = constraints.with_series(environment.default_series)
35
22 options.log.info("Bootstrapping environment %r (type: %s)..." % (36 options.log.info("Bootstrapping environment %r (type: %s)..." % (
23 environment.name, environment.type))37 environment.name, environment.type))
24 yield provider.bootstrap()38 yield provider.bootstrap(constraints)
2539
=== added file 'juju/control/constraints_get.py'
--- juju/control/constraints_get.py 1970-01-01 00:00:00 +0000
+++ juju/control/constraints_get.py 2012-03-30 16:56:32 +0000
@@ -0,0 +1,75 @@
1import argparse
2import sys
3import yaml
4
5from twisted.internet.defer import inlineCallbacks
6
7from juju.control.utils import get_environment, sync_environment_state
8from juju.state.environment import EnvironmentStateManager
9from juju.state.machine import MachineStateManager
10from juju.state.service import ServiceStateManager
11
12
13def configure_subparser(subparsers):
14 sub_parser = subparsers.add_parser(
15 "get-constraints",
16 help=command.__doc__,
17 formatter_class=argparse.RawDescriptionHelpFormatter,
18 description=constraints_get.__doc__)
19
20 sub_parser.add_argument(
21 "--environment", "-e",
22 help="Environment to affect")
23
24 sub_parser.add_argument(
25 "entities",
26 nargs="*",
27 help="names of machines, units or services")
28
29 return sub_parser
30
31
32def command(options):
33 """Show currently applicable constraints"""
34 environment = get_environment(options)
35 return constraints_get(
36 options.environments, environment, options.entities, options.log)
37
38
39@inlineCallbacks
40def constraints_get(env_config, environment, entity_names, log):
41 """
42 Show the complete set of applicable constraints for each specified entity.
43
44 This will show the final computed values of all constraints (including
45 internal constraints which cannot be set directly via set-constraints).
46 """
47 provider = environment.get_machine_provider()
48 client = yield provider.connect()
49 result = {}
50 try:
51 yield sync_environment_state(client, env_config, environment.name)
52 if entity_names:
53 msm = MachineStateManager(client)
54 ssm = ServiceStateManager(client)
55 for name in entity_names:
56 if name.isdigit():
57 kind = "machine"
58 entity = yield msm.get_machine_state(name)
59 elif "/" in name:
60 kind = "service unit"
61 entity = yield ssm.get_unit_state(name)
62 else:
63 kind = "service"
64 entity = yield ssm.get_service_state(name)
65 log.info("Fetching constraints for %s %s", kind, name)
66 constraints = yield entity.get_constraints()
67 result[name] = dict(constraints)
68 else:
69 esm = EnvironmentStateManager(client)
70 log.info("Fetching constraints for environment")
71 constraints = yield esm.get_constraints()
72 result = dict(constraints)
73 yaml.safe_dump(result, sys.stdout)
74 finally:
75 yield client.close()
076
=== modified file 'juju/control/constraints_set.py'
--- juju/control/constraints_set.py 2012-03-09 09:02:06 +0000
+++ juju/control/constraints_set.py 2012-03-30 16:56:32 +0000
@@ -2,8 +2,9 @@
22
3from twisted.internet.defer import inlineCallbacks3from twisted.internet.defer import inlineCallbacks
44
5from juju.control.utils import get_environment5from juju.control import legacy
6from juju.machine.constraints import Constraints6from juju.control.utils import get_environment, sync_environment_state
7from juju.state.environment import EnvironmentStateManager
7from juju.state.service import ServiceStateManager8from juju.state.service import ServiceStateManager
89
910
@@ -34,36 +35,43 @@
34 """Set machine constraints for the environment, or for a named service.35 """Set machine constraints for the environment, or for a named service.
35 """36 """
36 environment = get_environment(options)37 environment = get_environment(options)
38 env_config = options.environments
37 return constraints_set(39 return constraints_set(
38 environment, options.service, options.constraints)40 env_config, environment, options.service, options.constraints)
3941
4042
41@inlineCallbacks43@inlineCallbacks
42def constraints_set(environment, service_name, constraint_strs):44def constraints_set(env_config, environment, service_name, constraint_strs):
43 """45 """
44 Machine constraints allow you to pick the hardware to which your services46 Machine constraints allow you to pick the hardware to which your services
45 will be deployed. Examples:47 will be deployed. Examples:
4648
47 $ juju set-constraints --service-name mysql mem=8G cpu=449 $ juju set-constraints --service-name mysql mem=8G cpu=4
4850
49 $ juju set-constraints ec2-instance-type=t1.micro51 $ juju set-constraints instance-type=t1.micro
5052
51 "arch", "cpu" and "mem" are always available; other constraints are53 Available constraints vary by provider type, and will be ignored if not
52 provider-specific, and will be ignored if specified in an environment of54 understood by the current environment's provider. The current set of
53 the wrong kind. The recognised constraints are currently:55 available constraints across all providers is:
5456
55 * arch (CPU architecture: x86/amd64/arm; unset by default)57 On Amazon EC2:
56 * cpu (processing power in Amazon ECU; 1 by default)58
57 * mem (memory in [MGT]iB; 512M by default)59 * arch (CPU architecture: i386/amd64/arm; amd64 by default)
58 * ec2-region (us-east-1 by default)60 * cpu (processing power in Amazon ECU; 1 by default)
59 * ec2-zone (unset by default)61 * mem (memory in [MGT]iB; 512M by default)
60 * ec2-instance-type (unset by default)62 * instance-type (unset by default)
61 * orchestra-classes (unset by default)63 * ec2-zone (unset by default)
62 * orchestra-name (unset by default)64
65 On Orchestra:
66
67 * orchestra-classes (unset by default)
68
69 On MAAS:
70
71 * maas-name (unset by default)
6372
64 Service settings, if specified, will override environment settings, which73 Service settings, if specified, will override environment settings, which
65 will in turn override the juju defaults of mem=512M, cpu=1,74 will in turn override the juju defaults of mem=512M, cpu=1, arch=amd64.
66 ec2-region=us-east-1.
6775
68 New constraints set on an entity will completely replace that entity's76 New constraints set on an entity will completely replace that entity's
69 pre-existing constraints.77 pre-existing constraints.
@@ -72,17 +80,22 @@
72 service constraints, just specify "name=" (rather than just not specifying80 service constraints, just specify "name=" (rather than just not specifying
73 the constraint at all, which will cause it to inherit the environment's81 the constraint at all, which will cause it to inherit the environment's
74 value).82 value).
83
84 To entirely unset a constraint, specify "name=any".
75 """85 """
76 constraints = Constraints.from_strs(environment.type, constraint_strs)
77
78 if service_name is None:
79 raise NotImplementedError("Environment constraints not implemented")
80
81 provider = environment.get_machine_provider()86 provider = environment.get_machine_provider()
87 constraint_set = yield provider.get_constraint_set()
88 constraints = constraint_set.parse(constraint_strs)
82 client = yield provider.connect()89 client = yield provider.connect()
83 try:90 try:
84 service_state_manager = ServiceStateManager(client)91 yield legacy.check_constraints(client, constraint_strs)
85 service = yield service_state_manager.get_service_state(service_name)92 yield sync_environment_state(client, env_config, environment.name)
86 yield service.set_constraints(constraints)93 if service_name is None:
94 esm = EnvironmentStateManager(client)
95 yield esm.set_constraints(constraints)
96 else:
97 ssm = ServiceStateManager(client)
98 service = yield ssm.get_service_state(service_name)
99 yield service.set_constraints(constraints)
87 finally:100 finally:
88 yield client.close()101 yield client.close()
89102
=== modified file 'juju/control/deploy.py'
--- juju/control/deploy.py 2012-03-22 16:15:02 +0000
+++ juju/control/deploy.py 2012-03-30 16:56:32 +0000
@@ -4,26 +4,20 @@
44
5from twisted.internet.defer import inlineCallbacks5from twisted.internet.defer import inlineCallbacks
66
7from juju.control.utils import get_environment, expand_path7from juju.control import legacy
8from juju.control.utils import (
9 expand_constraints, expand_path, get_environment, sync_environment_state)
810
9from juju.charm.errors import ServiceConfigValueError11from juju.charm.errors import ServiceConfigValueError
10from juju.charm.publisher import CharmPublisher12from juju.charm.publisher import CharmPublisher
11from juju.charm.repository import resolve13from juju.charm.repository import resolve
12from juju.errors import CharmError14from juju.errors import CharmError
13from juju.machine.constraints import Constraints
14from juju.state.endpoint import RelationEndpoint15from juju.state.endpoint import RelationEndpoint
15from juju.state.environment import EnvironmentStateManager
16from juju.state.placement import place_unit16from juju.state.placement import place_unit
17from juju.state.relation import RelationStateManager17from juju.state.relation import RelationStateManager
18from juju.state.service import ServiceStateManager18from juju.state.service import ServiceStateManager
1919
2020
21def _expand_constraints(s):
22 if s:
23 return s.split(" ")
24 return []
25
26
27def configure_subparser(subparsers):21def configure_subparser(subparsers):
28 sub_parser = subparsers.add_parser("deploy", help=command.__doc__,22 sub_parser = subparsers.add_parser("deploy", help=command.__doc__,
29 description=deploy.__doc__)23 description=deploy.__doc__)
@@ -50,7 +44,7 @@
50 "--constraints",44 "--constraints",
51 help="Hardware constraints for the service",45 help="Hardware constraints for the service",
52 default=[],46 default=[],
53 type=_expand_constraints)47 type=expand_constraints)
5448
55 sub_parser.add_argument(49 sub_parser.add_argument(
56 "charm", nargs=None,50 "charm", nargs=None,
@@ -134,23 +128,23 @@
134 service_options = parse_config_options(128 service_options = parse_config_options(
135 config_file, service_name, charm)129 config_file, service_name, charm)
136130
137 constraints = Constraints.from_strs(environment.type, constraint_strs)
138
139 charm = yield repo.find(charm_url)131 charm = yield repo.find(charm_url)
140 charm_id = str(charm_url.with_revision(charm.get_revision()))132 charm_id = str(charm_url.with_revision(charm.get_revision()))
141133
142 provider = environment.get_machine_provider()134 provider = environment.get_machine_provider()
143 placement_policy = provider.get_placement_policy()135 placement_policy = provider.get_placement_policy()
136 constraint_set = yield provider.get_constraint_set()
137 constraints = constraint_set.parse(constraint_strs)
144 client = yield provider.connect()138 client = yield provider.connect()
145139
146 try:140 try:
141 yield legacy.check_constraints(client, constraint_strs)
142 yield legacy.check_environment(
143 client, provider.get_legacy_config_keys())
144 yield sync_environment_state(client, env_config, environment.name)
145
146 # Publish the charm to juju
147 storage = yield provider.get_file_storage()147 storage = yield provider.get_file_storage()
148 service_manager = ServiceStateManager(client)
149 environment_state_manager = EnvironmentStateManager(client)
150 yield environment_state_manager.set_config_state(
151 env_config, environment.name)
152
153 # Publish the charm to juju
154 publisher = CharmPublisher(client, storage)148 publisher = CharmPublisher(client, storage)
155 yield publisher.add_charm(charm_id, charm)149 yield publisher.add_charm(charm_id, charm)
156 result = yield publisher.publish()150 result = yield publisher.publish()
@@ -161,6 +155,7 @@
161 charm_state = result[0]155 charm_state = result[0]
162156
163 # Create the service state157 # Create the service state
158 service_manager = ServiceStateManager(client)
164 service_state = yield service_manager.add_service_state(159 service_state = yield service_manager.add_service_state(
165 service_name, charm_state, constraints)160 service_name, charm_state, constraints)
166161
167162
=== modified file 'juju/control/initialize.py'
--- juju/control/initialize.py 2011-09-23 20:35:02 +0000
+++ juju/control/initialize.py 2012-03-30 16:56:32 +0000
@@ -1,4 +1,6 @@
1from base64 import b64decode
1import os2import os
3import yaml
24
3from twisted.internet.defer import inlineCallbacks5from twisted.internet.defer import inlineCallbacks
46
@@ -12,11 +14,13 @@
12 sub_parser.add_argument(14 sub_parser.add_argument(
13 "--instance-id", required=True,15 "--instance-id", required=True,
14 help="Provider instance id for the bootstrap node")16 help="Provider instance id for the bootstrap node")
15
16 sub_parser.add_argument(17 sub_parser.add_argument(
17 "--admin-identity", required=True,18 "--admin-identity", required=True,
18 help="Admin access control identity for zookeeper ACLs")19 help="Admin access control identity for zookeeper ACLs")
19 sub_parser.add_argument(20 sub_parser.add_argument(
21 "--constraints-data", required=True,
22 help="Base64-encoded yaml dump of the environment constraints data")
23 sub_parser.add_argument(
20 "--provider-type", required=True,24 "--provider-type", required=True,
21 help="Environment machine provider type")25 help="Environment machine provider type")
22 return sub_parser26 return sub_parser
@@ -30,10 +34,12 @@
30 zk_address = os.environ.get("ZOOKEEPER_ADDRESS", "127.0.0.1:2181")34 zk_address = os.environ.get("ZOOKEEPER_ADDRESS", "127.0.0.1:2181")
31 client = yield ZookeeperClient(zk_address).connect()35 client = yield ZookeeperClient(zk_address).connect()
32 try:36 try:
37 constraints_data = yaml.load(b64decode(options.constraints_data))
33 hierarchy = StateHierarchy(38 hierarchy = StateHierarchy(
34 client,39 client,
35 options.admin_identity,40 options.admin_identity,
36 options.instance_id,41 options.instance_id,
42 constraints_data,
37 options.provider_type)43 options.provider_type)
38 yield hierarchy.initialize()44 yield hierarchy.initialize()
39 finally:45 finally:
4046
=== added file 'juju/control/legacy.py'
--- juju/control/legacy.py 1970-01-01 00:00:00 +0000
+++ juju/control/legacy.py 2012-03-30 16:56:32 +0000
@@ -0,0 +1,41 @@
1from twisted.internet.defer import inlineCallbacks
2
3from juju.errors import JujuError
4from juju.state.environment import EnvironmentStateManager
5
6_ERROR = """
7Your environments.yaml contains deprecated keys; they must not be used other
8than in legacy deployments. The affected keys are:
9
10 %s
11
12This error can be resolved according to the instructions available at:
13
14 https://juju.ubuntu.com/DeprecatedEnvironmentSettings
15"""
16
17
18def error(keys):
19 raise JujuError(_ERROR % "\n ".join(sorted(keys)))
20
21
22@inlineCallbacks
23def check_environment(client, keys):
24 if not keys:
25 return
26 esm = EnvironmentStateManager(client)
27 if not (yield esm.get_in_legacy_environment()):
28 error(keys)
29
30
31@inlineCallbacks
32def check_constraints(client, constraint_strs):
33 if not constraint_strs:
34 return
35 esm = EnvironmentStateManager(client)
36 if (yield esm.get_in_legacy_environment()):
37 raise JujuError(
38 "Constraints are not valid in legacy deployments. To use machine "
39 "constraints, please deploy your environment again from scratch. "
40 "You can continue to use this environment as before, but any "
41 "attempt to set constraints will fail.")
042
=== modified file 'juju/control/terminate_machine.py'
--- juju/control/terminate_machine.py 2011-09-15 18:50:23 +0000
+++ juju/control/terminate_machine.py 2012-03-30 16:56:32 +0000
@@ -2,6 +2,7 @@
22
3from twisted.internet.defer import inlineCallbacks3from twisted.internet.defer import inlineCallbacks
44
5from juju.control.utils import sync_environment_state
5from juju.errors import CannotTerminateMachine6from juju.errors import CannotTerminateMachine
6from juju.environment.errors import EnvironmentsConfigError7from juju.environment.errors import EnvironmentsConfigError
7from juju.state.errors import MachineStateNotFound8from juju.state.errors import MachineStateNotFound
@@ -57,6 +58,7 @@
57 client = yield provider.connect()58 client = yield provider.connect()
58 terminated_machine_ids = []59 terminated_machine_ids = []
59 try:60 try:
61 yield sync_environment_state(client, config, environment.name)
60 machine_state_manager = MachineStateManager(client)62 machine_state_manager = MachineStateManager(client)
61 for machine_id in machine_ids:63 for machine_id in machine_ids:
62 if machine_id == 0:64 if machine_id == 0:
6365
=== modified file 'juju/control/tests/test_add_relation.py'
--- juju/control/tests/test_add_relation.py 2012-02-09 03:52:45 +0000
+++ juju/control/tests/test_add_relation.py 2012-03-30 16:56:32 +0000
@@ -1,5 +1,4 @@
1import logging1import logging
2import yaml
32
4from twisted.internet.defer import inlineCallbacks3from twisted.internet.defer import inlineCallbacks
54
@@ -15,12 +14,6 @@
15 @inlineCallbacks14 @inlineCallbacks
16 def setUp(self):15 def setUp(self):
17 yield super(ControlAddRelationTest, self).setUp()16 yield super(ControlAddRelationTest, self).setUp()
18 config = {
19 "environments": {
20 "firstenv": {
21 "type": "dummy", "admin-secret": "homer"}}}
22 self.write_config(yaml.dump(config))
23 self.config.load()
24 self.output = self.capture_logging()17 self.output = self.capture_logging()
25 self.stderr = self.capture_stream("stderr")18 self.stderr = self.capture_stream("stderr")
2619
2720
=== modified file 'juju/control/tests/test_add_unit.py'
--- juju/control/tests/test_add_unit.py 2012-02-01 11:27:42 +0000
+++ juju/control/tests/test_add_unit.py 2012-03-30 16:56:32 +0000
@@ -1,7 +1,9 @@
1from yaml import dump
2
1from twisted.internet.defer import inlineCallbacks3from twisted.internet.defer import inlineCallbacks
2from yaml import dump
34
4from juju.control import main5from juju.control import main
6from juju.state.environment import EnvironmentStateManager
57
6from .common import MachineControlToolTest8from .common import MachineControlToolTest
79
@@ -11,11 +13,6 @@
11 @inlineCallbacks13 @inlineCallbacks
12 def setUp(self):14 def setUp(self):
13 yield super(ControlAddUnitTest, self).setUp()15 yield super(ControlAddUnitTest, self).setUp()
14 config = {
15 "environments": {"firstenv": {"type": "dummy"}}}
16
17 self.write_config(dump(config))
18 self.config.load()
19 self.service_state1 = yield self.add_service_from_charm("mysql")16 self.service_state1 = yield self.add_service_from_charm("mysql")
20 self.service_unit1 = yield self.service_state1.add_unit_state()17 self.service_unit1 = yield self.service_state1.add_unit_state()
21 self.machine_state1 = yield self.add_machine_state()18 self.machine_state1 = yield self.add_machine_state()
@@ -36,9 +33,15 @@
36 finished = self.setup_cli_reactor()33 finished = self.setup_cli_reactor()
37 self.setup_exit(0)34 self.setup_exit(0)
38 self.mocker.replay()35 self.mocker.replay()
36 # trash environment to check syncing
37 yield self.client.delete("/environment")
39 main(["add-unit", "mysql"])38 main(["add-unit", "mysql"])
40 yield finished39 yield finished
4140
41 # verify the env state was synced
42 esm = EnvironmentStateManager(self.client)
43 yield esm.get_config()
44
42 # verify the unit and its machine assignment.45 # verify the unit and its machine assignment.
43 unit_names = yield self.service_state1.get_unit_names()46 unit_names = yield self.service_state1.get_unit_names()
44 self.assertEqual(len(unit_names), 2)47 self.assertEqual(len(unit_names), 2)
@@ -121,9 +124,7 @@
121 "environments": {"firstenv": {124 "environments": {"firstenv": {
122 "placement": "local",125 "placement": "local",
123 "type": "dummy"}}}126 "type": "dummy"}}}
124127 yield self.push_config("firstenv", config)
125 self.write_config(dump(config))
126 self.config.load()
127128
128 ms0 = yield self.machine_state_manager.get_machine_state(0)129 ms0 = yield self.machine_state_manager.get_machine_state(0)
129 yield self.service_unit1.unassign_from_machine()130 yield self.service_unit1.unassign_from_machine()
@@ -149,3 +150,37 @@
149 self.output.getvalue())150 self.output.getvalue())
150 # adding a second unit still assigns to machine 0 with local policy151 # adding a second unit still assigns to machine 0 with local policy
151 yield self.assert_machine_assignments("mysql", [0, 0])152 yield self.assert_machine_assignments("mysql", [0, 0])
153
154 @inlineCallbacks
155 def test_legacy_option_in_legacy_env(self):
156 yield self.client.delete("/constraints")
157
158 finished = self.setup_cli_reactor()
159 self.setup_exit(0)
160 self.mocker.replay()
161 main(["add-unit", "mysql"])
162 yield finished
163
164 unit_names = yield self.service_state1.get_unit_names()
165 self.assertEqual(len(unit_names), 2)
166
167 @inlineCallbacks
168 def test_legacy_option_in_fresh_env(self):
169 local_config = {
170 "environments": {"firstenv": {
171 "some-legacy-key": "blah",
172 "type": "dummy"}}}
173 self.write_config(dump(local_config))
174 self.config.load()
175
176 finished = self.setup_cli_reactor()
177 self.setup_exit(0)
178 self.mocker.replay()
179 main(["add-unit", "mysql"])
180 yield finished
181
182 output = self.output.getvalue()
183 self.assertIn(
184 "Your environments.yaml contains deprecated keys", output)
185 unit_names = yield self.service_state1.get_unit_names()
186 self.assertEqual(len(unit_names), 1)
152187
=== modified file 'juju/control/tests/test_bootstrap.py'
--- juju/control/tests/test_bootstrap.py 2011-09-22 13:23:00 +0000
+++ juju/control/tests/test_bootstrap.py 2012-03-30 16:56:32 +0000
@@ -19,32 +19,29 @@
19 config = {19 config = {
20 "environments": {20 "environments": {
21 "firstenv": {21 "firstenv": {
22 "type": "dummy", "admin-secret": "homer"},22 "type": "dummy", "default-series": "homer"},
23 "secondenv": {23 "secondenv": {
24 "type": "dummy", "admin-secret": "marge"}}}24 "type": "dummy", "default-series": "marge"}}}
2525
26 self.write_config(dump(config))26 self.write_config(dump(config))
27 finished = self.setup_cli_reactor()27 finished = self.setup_cli_reactor()
28 self.setup_exit(0)28 self.setup_exit(0)
2929
30 envs = set(("firstenv", "secondenv"))
31
32 def track_bootstrap_call(self):
33 envs.remove(self.environment_name)
34 return succeed(True)
35
36 provider = self.mocker.patch(MachineProvider)30 provider = self.mocker.patch(MachineProvider)
3731 provider.bootstrap({
38 provider.bootstrap()32 "ubuntu-series": "homer",
39 self.mocker.call(track_bootstrap_call, with_object=True)33 "provider-type": "dummy",
34 "arch": "arm",
35 "cpu": 2.0,
36 "mem": 512.0})
37 self.mocker.result(succeed(True))
40 self.mocker.replay()38 self.mocker.replay()
4139
42 self.capture_stream("stderr")40 self.capture_stream("stderr")
43 main(["bootstrap", "-e", "firstenv"])41 main(["bootstrap", "-e", "firstenv",
42 "--constraints", "arch=arm cpu=2"])
44 yield finished43 yield finished
4544
46 self.assertEqual(envs, set(["secondenv"]))
47
48 lines = filter(None, self.log.getvalue().split("\n"))45 lines = filter(None, self.log.getvalue().split("\n"))
49 self.assertEqual(46 self.assertEqual(
50 lines,47 lines,
@@ -96,3 +93,25 @@
96 msg = "Invalid environment 'thirdenv'"93 msg = "Invalid environment 'thirdenv'"
97 self.assertIn(msg, self.log.getvalue())94 self.assertIn(msg, self.log.getvalue())
98 self.assertIn(msg, output.getvalue())95 self.assertIn(msg, output.getvalue())
96
97 @inlineCallbacks
98 def test_bootstrap_legacy_config_keys(self):
99 """
100 If the environment specified does not exist an error message is given.
101 """
102 config = {
103 "environments": {
104 "firstenv": {
105 "type": "dummy", "some-legacy-key": "blah"}}}
106 self.write_config(dump(config))
107 finished = self.setup_cli_reactor()
108 self.setup_exit(1)
109 self.mocker.replay()
110
111 output = self.capture_stream("stderr")
112 main(["bootstrap"])
113 yield finished
114
115 msg = "Your environments.yaml contains deprecated keys"
116 self.assertIn(msg, self.log.getvalue())
117 self.assertIn(msg, output.getvalue())
99118
=== modified file 'juju/control/tests/test_config_get.py'
--- juju/control/tests/test_config_get.py 2012-01-30 22:20:37 +0000
+++ juju/control/tests/test_config_get.py 2012-03-30 16:56:32 +0000
@@ -13,10 +13,6 @@
13 @inlineCallbacks13 @inlineCallbacks
14 def setUp(self):14 def setUp(self):
15 yield super(ControlJujuGetTest, self).setUp()15 yield super(ControlJujuGetTest, self).setUp()
16 config = {
17 "environments": {"firstenv": {"type": "dummy"}}}
18 self.write_config(yaml.dump(config))
19 self.config.load()
20 self.stderr = self.capture_stream("stderr")16 self.stderr = self.capture_stream("stderr")
2117
22 @inlineCallbacks18 @inlineCallbacks
2319
=== modified file 'juju/control/tests/test_config_set.py'
--- juju/control/tests/test_config_set.py 2012-03-09 13:24:58 +0000
+++ juju/control/tests/test_config_set.py 2012-03-30 16:56:32 +0000
@@ -12,10 +12,6 @@
12 @inlineCallbacks12 @inlineCallbacks
13 def setUp(self):13 def setUp(self):
14 yield super(ControlJujuSetTest, self).setUp()14 yield super(ControlJujuSetTest, self).setUp()
15 config = {
16 "environments": {"firstenv": {"type": "dummy"}}}
17 self.write_config(dump(config))
18 self.config.load()
19 self.service_state = yield self.add_service_from_charm("wordpress")15 self.service_state = yield self.add_service_from_charm("wordpress")
20 self.service_unit = yield self.service_state.add_unit_state()16 self.service_unit = yield self.service_state.add_unit_state()
21 self.environment = self.config.get_default()17 self.environment = self.config.get_default()
2218
=== added file 'juju/control/tests/test_constraints_get.py'
--- juju/control/tests/test_constraints_get.py 1970-01-01 00:00:00 +0000
+++ juju/control/tests/test_constraints_get.py 2012-03-30 16:56:32 +0000
@@ -0,0 +1,116 @@
1import yaml
2
3from twisted.internet.defer import inlineCallbacks
4
5from juju.control import main
6from juju.machine.tests.test_constraints import dummy_cs
7from juju.state.environment import EnvironmentStateManager
8
9from .common import MachineControlToolTest
10
11env_log = "Fetching constraints for environment"
12machine_log = "Fetching constraints for machine 1"
13service_log = "Fetching constraints for service mysql"
14unit_log = "Fetching constraints for service unit mysql/0"
15
16
17class ConstraintsGetTest(MachineControlToolTest):
18
19 @inlineCallbacks
20 def setUp(self):
21 yield super(ConstraintsGetTest, self).setUp()
22 env_constraints = dummy_cs.parse(["mem=1024"])
23 esm = EnvironmentStateManager(self.client)
24 yield esm.set_constraints(env_constraints)
25 self.expect_env = {
26 "arch": "amd64", "cpu": 1.0, "mem": 1024.0,
27 "provider-type": "dummy", "ubuntu-series": None}
28
29 service_constraints = dummy_cs.parse(["cpu=10"])
30 service = yield self.add_service_from_charm(
31 "mysql", constraints=service_constraints)
32 # unit will snapshot the state of service when added
33 unit = yield service.add_unit_state()
34 self.expect_unit = {
35 "arch": "amd64", "cpu": 10.0, "mem": 1024.0,
36 "provider-type": "dummy", "ubuntu-series": "series"}
37
38 # machine gets its own constraints
39 machine_constraints = dummy_cs.parse(["cpu=15", "mem=8G"])
40 machine = yield self.add_machine_state(
41 constraints=machine_constraints.with_series("series"))
42 self.expect_machine = {
43 "arch": "amd64", "cpu": 15.0, "mem": 8192.0,
44 "provider-type": "dummy", "ubuntu-series": "series"}
45 yield unit.assign_to_machine(machine)
46
47 # service gets new constraints, leaves unit untouched
48 yield service.set_constraints(dummy_cs.parse(["mem=16G"]))
49 self.expect_service = {
50 "arch": "amd64", "cpu": 1.0, "mem": 16384.0,
51 "provider-type": "dummy", "ubuntu-series": "series"}
52
53 self.log = self.capture_logging()
54 self.stdout = self.capture_stream("stdout")
55 self.finished = self.setup_cli_reactor()
56 self.setup_exit(0)
57 self.mocker.replay()
58
59 def assert_messages(self, *messages):
60 log = self.log.getvalue()
61 for message in messages:
62 self.assertIn(message, log)
63
64 @inlineCallbacks
65 def test_env(self):
66 main(["get-constraints"])
67 yield self.finished
68 result = yaml.load(self.stdout.getvalue())
69 self.assertEquals(result, self.expect_env)
70 self.assert_messages(env_log)
71
72 @inlineCallbacks
73 def test_service(self):
74 main(["get-constraints", "mysql"])
75 yield self.finished
76 result = yaml.load(self.stdout.getvalue())
77 self.assertEquals(result, {"mysql": self.expect_service})
78 self.assert_messages(service_log)
79
80 @inlineCallbacks
81 def test_unit(self):
82 main(["get-constraints", "mysql/0"])
83 yield self.finished
84 result = yaml.load(self.stdout.getvalue())
85 self.assertEquals(result, {"mysql/0": self.expect_unit})
86 self.assert_messages(unit_log)
87
88 @inlineCallbacks
89 def test_machine(self):
90 main(["get-constraints", "1"])
91 yield self.finished
92 result = yaml.load(self.stdout.getvalue())
93 self.assertEquals(result, {"1": self.expect_machine})
94 self.assert_messages(machine_log)
95
96 @inlineCallbacks
97 def test_all(self):
98 main(["get-constraints", "mysql", "mysql/0", "1"])
99 yield self.finished
100 result = yaml.load(self.stdout.getvalue())
101 expect = {"mysql": self.expect_service,
102 "mysql/0": self.expect_unit,
103 "1": self.expect_machine}
104 self.assertEquals(result, expect)
105 self.assert_messages(service_log, unit_log, machine_log)
106
107 @inlineCallbacks
108 def test_syncs_environment(self):
109 """If the environment were not synced, it would be impossible to create
110 the Constraints, so tool success proves sync."""
111 yield self.client.delete("/environment")
112 main(["get-constraints", "mysql/0"])
113 yield self.finished
114 result = yaml.load(self.stdout.getvalue())
115 self.assertEquals(result, {"mysql/0": self.expect_unit})
116 self.assert_messages(unit_log)
0117
=== modified file 'juju/control/tests/test_constraints_set.py'
--- juju/control/tests/test_constraints_set.py 2012-03-09 09:02:06 +0000
+++ juju/control/tests/test_constraints_set.py 2012-03-30 16:56:32 +0000
@@ -1,8 +1,7 @@
1from yaml import dump
2
3from twisted.internet.defer import inlineCallbacks1from twisted.internet.defer import inlineCallbacks
42
5from juju.control import main3from juju.control import main
4from juju.state.environment import EnvironmentStateManager
65
7from .common import MachineControlToolTest6from .common import MachineControlToolTest
87
@@ -12,11 +11,6 @@
12 @inlineCallbacks11 @inlineCallbacks
13 def setUp(self):12 def setUp(self):
14 yield super(ControlSetConstraintsTest, self).setUp()13 yield super(ControlSetConstraintsTest, self).setUp()
15 config = {
16 "environments": {"firstenv": {"type": "dummy"}}}
17
18 self.write_config(dump(config))
19 self.config.load()
20 self.service_state = yield self.add_service_from_charm("mysql")14 self.service_state = yield self.add_service_from_charm("mysql")
21 self.output = self.capture_logging()15 self.output = self.capture_logging()
22 self.stderr = self.capture_stream("stderr")16 self.stderr = self.capture_stream("stderr")
@@ -31,7 +25,7 @@
3125
32 constraints = yield self.service_state.get_constraints()26 constraints = yield self.service_state.get_constraints()
33 expect = {27 expect = {
34 "arch": None, "cpu": 8, "mem": 1024,28 "arch": "amd64", "cpu": 8, "mem": 1024,
35 "provider-type": "dummy", "ubuntu-series": "series"}29 "provider-type": "dummy", "ubuntu-series": "series"}
36 self.assertEquals(constraints, expect)30 self.assertEquals(constraints, expect)
3731
@@ -53,15 +47,32 @@
5347
54 @inlineCallbacks48 @inlineCallbacks
55 def test_environment_constraint(self):49 def test_environment_constraint(self):
56 initial_constraints = yield self.service_state.get_constraints()50 yield self.client.delete("/environment")
57 finished = self.setup_cli_reactor()51 finished = self.setup_cli_reactor()
58 self.setup_exit(1)52 self.setup_exit(0)
59 self.mocker.replay()53 self.mocker.replay()
60 main(["set-constraints", "arch=arm"])54 main(["set-constraints", "arch=arm", "cpu=any"])
55 yield finished
56
57 esm = EnvironmentStateManager(self.client)
58 yield esm.get_config()
59 constraints = yield esm.get_constraints()
60 self.assertEquals(constraints, {
61 "ubuntu-series": None,
62 "provider-type": "dummy",
63 "arch": "arm",
64 "cpu": None,
65 "mem": 512.0})
66
67 @inlineCallbacks
68 def test_legacy_environment(self):
69 yield self.client.delete("/constraints")
70 finished = self.setup_cli_reactor()
71 self.setup_exit(0)
72 self.mocker.replay()
73 main(["set-constraints", "arch=arm", "cpu=any"])
61 yield finished74 yield finished
6275
63 self.assertIn(76 self.assertIn(
64 "Environment constraints not implemented", self.stderr.getvalue())77 "Constraints are not valid in legacy deployments.",
6578 self.stderr.getvalue())
66 constraints = yield self.service_state.get_constraints()
67 self.assertEquals(constraints, initial_constraints)
6879
=== modified file 'juju/control/tests/test_control.py'
--- juju/control/tests/test_control.py 2011-09-15 19:24:47 +0000
+++ juju/control/tests/test_control.py 2012-03-30 16:56:32 +0000
@@ -4,7 +4,6 @@
44
5from StringIO import StringIO5from StringIO import StringIO
6from argparse import Namespace6from argparse import Namespace
7from yaml import dump
87
9from twisted.internet.defer import inlineCallbacks8from twisted.internet.defer import inlineCallbacks
109
@@ -12,6 +11,7 @@
12from juju.control import setup_logging, main, setup_parser11from juju.control import setup_logging, main, setup_parser
13from juju.control.options import ensure_abs_path12from juju.control.options import ensure_abs_path
14from juju.control.command import Commander13from juju.control.command import Commander
14from juju.state.tests.common import StateTestBase
1515
16from juju.lib.testing import TestCase16from juju.lib.testing import TestCase
1717
@@ -36,17 +36,12 @@
36 self.fail("EnvironmentsConfigError not raised")36 self.fail("EnvironmentsConfigError not raised")
3737
3838
39class ControlOutputTest(ControlToolTest):39class ControlOutputTest(ControlToolTest, StateTestBase):
4040
41 @inlineCallbacks
41 def setUp(self):42 def setUp(self):
42 super(ControlOutputTest, self).setUp()43 yield super(ControlOutputTest, self).setUp()
43 config = {44 yield self.push_default_config()
44 "environments": {
45 "firstenv": {
46 "type": "dummy", "admin-secret": "homer"},
47 "secondenv": {
48 "type": "dummy", "admin-secret": "marge"}}}
49 self.write_config(dump(config))
5045
51 def test_sans_args_produces_help(self):46 def test_sans_args_produces_help(self):
52 """47 """
5348
=== modified file 'juju/control/tests/test_debug_hooks.py'
--- juju/control/tests/test_debug_hooks.py 2012-02-01 11:27:42 +0000
+++ juju/control/tests/test_debug_hooks.py 2012-03-30 16:56:32 +0000
@@ -1,8 +1,6 @@
1import logging1import logging
2import os2import os
33
4import yaml
5
6from twisted.internet.defer import (4from twisted.internet.defer import (
7 inlineCallbacks, returnValue, succeed, Deferred)5 inlineCallbacks, returnValue, succeed, Deferred)
86
@@ -22,13 +20,6 @@
22 @inlineCallbacks20 @inlineCallbacks
23 def setUp(self):21 def setUp(self):
24 yield super(ControlDebugHookTest, self).setUp()22 yield super(ControlDebugHookTest, self).setUp()
25 config = {
26 "environments": {
27 "firstenv": {
28 "type": "dummy", "admin-secret": "homer"}}}
29 self.write_config(yaml.dump(config))
30 self.config.load()
31
32 self.environment = self.config.get_default()23 self.environment = self.config.get_default()
33 self.provider = self.environment.get_machine_provider()24 self.provider = self.environment.get_machine_provider()
3425
3526
=== modified file 'juju/control/tests/test_debug_log.py'
--- juju/control/tests/test_debug_log.py 2011-09-15 19:24:47 +0000
+++ juju/control/tests/test_debug_log.py 2012-03-30 16:56:32 +0000
@@ -1,5 +1,4 @@
1import json1import json
2import yaml
32
4from twisted.internet.defer import inlineCallbacks3from twisted.internet.defer import inlineCallbacks
54
@@ -13,12 +12,7 @@
13 @inlineCallbacks12 @inlineCallbacks
14 def setUp(self):13 def setUp(self):
15 yield super(ControlDebugLogTest, self).setUp()14 yield super(ControlDebugLogTest, self).setUp()
16 config = {15 yield self.push_default_config()
17 "environments": {
18 "firstenv": {
19 "type": "dummy", "admin-secret": "homer"}}}
20 self.write_config(yaml.dump(config))
21 self.config.load()
2216
23 @inlineCallbacks17 @inlineCallbacks
24 def test_replay(self):18 def test_replay(self):
2519
=== modified file 'juju/control/tests/test_deploy.py'
--- juju/control/tests/test_deploy.py 2012-03-09 20:13:36 +0000
+++ juju/control/tests/test_deploy.py 2012-03-30 16:56:32 +0000
@@ -32,7 +32,6 @@
32 @inlineCallbacks32 @inlineCallbacks
33 def setUp(self):33 def setUp(self):
34 yield super(ControlDeployTest, self).setUp()34 yield super(ControlDeployTest, self).setUp()
35
36 config = {35 config = {
37 "environments": {36 "environments": {
38 "firstenv": {37 "firstenv": {
@@ -40,8 +39,7 @@
40 "admin-secret": "homer",39 "admin-secret": "homer",
41 "placement": "unassigned",40 "placement": "unassigned",
42 "default-series": "series"}}}41 "default-series": "series"}}}
43 self.write_config(yaml.dump(config))42 yield self.push_config("firstenv", config)
44 self.config.load()
4543
46 def test_deploy_multiple_environments_none_specified(self):44 def test_deploy_multiple_environments_none_specified(self):
47 """45 """
@@ -228,7 +226,7 @@
228 self.assertEquals(charm_id, "local:series/sample-2")226 self.assertEquals(charm_id, "local:series/sample-2")
229 constraints = yield service_state.get_constraints()227 constraints = yield service_state.get_constraints()
230 expect_constraints = {228 expect_constraints = {
231 "arch": None, "cpu": 123, "mem": 512,229 "arch": "amd64", "cpu": 123, "mem": 512,
232 "provider-type": "dummy", "ubuntu-series": "series"}230 "provider-type": "dummy", "ubuntu-series": "series"}
233 self.assertEquals(constraints, expect_constraints)231 self.assertEquals(constraints, expect_constraints)
234232
@@ -490,9 +488,7 @@
490 "placement": "local",488 "placement": "local",
491 "type": "dummy",489 "type": "dummy",
492 "default-series": "series"}}}490 "default-series": "series"}}}
493491 yield self.push_config("firstenv", config)
494 self.write_config(yaml.dump(config))
495 self.config.load()
496492
497 finished = self.setup_cli_reactor()493 finished = self.setup_cli_reactor()
498 self.setup_exit(0)494 self.setup_exit(0)
@@ -508,3 +504,66 @@
508 unit = units[0]504 unit = units[0]
509 machine_id = yield unit.get_assigned_machine_id()505 machine_id = yield unit.get_assigned_machine_id()
510 self.assertEqual(machine_id, 0)506 self.assertEqual(machine_id, 0)
507
508 @inlineCallbacks
509 def test_deploy_legacy_keys_in_legacy_env(self):
510 yield self.client.delete("/constraints")
511
512 finished = self.setup_cli_reactor()
513 self.setup_exit(0)
514 self.mocker.replay()
515
516 main(["deploy", "--repository", self.unbundled_repo_path,
517 "local:sample", "beekeeper"])
518 yield finished
519
520 service_manager = ServiceStateManager(self.client)
521 yield service_manager.get_service_state("beekeeper")
522
523 @inlineCallbacks
524 def test_deploy_legacy_keys_in_fresh_env(self):
525 yield self.push_default_config()
526 local_config = {
527 "environments": {"firstenv": {
528 "type": "dummy",
529 "some-legacy-key": "blah",
530 "default-series": "series"}}}
531 self.write_config(yaml.dump(local_config))
532 self.config.load()
533 finished = self.setup_cli_reactor()
534 self.setup_exit(0)
535 self.mocker.replay()
536 stderr = self.capture_stream("stderr")
537
538 main(["deploy", "--repository", self.unbundled_repo_path,
539 "local:sample", "beekeeper"])
540 yield finished
541
542 self.assertIn(
543 "Your environments.yaml contains deprecated keys",
544 stderr.getvalue())
545 service_manager = ServiceStateManager(self.client)
546 yield self.assertFailure(
547 service_manager.get_service_state("beekeeper"),
548 ServiceStateNotFound)
549
550 @inlineCallbacks
551 def test_deploy_constraints_in_legacy_env(self):
552 yield self.client.delete("/constraints")
553
554 finished = self.setup_cli_reactor()
555 self.setup_exit(0)
556 self.mocker.replay()
557 stderr = self.capture_stream("stderr")
558
559 main(["deploy", "--repository", self.unbundled_repo_path,
560 "local:sample", "beekeeper", "--constraints", "arch=i386"])
561 yield finished
562
563 self.assertIn(
564 "Constraints are not valid in legacy deployments.",
565 stderr.getvalue())
566 service_manager = ServiceStateManager(self.client)
567 yield self.assertFailure(
568 service_manager.get_service_state("beekeeper"),
569 ServiceStateNotFound)
511570
=== modified file 'juju/control/tests/test_destroy_service.py'
--- juju/control/tests/test_destroy_service.py 2011-09-15 19:24:47 +0000
+++ juju/control/tests/test_destroy_service.py 2012-03-30 16:56:32 +0000
@@ -1,5 +1,4 @@
1from twisted.internet.defer import inlineCallbacks1from twisted.internet.defer import inlineCallbacks
2from yaml import dump
32
4from juju.control import main3from juju.control import main
54
@@ -15,11 +14,7 @@
15 @inlineCallbacks14 @inlineCallbacks
16 def setUp(self):15 def setUp(self):
17 yield super(ControlStopServiceTest, self).setUp()16 yield super(ControlStopServiceTest, self).setUp()
18 config = {
19 "environments": {"firstenv": {"type": "dummy"}}}
2017
21 self.write_config(dump(config))
22 self.config.load()
23 self.service_state1 = yield self.add_service_from_charm("mysql")18 self.service_state1 = yield self.add_service_from_charm("mysql")
24 self.service1_unit = yield self.service_state1.add_unit_state()19 self.service1_unit = yield self.service_state1.add_unit_state()
25 self.service_state2 = yield self.add_service_from_charm("wordpress")20 self.service_state2 = yield self.add_service_from_charm("wordpress")
2621
=== modified file 'juju/control/tests/test_initialize.py'
--- juju/control/tests/test_initialize.py 2011-09-23 20:35:02 +0000
+++ juju/control/tests/test_initialize.py 2012-03-30 16:56:32 +0000
@@ -1,3 +1,6 @@
1from base64 import b64encode
2from yaml import safe_dump
3
1from twisted.internet.defer import succeed4from twisted.internet.defer import succeed
25
3from txzookeeper import ZookeeperClient6from txzookeeper import ZookeeperClient
@@ -26,7 +29,27 @@
26 self.setup_exit(0)29 self.setup_exit(0)
27 self.mocker.replay()30 self.mocker.replay()
2831
29 admin(["initialize",32 constraints_data = b64encode(safe_dump({
30 "--instance-id", "foobar",33 "ubuntu-series": "foo", "provider-type": "bar"}))
31 "--admin-identity", "admin:genie",34
35 admin(["initialize",
36 "--instance-id", "foobar",
37 "--admin-identity", "admin:genie",
38 "--constraints-data", constraints_data,
39 "--provider-type", "dummy"])
40
41 def test_bad_constraints_data(self):
42 """Test that failing to unpack --constraints-data aborts initialize"""
43 client = self.mocker.patch(ZookeeperClient)
44 self.setup_cli_reactor()
45 client.connect()
46 self.mocker.result(succeed(client))
47 self.capture_stream('stderr')
48 self.setup_exit(1)
49 self.mocker.replay()
50
51 admin(["initialize",
52 "--instance-id", "foobar",
53 "--admin-identity", "admin:genie",
54 "--constraints-data", "zaphod's just this guy, you know?",
32 "--provider-type", "dummy"])55 "--provider-type", "dummy"])
3356
=== modified file 'juju/control/tests/test_remove_unit.py'
--- juju/control/tests/test_remove_unit.py 2012-02-01 11:27:42 +0000
+++ juju/control/tests/test_remove_unit.py 2012-03-30 16:56:32 +0000
@@ -2,7 +2,6 @@
2import sys2import sys
33
4from twisted.internet.defer import inlineCallbacks4from twisted.internet.defer import inlineCallbacks
5from yaml import dump
6import zookeeper5import zookeeper
76
8from .common import ControlToolTest7from .common import ControlToolTest
@@ -18,11 +17,6 @@
18 @inlineCallbacks17 @inlineCallbacks
19 def setUp(self):18 def setUp(self):
20 yield super(ControlRemoveUnitTest, self).setUp()19 yield super(ControlRemoveUnitTest, self).setUp()
21 config = {
22 "environments": {"firstenv": {"type": "dummy"}}}
23
24 self.write_config(dump(config))
25 self.config.load()
2620
27 self.environment = self.config.get_default()21 self.environment = self.config.get_default()
28 self.provider = self.environment.get_machine_provider()22 self.provider = self.environment.get_machine_provider()
2923
=== modified file 'juju/control/tests/test_resolved.py'
--- juju/control/tests/test_resolved.py 2012-03-27 22:06:24 +0000
+++ juju/control/tests/test_resolved.py 2012-03-30 16:56:32 +0000
@@ -1,5 +1,4 @@
1from twisted.internet.defer import inlineCallbacks, returnValue1from twisted.internet.defer import inlineCallbacks, returnValue
2from yaml import dump
32
4from juju.control import main3from juju.control import main
5from juju.control.tests.common import ControlToolTest4from juju.control.tests.common import ControlToolTest
@@ -20,11 +19,6 @@
20 @inlineCallbacks19 @inlineCallbacks
21 def setUp(self):20 def setUp(self):
22 yield super(ControlResolvedTest, self).setUp()21 yield super(ControlResolvedTest, self).setUp()
23 config = {
24 "environments": {"firstenv": {"type": "dummy"}}}
25
26 self.write_config(dump(config))
27 self.config.load()
2822
29 yield self.add_relation_state("wordpress", "mysql")23 yield self.add_relation_state("wordpress", "mysql")
30 yield self.add_relation_state("wordpress", "varnish")24 yield self.add_relation_state("wordpress", "varnish")
3125
=== modified file 'juju/control/tests/test_scp.py'
--- juju/control/tests/test_scp.py 2012-02-01 11:27:42 +0000
+++ juju/control/tests/test_scp.py 2012-03-30 16:56:32 +0000
@@ -19,14 +19,7 @@
19 @inlineCallbacks19 @inlineCallbacks
20 def setUp(self):20 def setUp(self):
21 yield super(SCPTest, self).setUp()21 yield super(SCPTest, self).setUp()
22 config = {
23 "environments": {
24 "firstenv": {
25 "type": "dummy", "admin-secret": "homer"}}}
26 self.write_config(dump(config))
27 self.setup_exit(0)22 self.setup_exit(0)
28
29 self.config.load()
30 self.environment = self.config.get_default()23 self.environment = self.config.get_default()
31 self.provider = self.environment.get_machine_provider()24 self.provider = self.environment.get_machine_provider()
3225
@@ -53,10 +46,6 @@
53 @inlineCallbacks46 @inlineCallbacks
54 def test_scp_unit_name(self):47 def test_scp_unit_name(self):
55 """Verify scp command is invoked against the host for a unit name."""48 """Verify scp command is invoked against the host for a unit name."""
56 mock_environment = self.mocker.patch(Environment)
57 mock_environment.get_machine_provider()
58 self.mocker.result(self.provider)
59
60 # Verify expected call against scp49 # Verify expected call against scp
61 mock_exec = self.mocker.replace(os.execvp)50 mock_exec = self.mocker.replace(os.execvp)
62 mock_exec("scp", [51 mock_exec("scp", [
@@ -79,6 +68,8 @@
79 @inlineCallbacks68 @inlineCallbacks
80 def test_scp_machine_id(self):69 def test_scp_machine_id(self):
81 """Verify scp command is invoked against the host for a machine ID."""70 """Verify scp command is invoked against the host for a machine ID."""
71 # We need to do this because separate instances of DummyProvider don't
72 # share instance state.
82 mock_environment = self.mocker.patch(Environment)73 mock_environment = self.mocker.patch(Environment)
83 mock_environment.get_machine_provider()74 mock_environment.get_machine_provider()
84 self.mocker.result(self.provider)75 self.mocker.result(self.provider)
@@ -111,10 +102,6 @@
111102
112 $ juju scp -o "ConnectTimeout 60" foo mysql/0:/foo/bar103 $ juju scp -o "ConnectTimeout 60" foo mysql/0:/foo/bar
113 """104 """
114 mock_environment = self.mocker.patch(Environment)
115 mock_environment.get_machine_provider()
116 self.mocker.result(self.provider)
117
118 # Verify expected call against scp105 # Verify expected call against scp
119 mock_exec = self.mocker.replace(os.execvp)106 mock_exec = self.mocker.replace(os.execvp)
120 mock_exec("scp", [107 mock_exec("scp", [
@@ -141,11 +128,6 @@
141 @inlineCallbacks128 @inlineCallbacks
142 def setUp(self):129 def setUp(self):
143 yield super(ParseErrorsTest, self).setUp()130 yield super(ParseErrorsTest, self).setUp()
144 config = {
145 "environments": {"firstenv": {"type": "dummy"}}}
146
147 self.write_config(dump(config))
148 self.config.load()
149 self.stderr = self.capture_stream("stderr")131 self.stderr = self.capture_stream("stderr")
150132
151 def test_passthrough_args_parse_error(self):133 def test_passthrough_args_parse_error(self):
152134
=== modified file 'juju/control/tests/test_ssh.py'
--- juju/control/tests/test_ssh.py 2012-03-23 02:13:50 +0000
+++ juju/control/tests/test_ssh.py 2012-03-30 16:56:32 +0000
@@ -23,14 +23,7 @@
23 @inlineCallbacks23 @inlineCallbacks
24 def setUp(self):24 def setUp(self):
25 yield super(ControlShellTest, self).setUp()25 yield super(ControlShellTest, self).setUp()
26 config = {
27 "environments": {
28 "firstenv": {
29 "type": "dummy", "admin-secret": "homer"}}}
30 self.write_config(dump(config))
31 self.setup_exit(0)26 self.setup_exit(0)
32
33 self.config.load()
34 self.environment = self.config.get_default()27 self.environment = self.config.get_default()
35 self.provider = self.environment.get_machine_provider()28 self.provider = self.environment.get_machine_provider()
3629
@@ -346,10 +339,6 @@
346 def test_shell_with_unassigned_unit(self):339 def test_shell_with_unassigned_unit(self):
347 """If the service unit is not assigned, attempting to340 """If the service unit is not assigned, attempting to
348 connect, raises an error."""341 connect, raises an error."""
349 mock_environment = self.mocker.patch(Environment)
350 mock_environment.get_machine_provider()
351 self.mocker.result(self.provider)
352
353 finished = self.setup_cli_reactor()342 finished = self.setup_cli_reactor()
354 self.mocker.replay()343 self.mocker.replay()
355344
@@ -384,11 +373,6 @@
384 @inlineCallbacks373 @inlineCallbacks
385 def setUp(self):374 def setUp(self):
386 yield super(ParseErrorsTest, self).setUp()375 yield super(ParseErrorsTest, self).setUp()
387 config = {
388 "environments": {"firstenv": {"type": "dummy"}}}
389
390 self.write_config(dump(config))
391 self.config.load()
392 self.stderr = self.capture_stream("stderr")376 self.stderr = self.capture_stream("stderr")
393377
394 def test_passthrough_args_parse_error(self):378 def test_passthrough_args_parse_error(self):
395379
=== modified file 'juju/control/tests/test_status.py'
--- juju/control/tests/test_status.py 2012-03-29 02:50:29 +0000
+++ juju/control/tests/test_status.py 2012-03-30 16:56:32 +0000
@@ -46,14 +46,6 @@
46 yield settings.set_provider_type("dummy")46 yield settings.set_provider_type("dummy")
47 self.log = self.capture_logging()47 self.log = self.capture_logging()
4848
49 config = {
50 "environments": {
51 "firstenv": {
52 "type": "dummy",
53 "admin-secret": "homer"}}}
54 self.write_config(yaml.dump(config))
55 self.config.load()
56
57 self.environment = self.config.get_default()49 self.environment = self.config.get_default()
58 self.provider = self.environment.get_machine_provider()50 self.provider = self.environment.get_machine_provider()
59 self.machine_count = 051 self.machine_count = 0
6052
=== modified file 'juju/control/tests/test_terminate_machine.py'
--- juju/control/tests/test_terminate_machine.py 2012-01-09 16:57:13 +0000
+++ juju/control/tests/test_terminate_machine.py 2012-03-30 16:56:32 +0000
@@ -1,5 +1,4 @@
1import logging1import logging
2import yaml
32
4from twisted.internet.defer import inlineCallbacks3from twisted.internet.defer import inlineCallbacks
54
@@ -7,6 +6,7 @@
7from juju.control.tests.common import MachineControlToolTest6from juju.control.tests.common import MachineControlToolTest
8from juju.errors import CannotTerminateMachine7from juju.errors import CannotTerminateMachine
9from juju.state.errors import MachineStateInUse, MachineStateNotFound8from juju.state.errors import MachineStateInUse, MachineStateNotFound
9from juju.state.environment import EnvironmentStateManager
1010
1111
12class ControlTerminateMachineTest(MachineControlToolTest):12class ControlTerminateMachineTest(MachineControlToolTest):
@@ -14,12 +14,6 @@
14 @inlineCallbacks14 @inlineCallbacks
15 def setUp(self):15 def setUp(self):
16 yield super(ControlTerminateMachineTest, self).setUp()16 yield super(ControlTerminateMachineTest, self).setUp()
17 config = {
18 "environments": {
19 "firstenv": {
20 "type": "dummy", "admin-secret": "homer"}}}
21 self.write_config(yaml.dump(config))
22 self.config.load()
23 self.output = self.capture_logging()17 self.output = self.capture_logging()
24 self.stderr = self.capture_stream("stderr")18 self.stderr = self.capture_stream("stderr")
2519
@@ -113,8 +107,16 @@
113 yield wordpress_unit_state.unassign_from_machine()107 yield wordpress_unit_state.unassign_from_machine()
114 yield mysql_unit_state.unassign_from_machine()108 yield mysql_unit_state.unassign_from_machine()
115 yield self.assert_machine_states([0, 1, 2, 3], [])109 yield self.assert_machine_states([0, 1, 2, 3], [])
110
111 # trash environment to check syncing
112 yield self.client.delete("/environment")
116 main(["terminate-machine", "1", "3"])113 main(["terminate-machine", "1", "3"])
117 yield wait_on_reactor_stopped114 yield wait_on_reactor_stopped
115
116 # check environment synced
117 esm = EnvironmentStateManager(self.client)
118 yield esm.get_config()
119
118 self.assertIn(120 self.assertIn(
119 "Machines terminated: 1, 3", self.output.getvalue())121 "Machines terminated: 1, 3", self.output.getvalue())
120 yield self.assert_machine_states([0, 2], [1, 3])122 yield self.assert_machine_states([0, 2], [1, 3])
121123
=== modified file 'juju/control/tests/test_upgrade_charm.py'
--- juju/control/tests/test_upgrade_charm.py 2012-03-26 17:22:23 +0000
+++ juju/control/tests/test_upgrade_charm.py 2012-03-30 16:56:32 +0000
@@ -56,11 +56,7 @@
56 @inlineCallbacks56 @inlineCallbacks
57 def setUp(self):57 def setUp(self):
58 yield super(ControlCharmUpgradeTest, self).setUp()58 yield super(ControlCharmUpgradeTest, self).setUp()
59 config = {
60 "environments": {"firstenv": {"type": "dummy"}}}
6159
62 self.write_config(dump(config))
63 self.config.load()
64 self.service_state1 = yield self.add_service_from_charm("mysql")60 self.service_state1 = yield self.add_service_from_charm("mysql")
65 self.service_unit1 = yield self.service_state1.add_unit_state()61 self.service_unit1 = yield self.service_state1.add_unit_state()
6662
@@ -459,12 +455,6 @@
459 @inlineCallbacks455 @inlineCallbacks
460 def setUp(self):456 def setUp(self):
461 yield super(RemoteUpgradeCharmTest, self).setUp()457 yield super(RemoteUpgradeCharmTest, self).setUp()
462 config = {
463 "environments": {"firstenv": {"type": "dummy"}}}
464
465 self.write_config(dump(config))
466 self.config.load()
467
468 charm = CharmDirectory(os.path.join(458 charm = CharmDirectory(os.path.join(
469 test_repository_path, "series", "mysql"))459 test_repository_path, "series", "mysql"))
470 self.charm_state_manager.add_charm_state(460 self.charm_state_manager.add_charm_state(
471461
=== modified file 'juju/control/tests/test_utils.py'
--- juju/control/tests/test_utils.py 2012-03-09 13:24:58 +0000
+++ juju/control/tests/test_utils.py 2012-03-30 16:56:32 +0000
@@ -24,12 +24,6 @@
24 @inlineCallbacks24 @inlineCallbacks
25 def setUp(self):25 def setUp(self):
26 yield super(LookupTest, self).setUp()26 yield super(LookupTest, self).setUp()
27 config = {
28 "environments": {
29 "firstenv": {
30 "type": "dummy", "admin-secret": "homer"}}}
31 self.write_config(dump(config))
32 self.config.load()
33 self.environment = self.config.get_default()27 self.environment = self.config.get_default()
34 self.provider = self.environment.get_machine_provider()28 self.provider = self.environment.get_machine_provider()
3529
3630
=== modified file 'juju/control/utils.py'
--- juju/control/utils.py 2012-02-15 23:32:23 +0000
+++ juju/control/utils.py 2012-03-30 16:56:32 +0000
@@ -5,6 +5,7 @@
55
6from juju.environment.errors import EnvironmentsConfigError6from juju.environment.errors import EnvironmentsConfigError
7from juju.state.errors import ServiceUnitStateMachineNotAssigned7from juju.state.errors import ServiceUnitStateMachineNotAssigned
8from juju.state.environment import EnvironmentStateManager
8from juju.state.machine import MachineStateManager9from juju.state.machine import MachineStateManager
9from juju.state.service import ServiceStateManager10from juju.state.service import ServiceStateManager
1011
@@ -20,6 +21,26 @@
20 return environment21 return environment
2122
2223
24def sync_environment_state(client, config, name):
25 """Push the local environment config to zookeeper.
26
27 This needs to be done:
28
29 * On any command which can cause the provisioning agent to take action
30 against the provider (ie create/destroy a machine), because the PA
31 needs to use credentials stored in the environment config to do so.
32 * On any command which uses constraints-related code (even if indirectly)
33 because Constraints objects are provider-specific, and need to be
34 created with the help of a MachineProvider; and the only way state code
35 can get a MachineProvider is by getting one from ZK (we certainly don't
36 want to thread the relevant provider from juju.control and/or the PA
37 itself all the way through the state code). So, we sync, to ensure
38 that state code can use an EnvironmentStateManager to get a provider.
39 """
40 esm = EnvironmentStateManager(client)
41 return esm.set_config_state(config, name)
42
43
23@inlineCallbacks44@inlineCallbacks
24def get_ip_address_for_machine(client, provider, machine_id):45def get_ip_address_for_machine(client, provider, machine_id):
25 """Returns public DNS name and machine state for the machine id.46 """Returns public DNS name and machine state for the machine id.
@@ -59,6 +80,12 @@
59 return os.path.abspath(os.path.expanduser(p))80 return os.path.abspath(os.path.expanduser(p))
6081
6182
83def expand_constraints(s):
84 if s:
85 return s.split(" ")
86 return []
87
88
62class ParseError(Exception):89class ParseError(Exception):
63 """Used to support returning custom parse errors in passthrough parsing.90 """Used to support returning custom parse errors in passthrough parsing.
6491
6592
=== modified file 'juju/environment/config.py'
--- juju/environment/config.py 2012-02-29 20:38:01 +0000
+++ juju/environment/config.py 2012-03-30 16:56:32 +0000
@@ -4,20 +4,10 @@
44
5from juju.environment.environment import Environment5from juju.environment.environment import Environment
6from juju.environment.errors import EnvironmentsConfigError6from juju.environment.errors import EnvironmentsConfigError
7from juju.errors import (7from juju.errors import FileAlreadyExists, FileNotFound
8 FileAlreadyExists,
9 FileNotFound,
10 )
11from juju.lib.schema import (8from juju.lib.schema import (
12 Constant,9 Constant, Dict, KeyDict, OAuthString, OneOf, SchemaError, SelectDict,
13 Dict,10 String)
14 KeyDict,
15 OAuthString,
16 OneOf,
17 SchemaError,
18 SelectDict,
19 String,
20 )
2111
2212
23DEFAULT_CONFIG_PATH = "~/.juju/environments.yaml"13DEFAULT_CONFIG_PATH = "~/.juju/environments.yaml"
@@ -31,56 +21,60 @@
31 default-series: oneiric21 default-series: oneiric
32"""22"""
3323
24_EITHER_PLACEMENT = OneOf(Constant("unassigned"), Constant("local"))
3425
35SCHEMA = KeyDict({26SCHEMA = KeyDict({
36 "default": String(),27 "default": String(),
37 "environments": Dict(String(), SelectDict("type", {28 "environments": Dict(String(), SelectDict("type", {
38 "ec2": KeyDict({"control-bucket": String(),29 "ec2": KeyDict({
39 "admin-secret": String(),30 "control-bucket": String(),
40 "access-key": String(),31 "admin-secret": String(),
41 "secret-key": String(),32 "access-key": String(),
42 "region": OneOf(33 "secret-key": String(),
43 Constant("us-east-1"),34 "region": OneOf(
44 Constant("us-west-1"),35 Constant("us-east-1"),
45 Constant("us-west-2"),36 Constant("us-west-1"),
46 Constant("eu-west-1"),37 Constant("us-west-2"),
47 Constant("sa-east-1"),38 Constant("eu-west-1"),
48 Constant("ap-northeast-1"),39 Constant("sa-east-1"),
49 Constant("ap-southeast-1")),40 Constant("ap-northeast-1"),
50 "default-instance-type": String(),41 Constant("ap-southeast-1")),
51 "default-ami": String(),42 "ec2-uri": String(),
52 "ec2-uri": String(),43 "s3-uri": String(),
53 "s3-uri": String(),44 "placement": _EITHER_PLACEMENT,
54 "placement": OneOf(45 "default-series": String()},
55 Constant("unassigned"),46 optional=[
56 Constant("local")),47 "access-key", "secret-key", "region", "ec2-uri", "s3-uri",
57 "default-series": String()},48 "placement"]),
58 optional=["access-key", "secret-key",49 "orchestra": KeyDict({
59 "default-instance-type", "default-ami",50 "orchestra-server": String(),
60 "region", "ec2-uri", "s3-uri", "placement"]),51 "orchestra-user": String(),
61 "orchestra": KeyDict({"orchestra-server": String(),52 "orchestra-pass": String(),
62 "orchestra-user": String(),53 "admin-secret": String(),
63 "orchestra-pass": String(),54 "acquired-mgmt-class": String(),
64 "admin-secret": String(),55 "available-mgmt-class": String(),
65 "acquired-mgmt-class": String(),56 "storage-url": String(),
66 "available-mgmt-class": String(),57 "storage-user": String(),
67 "storage-url": String(),58 "storage-pass": String(),
68 "storage-user": String(),59 "placement": _EITHER_PLACEMENT,
69 "storage-pass": String(),60 "default-series": String()},
70 "placement": String(),61 optional=[
71 "default-series": String()},62 "storage-url", "storage-user", "storage-pass", "placement"]),
72 optional=["storage-url", "storage-user",
73 "storage-pass", "placement"]),
74 "maas": KeyDict({63 "maas": KeyDict({
75 "maas-server": String(),64 "maas-server": String(),
76 "maas-oauth": OAuthString(),65 "maas-oauth": OAuthString(),
77 "admin-secret": String(),66 "admin-secret": String(),
78 }),67 "placement": _EITHER_PLACEMENT,
79 "local": KeyDict({"admin-secret": String(),68 # MAAS currently only provisions precise; any other default-series
80 "data-dir": String(),69 # would just lead to errors down the line.
81 "placement": Constant("local"),70 "default-series": Constant("precise")},
82 "default-series": String()},71 optional=["placement"]),
83 optional=["placement"]),72 "local": KeyDict({
73 "admin-secret": String(),
74 "data-dir": String(),
75 "placement": Constant("local"),
76 "default-series": String()},
77 optional=["placement"]),
84 "dummy": KeyDict({})}))},78 "dummy": KeyDict({})}))},
85 optional=["default"])79 optional=["default"])
8680
8781
=== modified file 'juju/environment/tests/test_config.py'
--- juju/environment/tests/test_config.py 2011-10-06 22:24:21 +0000
+++ juju/environment/tests/test_config.py 2012-03-30 16:56:32 +0000
@@ -8,6 +8,7 @@
8from juju.environment.environment import Environment8from juju.environment.environment import Environment
9from juju.environment.errors import EnvironmentsConfigError9from juju.environment.errors import EnvironmentsConfigError
10from juju.errors import FileNotFound, FileAlreadyExists10from juju.errors import FileNotFound, FileAlreadyExists
11from juju.state.environment import EnvironmentStateManager
1112
12from juju.lib.testing import TestCase13from juju.lib.testing import TestCase
1314
@@ -38,6 +39,16 @@
38 default-series: oneiric39 default-series: oneiric
39"""40"""
4041
42SAMPLE_MAAS = """
43environments:
44 sample:
45 type: maas
46 maas-server: somewhe.re
47 maas-oauth: foo:bar:baz
48 admin-secret: garden
49 default-series: precise
50"""
51
41SAMPLE_LOCAL = """52SAMPLE_LOCAL = """
42ensemble: environments53ensemble: environments
4354
@@ -58,7 +69,7 @@
58 self.patch(environment, "LSB_RELEASE_PATH", release_path)69 self.patch(environment, "LSB_RELEASE_PATH", release_path)
59 self.old_home = os.environ.get("HOME")70 self.old_home = os.environ.get("HOME")
60 self.tmp_home = self.makeDir()71 self.tmp_home = self.makeDir()
61 self.change_environment(HOME=self.tmp_home)72 self.change_environment(HOME=self.tmp_home, PATH=os.environ["PATH"])
62 self.default_path = os.path.join(self.tmp_home,73 self.default_path = os.path.join(self.tmp_home,
63 ".juju/environments.yaml")74 ".juju/environments.yaml")
64 self.other_path = os.path.join(self.tmp_home,75 self.other_path = os.path.join(self.tmp_home,
@@ -76,6 +87,30 @@
76 with open(path, "w") as file:87 with open(path, "w") as file:
77 file.write(config_text)88 file.write(config_text)
7889
90 # The following methods expect to be called *after* a subclass has set
91 # self.client.
92
93 def push_config(self, name, config):
94 self.write_config(yaml.dump(config))
95 self.config.load()
96 esm = EnvironmentStateManager(self.client)
97 return esm.set_config_state(self.config, name)
98
99 @inlineCallbacks
100 def push_env_constraints(self, *constraint_strs):
101 esm = EnvironmentStateManager(self.client)
102 constraint_set = yield esm.get_constraint_set()
103 yield esm.set_constraints(constraint_set.parse(constraint_strs))
104
105 @inlineCallbacks
106 def push_default_config(self, with_constraints=True):
107 config = {
108 "environments": {"firstenv": {
109 "type": "dummy", "storage-directory": self.makeDir()}}}
110 yield self.push_config("firstenv", config)
111 if with_constraints:
112 yield self.push_env_constraints()
113
79114
80class EnvironmentsConfigTest(EnvironmentsConfigTestBase):115class EnvironmentsConfigTest(EnvironmentsConfigTestBase):
81116
@@ -540,6 +575,9 @@
540 def test_ec2_sample_config_without_admin_secret(self):575 def test_ec2_sample_config_without_admin_secret(self):
541 self.assert_ec2_sample_config("admin-secret")576 self.assert_ec2_sample_config("admin-secret")
542577
578 def test_ec2_sample_config_without_default_series(self):
579 self.assert_ec2_sample_config("default-series")
580
543 def test_ec2_sample_config_without_control_buckets(self):581 def test_ec2_sample_config_without_control_buckets(self):
544 self.assert_ec2_sample_config("control-bucket")582 self.assert_ec2_sample_config("control-bucket")
545583
@@ -603,7 +641,6 @@
603641
604 def test_orchestra_respects_default_series(self):642 def test_orchestra_respects_default_series(self):
605 config = yaml.load(SAMPLE_ORCHESTRA)643 config = yaml.load(SAMPLE_ORCHESTRA)
606
607 config["environments"]["sample"]["default-series"] = "magnificent"644 config["environments"]["sample"]["default-series"] = "magnificent"
608 self.write_config(yaml.dump(config), other_path=True)645 self.write_config(yaml.dump(config), other_path=True)
609 self.config.load(self.other_path)646 self.config.load(self.other_path)
@@ -613,6 +650,58 @@
613650
614 def test_orchestra_verifies_placement(self):651 def test_orchestra_verifies_placement(self):
615 config = yaml.load(SAMPLE_ORCHESTRA)652 config = yaml.load(SAMPLE_ORCHESTRA)
653 config["environments"]["sample"]["placement"] = "random"
654 self.write_config(yaml.dump(config), other_path=True)
655 e = self.assertRaises(
656 EnvironmentsConfigError, self.config.load, self.other_path)
657 self.assertIn("expected 'unassigned', got 'random'",
658 str(e))
659
660 config["environments"]["sample"]["placement"] = "local"
661 self.write_config(yaml.dump(config), other_path=True)
662 self.config.load(self.other_path)
663
664 data = self.config.get_default().placement
665 self.assertEqual(data, "local")
666
667 def test_maas_schema_requires(self):
668 requires = "maas-server maas-oauth admin-secret default-series".split()
669 for require in requires:
670 config = yaml.load(SAMPLE_MAAS)
671 del config["environments"]["sample"][require]
672 self.write_config(yaml.dump(config), other_path=True)
673
674 try:
675 self.config.load(self.other_path)
676 except EnvironmentsConfigError as error:
677 self.assertEquals(str(error),
678 "Environments configuration error: %s: "
679 "environments.sample.%s: "
680 "required value not found"
681 % (self.other_path, require))
682 else:
683 self.fail("Did not properly require %s when type == maas"
684 % require)
685
686 def test_maas_default_series(self):
687 config = yaml.load(SAMPLE_MAAS)
688 config["environments"]["sample"]["default-series"] = "magnificent"
689 self.write_config(yaml.dump(config), other_path=True)
690 e = self.assertRaises(
691 EnvironmentsConfigError, self.config.load, self.other_path)
692 self.assertIn(
693 "environments.sample.default-series: expected 'precise', got "
694 "'magnificent'",
695 str(e))
696
697 def test_maas_verifies_placement(self):
698 config = yaml.load(SAMPLE_MAAS)
699 config["environments"]["sample"]["placement"] = "random"
700 self.write_config(yaml.dump(config), other_path=True)
701 e = self.assertRaises(
702 EnvironmentsConfigError, self.config.load, self.other_path)
703 self.assertIn("expected 'unassigned', got 'random'",
704 str(e))
616705
617 config["environments"]["sample"]["placement"] = "local"706 config["environments"]["sample"]["placement"] = "local"
618 self.write_config(yaml.dump(config), other_path=True)707 self.write_config(yaml.dump(config), other_path=True)
@@ -623,25 +712,17 @@
623712
624 def test_lxc_requires_data_dir(self):713 def test_lxc_requires_data_dir(self):
625 """lxc dev only supports local placement."""714 """lxc dev only supports local placement."""
626 self.config.write_sample()
627 config = yaml.load(SAMPLE_LOCAL)715 config = yaml.load(SAMPLE_LOCAL)
628
629 self.write_config(yaml.dump(config), other_path=True)716 self.write_config(yaml.dump(config), other_path=True)
630 error = self.assertRaises(717 error = self.assertRaises(
631 EnvironmentsConfigError,718 EnvironmentsConfigError, self.config.load, self.other_path)
632 self.config.load,
633 self.other_path)
634 self.assertIn("data-dir: required value not found", str(error))719 self.assertIn("data-dir: required value not found", str(error))
635720
636 def test_lxc_verifies_placement(self):721 def test_lxc_verifies_placement(self):
637 """lxc dev only supports local placement."""722 """lxc dev only supports local placement."""
638 self.config.write_sample()
639 config = yaml.load(SAMPLE_LOCAL)723 config = yaml.load(SAMPLE_LOCAL)
640
641 config["environments"]["sample"]["placement"] = "unassigned"724 config["environments"]["sample"]["placement"] = "unassigned"
642 self.write_config(yaml.dump(config), other_path=True)725 self.write_config(yaml.dump(config), other_path=True)
643 error = self.assertRaises(726 error = self.assertRaises(
644 EnvironmentsConfigError,727 EnvironmentsConfigError, self.config.load, self.other_path)
645 self.config.load,
646 self.other_path)
647 self.assertIn("expected 'local', got 'unassigned'", str(error))728 self.assertIn("expected 'local', got 'unassigned'", str(error))
648729
=== modified file 'juju/hooks/tests/test_invoker.py'
--- juju/hooks/tests/test_invoker.py 2012-03-29 02:50:29 +0000
+++ juju/hooks/tests/test_invoker.py 2012-03-30 16:56:32 +0000
@@ -12,12 +12,12 @@
12import juju12import juju
13from juju import errors13from juju import errors
14from juju.control.tests.test_status import StatusTestBase14from juju.control.tests.test_status import StatusTestBase
15from juju.environment.tests.test_config import EnvironmentsConfigTestBase
15from juju.lib.pick import pick_attr16from juju.lib.pick import pick_attr
16from juju.hooks import invoker17from juju.hooks import invoker
17from juju.hooks import commands18from juju.hooks import commands
18from juju.hooks.protocol import UnitSettingsFactory19from juju.hooks.protocol import UnitSettingsFactory
19from juju.lib.mocker import MATCH20from juju.lib.mocker import MATCH
20from juju.lib.testing import TestCase
21from juju.lib.twistutils import get_module_directory21from juju.lib.twistutils import get_module_directory
22from juju.state import hook22from juju.state import hook
23from juju.state.endpoint import RelationEndpoint23from juju.state.endpoint import RelationEndpoint
@@ -132,7 +132,12 @@
132 return ":".join(search_path)132 return ":".join(search_path)
133133
134134
135class InvokerTestBase(TestCase):135class InvokerTestBase(EnvironmentsConfigTestBase):
136
137 @defer.inlineCallbacks
138 def setUp(self):
139 yield super(InvokerTestBase, self).setUp()
140 yield self.push_default_config()
136141
137 def update_invoker_env(self, local_unit, remote_unit):142 def update_invoker_env(self, local_unit, remote_unit):
138 """Update os.env for a hook invocation.143 """Update os.env for a hook invocation.
139144
=== modified file 'juju/machine/constraints.py'
--- juju/machine/constraints.py 2012-03-09 13:24:58 +0000
+++ juju/machine/constraints.py 2012-03-30 16:56:32 +0000
@@ -1,64 +1,197 @@
1import logging
1import operator2import operator
2from string import ascii_lowercase
3from UserDict import DictMixin3from UserDict import DictMixin
44
5from juju.errors import ConstraintError, UnknownConstraintError5from juju.errors import ConstraintError, UnknownConstraintError
66
77log = logging.getLogger("juju.machine.constraints")
8class Constraints(object, DictMixin):8
9 """A Constraints object encapsulates a set of machine constraints.9# To allow providers to construct ConstraintSets which silently ignore known-
1010# but-inapplicable constraints (ec2-zone on orchestra, for example), we keep
11 Constraints objects are expected to be initially constructed using the11# a hardcoded global registry here. It will not be hard to remember to update
12 `from_strs` method to parse user input; they can subsequently be serialised12# this when working with providers, because _ConstraintTypes with unknown
13 as a `data` dict and reconstructed directly from same.13# names cannot be created and will cause test failures so long as the added
1414# constraint registration code is exercised in the provider tests.
15 They also implement a dict interface, which exposes all constraints for15ALL_NAMES = (
16 the appropriate provider, and is the expected mode of usage for clients16 "ubuntu-series", "provider-type",
17 not concerned with the construction or comparison of Constraints objects.17 "arch", "cpu", "mem", "instance-type",
1818 "ec2-zone",
19 A Constraints object only ever contains a single "layer" of data, but can19 "maas-name",
20 be combined with other Constraints objects in such a way as to produce a20 "orchestra-classes",
21 single object following the rules laid down in internals/placement-spec.21)
2222
23 Constraints objects can be compared, in a limited sense, by using the23
24 `can_satisfy` method.24def _dont_convert(s):
25 return s
26
27
28class _ConstraintType(object):
29 """Defines a constraint.
30
31 :param str name: The constraint's name
32 :param default: The default value as a str, or None to indicate "unset"
33 :param converter: Function to convert str value to "real" value (and
34 thereby implicitly validate it; should raise ValueError)
35 :param comparer: Function used to determine whether one constraint
36 satisfies another
37 :param bool visible: If False, indicates a computed constraint which
38 should not be settable by a user.
39
40 Merely creating a Constraint does not activate it; you also need to
41 register it with a specific ConstraintSet.
25 """42 """
2643
27 def __init__(self, data):44 def __init__(self, name, default, converter, comparer, visible):
28 # To avoid inconsistency, all Constraints objects must be constructed45 assert name in ALL_NAMES, "please update ALL_NAMES"
29 # with the same set of _Constraint~s (and conflicts) in play.46 self.name = name
30 _Constraint.freeze()47 self.default = default
31 assert data.get("provider-type"), "missing provider-type"48 self._converter = converter
32 for k, v in data.items():49 self._comparer = comparer
33 _Constraint.get(k).convert(v)50 self.visible = visible
34 self._data = data51
3552 def convert(self, s):
36 @classmethod53 """Convert a string representation of a constraint into a useful form.
37 def from_strs(cls, provider, strs):54 """
38 """Create from strings (as used on the command line)"""55 if s is None:
39 data = {"provider-type": provider}56 return
40 relevant_names = _Constraint.names(provider)57 try:
58 return self._converter(s)
59 except ValueError as e:
60 raise ConstraintError(
61 "Bad %r constraint %r: %s" % (self.name, s, e))
62
63 def can_satisfy(self, candidate, benchmark):
64 """Check whether candidate can satisfy benchmark"""
65 return self._comparer(candidate, benchmark)
66
67
68class ConstraintSet(object):
69 """A ConstraintSet represents all constraints applicable to a provider.
70
71 Individual providers can construct ConstraintSets which will be used to
72 construct Constraints objects directly relevant to that provider."""
73
74 def __init__(self, provider_type):
75 self._provider_type = provider_type
76 self._registry = {}
77 self._conflicts = {}
78
79 # These constraints must always be available (but are not user-visible
80 # or -settable).
81 self.register("ubuntu-series", visible=False)
82 self.register("provider-type", visible=False)
83
84 def register(self, name, default=None, converter=_dont_convert,
85 comparer=operator.eq, visible=True):
86 """Register a constraint to be handled by this ConstraintSet.
87
88 :param str name: The constraint's name
89 :param default: The default value as a str, or None to indicate "unset"
90 :param converter: Function to convert str value to "real" value (and
91 thereby implicitly validate it; should raise ValueError)
92 :param comparer: Function used to determine whether one constraint
93 satisfies another
94 :param bool visible: If False, indicates a computed constraint which
95 should not be settable by a user.
96 """
97 self._registry[name] = _ConstraintType(
98 name, default, converter, comparer, visible)
99 self._conflicts[name] = set()
100
101 def register_conflicts(self, reds, blues):
102 """Set cross-constraint override behaviour.
103
104 :param reds: list of constraint names which affect all constraints
105 specified in `blues`
106 :param blues: list of constraint names which affect all constraints
107 specified in `reds`
108
109 When two constraints conflict:
110
111 * It is an error to set both constraints in the same Constraints.
112 * When a Constraints overrides another which specifies a conflicting
113 constraint, the value in the overridden Constraints is cleared.
114 """
115 for red in reds:
116 self._conflicts[red].update(blues)
117 for blue in blues:
118 self._conflicts[blue].update(reds)
119
120 def register_generics(self, instance_type_names):
121 """Register a common set of constraints.
122
123 This always includes arch, cpu, and mem; and will include instance-type
124 if instance_type_names is not empty. This is because we believe
125 instance-type to be a broadly applicable concept, even though the only
126 provider that registers names here (and hence accepts the constraint)
127 is currently EC2.
128 """
129 self.register("arch", default="amd64", converter=_convert_arch)
130 self.register(
131 "cpu", default="1", converter=_convert_cpu, comparer=operator.ge)
132 self.register(
133 "mem", default="512M", converter=_convert_mem,
134 comparer=operator.ge)
135
136 if instance_type_names:
137
138 def convert(instance_type_name):
139 if instance_type_name in instance_type_names:
140 return instance_type_name
141 raise ValueError("unknown instance type")
142
143 self.register("instance-type", converter=convert)
144 self.register_conflicts(["cpu", "mem"], ["instance-type"])
145
146 def names(self):
147 """Get the names of all registered constraints."""
148 return self._registry.keys()
149
150 def get(self, name):
151 """Get the (internal) _ConstraintType object corresponding to `name`.
152
153 If `name` is known, but not registered in this ConstraintSet, None will
154 be returned; if the constraint name is entirely unknown, an
155 UnknownConstraintError will be raised.
156 """
157 try:
158 return self._registry[name]
159 except KeyError:
160 if name not in ALL_NAMES:
161 raise UnknownConstraintError(name)
162
163 def parse(self, strs):
164 """Create a Constraints from strings (as used on the command line)"""
165 data = {"provider-type": self._provider_type}
41 for s in strs:166 for s in strs:
42 try:167 try:
43 name, value = s.split("=", 1)168 name, value = s.split("=", 1)
44 constraint = _Constraint.get(name)169 constraint = self.get(name)
45 except KeyError:170 if constraint is None:
46 raise UnknownConstraintError(name)171 # A constraint called name does exist for some provider (if
172 # it didn't exist anywhere, .get would have raised) but is
173 # not relevant for this provider.
174 log.warn(
175 "ignored irrelevant %r constraint", name)
176 continue
177 if value == "any":
178 value = None
179 if value == "":
180 value = constraint.default
181 constraint.convert(value)
47 except ValueError as e:182 except ValueError as e:
48 raise ConstraintError(183 raise ConstraintError(
49 "Could not interpret %r constraint: %s" % (s, e))184 "Could not interpret %r constraint: %s" % (s, e))
50 if name not in relevant_names:
51 continue
52 if not constraint.visible:185 if not constraint.visible:
53 raise ConstraintError(186 raise ConstraintError(
54 "Cannot set computed constraint: %r" % name)187 "Cannot set computed constraint: %r" % name)
55 data[name] = value or constraint.default188 data[name] = value
56189
57 conflicts = set()190 conflicts = set()
58 for name in sorted(data):191 for name in sorted(data):
59 for conflict in sorted(_Constraint.get(name).conflicts):192 if data[name] is None:
60 if conflict not in relevant_names:193 continue
61 continue194 for conflict in sorted(self._conflicts[name]):
62 if conflict in data:195 if conflict in data:
63 raise ConstraintError(196 raise ConstraintError(
64 "Ambiguous constraints: %r overlaps with %r"197 "Ambiguous constraints: %r overlaps with %r"
@@ -66,13 +199,54 @@
66 conflicts.add(conflict)199 conflicts.add(conflict)
67200
68 data.update(dict((conflict, None) for conflict in conflicts))201 data.update(dict((conflict, None) for conflict in conflicts))
69 return Constraints(data)202 return Constraints(self, data)
203
204 def load(self, data):
205 """Convert a data dict to a Constraints"""
206 for k, v in data.items():
207 self.get(k).convert(v)
208 return Constraints(self, data)
209
210
211class Constraints(object, DictMixin):
212 """A Constraints object encapsulates a set of machine constraints.
213
214 Constraints instances should not be constructed directly; please use
215 ConstraintSet's parse and load methods instead.
216
217 They implement a dict interface, which exposes all constraints for the
218 appropriate provider, and is the expected mode of usage for clients not
219 concerned with the construction or comparison of Constraints objects.
220
221 A Constraints object only ever contains a single "layer" of data, but can
222 be combined with other Constraints objects in such a way as to produce a
223 single object following the rules laid down in internals/placement-spec.
224
225 Constraints objects can be compared, in a limited sense, by using the
226 `can_satisfy` method.
227 """
228
229 def __init__(self, available, data):
230 self._available = available
231 self._data = data
232
233 def keys(self):
234 """DictMixin"""
235 return self._available.names()
236
237 def __getitem__(self, name):
238 """DictMixin"""
239 if name not in self.keys():
240 raise KeyError(name)
241 constraint = self._available.get(name)
242 raw_value = self.data.get(name, constraint.default)
243 return constraint.convert(raw_value)
70244
71 def with_series(self, series):245 def with_series(self, series):
72 """Return a Constraints with the "ubuntu-series" set to `series`"""246 """Return a Constraints with the "ubuntu-series" set to `series`"""
73 data = dict(self._data)247 data = dict(self._data)
74 data["ubuntu-series"] = series248 data["ubuntu-series"] = series
75 return Constraints(data)249 return self._available.load(data)
76250
77 @property251 @property
78 def complete(self):252 def complete(self):
@@ -108,7 +282,11 @@
108 # place unit on machine282 # place unit on machine
109 """283 """
110 if not (self.complete and other.complete):284 if not (self.complete and other.complete):
111 raise ConstraintError("Cannot compare incomplete constraints")285 # Incomplete constraints cannot satisfy or be satisfied; we should
286 # only ever hit this branch if we're running new code (that knows
287 # about constraints) against an old deployment (which will contain
288 # at least *some* services/machines which don't have constraints).
289 return False
112290
113 for (name, unit_value) in other.items():291 for (name, unit_value) in other.items():
114 if unit_value is None:292 if unit_value is None:
@@ -116,168 +294,29 @@
116 continue294 continue
117 machine_value = self[name]295 machine_value = self[name]
118 if machine_value is None:296 if machine_value is None:
119 # The unit *does* care, and the machine value isn't specified,297 # The unit *does* care, and the machine value isn't
120 # so we can't guarantee a match. If we were to update machine298 # specified, so we can't guarantee a match. If we were
121 # constraints after provisioning (ie when we knew the values of299 # to update machine constraints after provisioning (ie
122 # the constraints left unspecified) we'd hit this branch less300 # when we knew the values of the constraints left
123 # often.301 # unspecified) we'd hit this branch less often. We
124 # We may also need to do something clever here to get sensible302 # may also need to do something clever here to get
125 # machine reuse on ec2 -- in what circumstances, if ever, is it303 # sensible machine reuse on ec2 -- in what
126 # OK to place a unit specced for one instance-type on a machine304 # circumstances, if ever, is it OK to place a unit
127 # of another type? Does it matter if either or both were derived305 # specced for one instance-type on a machine of
128 # from generic constraints? What about cost?306 # another type? Does it matter if either or both were
307 # derived from generic constraints? What about cost?
129 return False308 return False
130 if not _Constraint.get(name).can_satisfy(machine_value, unit_value):309 constraint = self._available.get(name)
310 if not constraint.can_satisfy(machine_value, unit_value):
131 # The machine's value is definitely not ok for the unit.311 # The machine's value is definitely not ok for the unit.
132 return False312 return False
133313
134 return True314 return True
135315
136 # DictMixin methods316
137317#==============================================================================
138 def keys(self):318# Generic constraint information (used by multiple providers).
139 return _Constraint.names(self._data.get("provider-type"))
140
141 def __getitem__(self, name):
142 if name not in self.keys():
143 raise KeyError(name)
144 constraint = _Constraint.get(name)
145 raw_value = self.data.get(name, constraint.default)
146 return constraint.convert(raw_value)
147
148
149#======================================================================
150# Constraint type registration
151
152def _dont_convert(s):
153 return s
154
155
156class _Constraint(object):
157
158 _registry = {}
159 _conflicts = {}
160 _frozen = False
161
162 def __init__(self, name, default, converter, comparer, provider, visible):
163 self._name = name
164 self._default = default
165 self._converter = converter
166 self._comparer = comparer
167 self._provider = provider
168 self._visible = visible
169
170 @classmethod
171 def freeze(cls):
172 """Once called, prevents register and register_conflicts from working.
173
174 Intent is to enforce consistency of Constraints construction and
175 overriding: if the set of constraints or the registered overlaps
176 were to change at runtime, the same operations could end up
177 producing different results.
178 """
179 cls._frozen = True
180
181 @classmethod
182 def register(cls, name, default=None, converter=_dont_convert,
183 comparer=operator.eq, provider=None, visible=True):
184 """Define a constraint.
185
186 :param str name: The constraint's name
187 :param default: The default value as a str, or None to indicate "unset"
188 :param converter: Function to convert str value to "real" value
189 :param comparer: Function used to determine whether one constraint
190 satisfies another
191 :param provider: The name of the provider for which this constraint is
192 meaningful (None indicates always meaningful)
193 :param bool visible: If False, indicates a computed constraint which
194 should not be settable by a user.
195
196 In service of consistency, is an error to attempt to register a new
197 constraint once a Constraints object has been created.
198 """
199 assert not cls._frozen
200 inst = cls(name, default, converter, comparer, provider, visible)
201 cls._registry[name] = inst
202 cls._conflicts[name] = set()
203
204 @classmethod
205 def register_conflicts(cls, reds, blues):
206 """Set cross-constraint override behaviour.
207
208 :param reds: list of constraint names which affect all constraints
209 specified in `blues`
210 :param blues: list of constraint names which affect all constraints
211 specified in `reds`
212
213 When two constraints conflict:
214
215 * It is an error to set both constraints in the same Constraints.
216 * When a Constraints overrides another which specifies a conflicting
217 constraint, the value in the overridden Constraints is cleared.
218
219 In service of consistency, is an error to attempt to register any new
220 conflicts once a Constraints object has been created.
221 """
222 assert not cls._frozen
223 for red in reds:
224 cls._conflicts[red].update(blues)
225 for blue in blues:
226 cls._conflicts[blue].update(reds)
227
228 @classmethod
229 def get(cls, name):
230 try:
231 return cls._registry[name]
232 except KeyError:
233 raise UnknownConstraintError(name)
234
235 @classmethod
236 def names(cls, provider):
237 return [
238 name for (name, constraint) in cls._registry.items()
239 if constraint.provider in set([None, provider])]
240
241 @property
242 def conflicts(self):
243 return self._conflicts[self._name]
244
245 @property
246 def default(self):
247 return self._default
248
249 @property
250 def provider(self):
251 return self._provider
252
253 @property
254 def visible(self):
255 return self._visible
256
257 def convert(self, s):
258 if s is None:
259 return
260 try:
261 return self._converter(s)
262 except ValueError as e:
263 raise ConstraintError(
264 "Bad %r constraint %r: %s" % (self._name, s, e))
265
266 def can_satisfy(self, candidate, benchmark):
267 return self._comparer(candidate, benchmark)
268
269
270#======================================================================
271# Generic (but not user-visible) constraints; always relevant
272_Constraint.register("ubuntu-series", visible=False)
273_Constraint.register("provider-type", visible=False)
274
275
276#======================================================================
277# Generic (user-visible) constraints; always relevant
278
279_VALID_ARCHS = ("i386", "amd64", "arm")319_VALID_ARCHS = ("i386", "amd64", "arm")
280
281_MEGABYTES = 1320_MEGABYTES = 1
282_GIGABYTES = _MEGABYTES * 1024321_GIGABYTES = _MEGABYTES * 1024
283_TERABYTES = _GIGABYTES * 1024322_TERABYTES = _GIGABYTES * 1024
@@ -292,7 +331,7 @@
292331
293def _convert_cpu(s):332def _convert_cpu(s):
294 value = float(s)333 value = float(s)
295 if value > 0:334 if value >= 0:
296 return value335 return value
297 raise ValueError("must be non-negative")336 raise ValueError("must be non-negative")
298337
@@ -302,56 +341,6 @@
302 value = float(s[:-1]) * _MEM_SUFFIXES[s[-1]]341 value = float(s[:-1]) * _MEM_SUFFIXES[s[-1]]
303 else:342 else:
304 value = float(s)343 value = float(s)
305 if value > 0:344 if value >= 0:
306 return value345 return value
307 raise ValueError("must be non-negative")346 raise ValueError("must be non-negative")
308
309
310_Constraint.register("arch", converter=_convert_arch)
311_Constraint.register(
312 "mem", default="512M", converter=_convert_mem, comparer=operator.ge)
313_Constraint.register(
314 "cpu", default="1", converter=_convert_cpu, comparer=operator.ge)
315
316
317#======================================================================
318# Orchestra-only
319
320def _convert_orchestra_classes(s):
321 return s.split(",")
322
323
324def _compare_orchestra_classes(candidate, benchmark):
325 return set(candidate) >= set(benchmark)
326
327
328_Constraint.register(
329 "orchestra-classes", converter=_convert_orchestra_classes,
330 comparer=_compare_orchestra_classes, provider="orchestra")
331_Constraint.register("orchestra-name", provider="orchestra")
332
333
334#======================================================================
335# EC2-only
336
337def _convert_ec2_zone(s):
338 if s not in ascii_lowercase:
339 raise ValueError("expected lowercase ascii char")
340 return s
341
342
343_Constraint.register("ec2-instance-type", provider="ec2")
344_Constraint.register("ec2-zone", converter=_convert_ec2_zone, provider="ec2")
345
346
347#======================================================================
348# All conflicts
349
350_Constraint.register_conflicts(["orchestra-name"], ["orchestra-classes"])
351_Constraint.register_conflicts(
352 ["ec2-instance-type", "orchestra-name"], ["arch", "cpu", "mem"])
353
354#=====================================================================
355# Ensure consistency
356
357_Constraint.freeze()
358347
=== modified file 'juju/machine/tests/test_constraints.py'
--- juju/machine/tests/test_constraints.py 2012-03-09 13:24:58 +0000
+++ juju/machine/tests/test_constraints.py 2012-03-30 16:56:32 +0000
@@ -1,270 +1,168 @@
1import operator
2
1from juju.errors import ConstraintError3from juju.errors import ConstraintError
2from juju.lib.testing import TestCase4from juju.lib.testing import TestCase
3from juju.machine.constraints import Constraints5from juju.machine.constraints import Constraints, ConstraintSet
46
7# These objects exist for the convenience of other test files
8dummy_cs = ConstraintSet("dummy")
9dummy_cs.register_generics([])
10dummy_constraints = dummy_cs.parse([])
11series_constraints = dummy_constraints.with_series("series")
512
6generic_defaults = {13generic_defaults = {
7 "arch": None, "cpu": 1, "mem": 512,14 "arch": "amd64", "cpu": 1, "mem": 512,
8 "ubuntu-series": None, "provider-type": None}15 "ubuntu-series": None, "provider-type": None}
9dummy_defaults = dict(generic_defaults, **{16dummy_defaults = dict(generic_defaults, **{
10 "provider-type": "dummy"})17 "provider-type": "dummy"})
11ec2_defaults = dict(generic_defaults, **{18ec2_defaults = dict(generic_defaults, **{
12 "provider-type": "ec2",19 "provider-type": "ec2",
13 "ec2-zone": None,20 "ec2-zone": None,
14 "ec2-instance-type": None})21 "instance-type": None})
15orchestra_defaults = dict(generic_defaults, **{22orchestra_defaults = {
16 "provider-type": "orchestra",23 "provider-type": "orchestra",
17 "orchestra-name": None,24 "ubuntu-series": None,
18 "orchestra-classes": None})25 "orchestra-classes": None}
1926
20all_providers = ["dummy", "ec2", "orchestra"]27all_providers = ["dummy", "ec2", "orchestra"]
2128
29
22class ConstraintsTestCase(TestCase):30class ConstraintsTestCase(TestCase):
2331
24 def assert_error(self, message, *raises_args):32 def assert_error(self, message, *raises_args):
25 e = self.assertRaises(ConstraintError, *raises_args)33 e = self.assertRaises(ConstraintError, *raises_args)
26 self.assertEquals(str(e), message)34 self.assertEquals(str(e), message)
2735
28 def assert_roundtrip_equal(self, constraints, expected):36 def assert_roundtrip_equal(self, cs, constraints, expected):
37 self.assertEquals(dict(constraints), expected)
29 self.assertEquals(constraints, expected)38 self.assertEquals(constraints, expected)
30 self.assertEquals(Constraints(constraints.data), expected)39 self.assertEquals(dict(cs.load(constraints.data)), expected)
40 self.assertEquals(cs.load(constraints.data), expected)
3141
3242
33class ConstraintsTest(ConstraintsTestCase):43class ConstraintsTest(ConstraintsTestCase):
3444
35 def test_defaults(self):45 def test_equality(self):
36 constraints = Constraints.from_strs("orchestra", [])46 self.assert_roundtrip_equal(
37 self.assert_roundtrip_equal(constraints, orchestra_defaults)47 dummy_cs, dummy_constraints, dummy_defaults)
38 constraints = Constraints.from_strs("ec2", [])
39 self.assert_roundtrip_equal(constraints, ec2_defaults)
40 constraints = Constraints.from_strs("dummy", [])
41 self.assert_roundtrip_equal(constraints, dummy_defaults)
4248
43 def test_complete(self):49 def test_complete(self):
44 incomplete_constraints = Constraints.from_strs("womble", [])50 incomplete_constraints = dummy_cs.parse([])
45 self.assertFalse(incomplete_constraints.complete)
46 complete_constraints = incomplete_constraints.with_series("wandering")51 complete_constraints = incomplete_constraints.with_series("wandering")
47 self.assertTrue(complete_constraints.complete)52 self.assertTrue(complete_constraints.complete)
4853
49 def assert_invalid(self, message, providers, *constraint_strs):54 def assert_invalid(self, message, *constraint_strs):
50 for provider in providers:55 self.assert_error(
51 self.assert_error(56 message, dummy_cs.parse, constraint_strs)
52 message, Constraints.from_strs, provider, constraint_strs)
53
54 if len(providers) != len(all_providers):
55 # Check it *is* valid for other providers
56 for provider in all_providers:
57 if provider in providers:
58 continue
59 Constraints.from_strs(provider, constraint_strs)
6057
61 def test_invalid_input(self):58 def test_invalid_input(self):
62 """Reject nonsense constraints"""59 """Reject nonsense constraints"""
63 self.assert_invalid(60 self.assert_invalid(
64 "Could not interpret 'BLAH' constraint: need more than 1 value to "61 "Could not interpret 'BLAH' constraint: need more than 1 value to "
65 "unpack",62 "unpack",
66 all_providers, "BLAH")63 "BLAH")
67 self.assert_invalid(64 self.assert_invalid(
68 "Unknown constraint: 'foo'",65 "Unknown constraint: 'foo'",
69 all_providers, "foo=", "bar=")66 "foo=", "bar=")
7067
71 def test_invalid_constraints(self):68 def test_invalid_constraints(self):
72 """Reject nonsensical constraint values"""69 """Reject nonsensical constraint values"""
73 self.assert_invalid(70 self.assert_invalid(
74 "Bad 'arch' constraint 'leg': unknown architecture", all_providers,71 "Bad 'arch' constraint 'leg': unknown architecture",
75 "arch=leg")72 "arch=leg")
76 self.assert_invalid(73 self.assert_invalid(
77 "Bad 'cpu' constraint '-1': must be non-negative", all_providers,74 "Bad 'cpu' constraint '-1': must be non-negative",
78 "cpu=-1")75 "cpu=-1")
79 self.assert_invalid(76 self.assert_invalid(
80 "Bad 'cpu' constraint 'fish': could not convert string to float: "77 "Bad 'cpu' constraint 'fish': could not convert string to float: "
81 "fish",78 "fish",
82 all_providers, "cpu=fish")79 "cpu=fish")
83 self.assert_invalid(80 self.assert_invalid(
84 "Bad 'mem' constraint '-1': must be non-negative", all_providers,81 "Bad 'mem' constraint '-1': must be non-negative",
85 "mem=-1")82 "mem=-1")
86 self.assert_invalid(83 self.assert_invalid(
87 "Bad 'mem' constraint '4P': invalid literal for float(): 4P",84 "Bad 'mem' constraint '4P': invalid literal for float(): 4P",
88 all_providers, "mem=4P")85 "mem=4P")
89 self.assert_invalid(
90 "Bad 'ec2-zone' constraint 'Q': expected lowercase ascii char",
91 ["ec2"], "ec2-zone=Q")
9286
93 def test_hidden_constraints(self):87 def test_hidden_constraints(self):
94 """Reject attempts to explicitly specify computed constraints"""88 """Reject attempts to explicitly specify computed constraints"""
95 self.assert_invalid(89 self.assert_invalid(
96 "Cannot set computed constraint: 'ubuntu-series'", all_providers,90 "Cannot set computed constraint: 'ubuntu-series'",
97 "ubuntu-series=cheesy")91 "ubuntu-series=cheesy")
98 self.assert_invalid(92 self.assert_invalid(
99 "Cannot set computed constraint: 'provider-type'", all_providers,93 "Cannot set computed constraint: 'provider-type'",
100 "provider-type=dummy")94 "provider-type=dummy")
10195
102 def test_overlap_ec2(self):
103 """Overlapping ec2 constraints should be detected"""
104 self.assert_invalid(
105 "Ambiguous constraints: 'arch' overlaps with 'ec2-instance-type'",
106 ["ec2"], "ec2-instance-type=m1.small", "arch=i386")
107 self.assert_invalid(
108 "Ambiguous constraints: 'cpu' overlaps with 'ec2-instance-type'",
109 ["ec2"], "ec2-instance-type=m1.small", "cpu=1")
110 self.assert_invalid(
111 "Ambiguous constraints: 'ec2-instance-type' overlaps with 'mem'",
112 ["ec2"], "ec2-instance-type=m1.small", "mem=2G")
113
114 def test_overlap_orchestra(self):
115 """Overlapping orchestra constraints should be detected"""
116 self.assert_invalid(
117 "Ambiguous constraints: 'arch' overlaps with 'orchestra-name'",
118 ["orchestra"], "orchestra-name=herbert", "arch=i386")
119 self.assert_invalid(
120 "Ambiguous constraints: 'cpu' overlaps with 'orchestra-name'",
121 ["orchestra"], "orchestra-name=herbert", "cpu=1")
122 self.assert_invalid(
123 "Ambiguous constraints: 'mem' overlaps with 'orchestra-name'",
124 ["orchestra"], "orchestra-name=herbert", "mem=2G")
125 self.assert_invalid(
126 "Ambiguous constraints: 'orchestra-classes' overlaps with "
127 "'orchestra-name'",
128 ["orchestra"], "orchestra-name=herbert", "orchestra-classes=x,y")
129
13096
131class ConstraintsUpdateTest(ConstraintsTestCase):97class ConstraintsUpdateTest(ConstraintsTestCase):
13298
133 def assert_constraints(self, provider, strss, expected):99 def assert_constraints(self, strss, expected):
134 constraints = Constraints.from_strs(provider, strss[0])100 constraints = dummy_cs.parse(strss[0])
135 for strs in strss[1:]:101 for strs in strss[1:]:
136 constraints.update(Constraints.from_strs(provider, strs))102 constraints.update(dummy_cs.parse(strs))
137 self.assert_roundtrip_equal(constraints, expected)
138
139 def assert_constraints_dummy(self, strss, expected):
140 expected = dict(dummy_defaults, **expected)103 expected = dict(dummy_defaults, **expected)
141 self.assert_constraints("dummy", strss, expected)104 self.assert_roundtrip_equal(dummy_cs, constraints, expected)
142105
143 def test_constraints_dummy(self):106 def test_constraints(self):
144 """Sane constraints dicts are generated for unknown environments"""107 """Sane constraints dicts are generated for unknown environments"""
145 self.assert_constraints_dummy([[]], {})108 self.assert_constraints([[]], {})
146 self.assert_constraints_dummy([["arch=arm"]], {"arch": "arm"})109 self.assert_constraints([["cpu=", "mem="]], {})
147 self.assert_constraints_dummy([["cpu=0.1"]], {"cpu": 0.1})110 self.assert_constraints([["arch=arm"]], {"arch": "arm"})
148 self.assert_constraints_dummy([["mem=128"]], {"mem": 128})111 self.assert_constraints([["cpu=0.1"]], {"cpu": 0.1})
149 self.assert_constraints_dummy(112 self.assert_constraints([["mem=128"]], {"mem": 128})
113 self.assert_constraints([["cpu=0"]], {"cpu": 0})
114 self.assert_constraints([["mem=0"]], {"mem": 0})
115 self.assert_constraints(
150 [["arch=amd64", "cpu=6", "mem=1.5G"]],116 [["arch=amd64", "cpu=6", "mem=1.5G"]],
151 {"arch": "amd64", "cpu": 6, "mem": 1536,})117 {"arch": "amd64", "cpu": 6, "mem": 1536})
152118
153 def test_overwriting_basic(self):119 def test_overwriting_basic(self):
154 """Later values shadow earlier values"""120 """Later values shadow earlier values"""
155 self.assert_constraints_dummy(121 self.assert_constraints(
156 [["cpu=4", "mem=512"], ["arch=i386", "mem=1G"]],122 [["cpu=4", "mem=512"], ["arch=i386", "mem=1G"]],
157 {"arch": "i386", "cpu": 4, "mem": 1024})123 {"arch": "i386", "cpu": 4, "mem": 1024})
158124
159 def test_reset(self):125 def test_reset(self):
160 """Empty string resets to juju default"""126 """Empty string resets to juju default"""
161 self.assert_constraints_dummy(127 self.assert_constraints(
162 [["arch=arm", "cpu=4", "mem=1024"], ["arch=", "cpu=", "mem="]],128 [["arch=arm", "cpu=4", "mem=1024"], ["arch=", "cpu=", "mem="]],
163 {"arch": None, "cpu": 1, "mem": 512})129 {"arch": "amd64", "cpu": 1, "mem": 512})
164 self.assert_constraints_dummy(130 self.assert_constraints(
165 [["arch=", "cpu=", "mem="], ["arch=arm", "cpu=4", "mem=1024"]],131 [["arch=", "cpu=", "mem="], ["arch=arm", "cpu=4", "mem=1024"]],
166 {"arch": "arm", "cpu": 4, "mem": 1024})132 {"arch": "arm", "cpu": 4, "mem": 1024})
167133
168 def assert_constraints_ec2(self, strss, expected):
169 expected = dict(ec2_defaults, **expected)
170 self.assert_constraints("ec2", strss, expected)
171
172 def test_constraints_ec2(self):
173 """Sane constraints dicts are generated for ec2"""
174 self.assert_constraints_ec2([[]], {})
175 self.assert_constraints_ec2([["arch=arm"]], {"arch": "arm"})
176 self.assert_constraints_ec2([["cpu=128"]], {"cpu": 128})
177 self.assert_constraints_ec2([["mem=2G"]], {"mem": 2048})
178 self.assert_constraints_ec2([["ec2-zone=b"]], {"ec2-zone": "b"})
179 self.assert_constraints_ec2(
180 [["arch=amd64", "cpu=32", "mem=2G", "ec2-zone=b"]],
181 {"arch": "amd64", "cpu": 32, "mem": 2048, "ec2-zone": "b"})
182
183 def test_overwriting_ec2_instance_type(self):
184 """ec2-instance-type interacts correctly with arch, cpu, mem"""
185 self.assert_constraints_ec2(
186 [["ec2-instance-type=t1.micro"]],
187 {"ec2-instance-type": "t1.micro", "cpu": None, "mem": None})
188 self.assert_constraints_ec2(
189 [["arch=arm", "cpu=8"], ["ec2-instance-type=t1.micro"]],
190 {"ec2-instance-type": "t1.micro", "cpu": None, "mem": None})
191 self.assert_constraints_ec2(
192 [["ec2-instance-type=t1.micro"], ["arch=arm", "cpu=8"]],
193 {"ec2-instance-type": None, "arch": "arm", "cpu": 8, "mem": None})
194
195 def assert_constraints_orchestra(self, strss, expected):
196 expected = dict(orchestra_defaults, **expected)
197 self.assert_constraints("orchestra", strss, expected)
198
199 def test_constraints_orchestra(self):
200 """Sane constraints dicts are generated for orchestra"""
201 self.assert_constraints_orchestra([[]], {})
202 self.assert_constraints_orchestra([["arch=arm"]], {"arch": "arm"})
203 self.assert_constraints_orchestra([["cpu=128"]], {"cpu": 128})
204 self.assert_constraints_orchestra([["mem=0.25T"]], {"mem": 262144})
205 self.assert_constraints_orchestra(
206 [["orchestra-classes=x,y"]], {"orchestra-classes": ["x", "y"]})
207 self.assert_constraints_orchestra(
208 [["arch=i386", "cpu=2", "mem=768M", "orchestra-classes=a,b"]],
209 {"arch": "i386", "cpu": 2, "mem": 768,
210 "orchestra-classes": ["a", "b"]})
211
212 def test_overwriting_orchestra_name(self):
213 """orchestra-name interacts correctly with arch, cpu, mem"""
214 self.assert_constraints_orchestra(
215 [["orchestra-name=baggins"]],
216 {"orchestra-name": "baggins", "cpu": None, "mem": None})
217 self.assert_constraints_orchestra(
218 [["orchestra-name=baggins"], ["arch=arm"]],
219 {"orchestra-name": None, "arch": "arm", "cpu": None, "mem": None})
220 self.assert_constraints_orchestra(
221 [["arch=arm", "cpu=2"], ["orchestra-name=baggins"]],
222 {"orchestra-name": "baggins", "arch": None, "cpu": None,
223 "mem": None})
224
225 def test_overwriting_orchestra_classes(self):
226 """orchestra-classes interacts correctly with orchestra-name"""
227 self.assert_constraints_orchestra(
228 [["orchestra-name=baggins"], ["orchestra-classes=c,d", "cpu=2"]],
229 {"orchestra-classes": ["c", "d"], "cpu": 2, "mem": None})
230 self.assert_constraints_orchestra(
231 [["orchestra-classes=zz,top", "arch=amd64", "cpu=2"],
232 ["orchestra-name=baggins"]],
233 {"orchestra-name": "baggins", "orchestra-classes": None,
234 "cpu": None, "mem": None})
235
236134
237class ConstraintsFulfilmentTest(ConstraintsTestCase):135class ConstraintsFulfilmentTest(ConstraintsTestCase):
238136
239 def completed_constraints(self, provider="provider", series="series"):
240 return Constraints.from_strs(provider, []).with_series(series)
241
242 def assert_incomparable(self, c1, c2):
243 self.assert_error(
244 "Cannot compare incomplete constraints", c1.can_satisfy, c2)
245 self.assert_error(
246 "Cannot compare incomplete constraints", c2.can_satisfy, c1)
247
248 def assert_match(self, c1, c2, expected):137 def assert_match(self, c1, c2, expected):
249 self.assertEquals(c1.can_satisfy(c2), expected)138 self.assertEquals(c1.can_satisfy(c2), expected)
250 self.assertEquals(c2.can_satisfy(c1), expected)139 self.assertEquals(c2.can_satisfy(c1), expected)
251140
252 def test_fulfil_completeness(self):141 def test_fulfil_completeness(self):
253 """can_satisfy needs to be called on and with complete Constraints~s"""142 """
254 c1 = Constraints({"provider-type": "a"})143 can_satisfy needs to be called on and with complete Constraints~s to
255 c2 = Constraints({"provider-type": "a"})144 have any chance of working.
256 self.assert_incomparable(c1, c2)145 """
257 c3 = self.completed_constraints("a")146 good = Constraints(
258 self.assert_incomparable(c1, c3)147 dummy_cs, {"provider-type": "dummy", "ubuntu-series": "x"})
259 c4 = self.completed_constraints("a")148 self.assert_match(good, good, True)
260 self.assert_match(c3, c4, True)149 bad = [Constraints(dummy_cs, {}),
150 Constraints(dummy_cs, {"provider-type": "dummy"}),
151 Constraints(dummy_cs, {"ubuntu-series": "x"})]
152 for i, bad1 in enumerate(bad):
153 self.assert_match(bad1, good, False)
154 for bad2 in bad[i:]:
155 self.assert_match(bad1, bad2, False)
261156
262 def test_fulfil_matches(self):157 def test_fulfil_matches(self):
158 other_cs = ConstraintSet("other")
159 other_cs.register_generics([])
160 other_constraints = other_cs.parse([])
263 instances = (161 instances = (
264 Constraints({"provider-type": "a", "ubuntu-series": "x"}),162 dummy_constraints.with_series("x"),
265 Constraints({"provider-type": "a", "ubuntu-series": "y"}),163 dummy_constraints.with_series("y"),
266 Constraints({"provider-type": "b", "ubuntu-series": "x"}),164 other_constraints.with_series("x"),
267 Constraints({"provider-type": "b", "ubuntu-series": "y"}))165 other_constraints.with_series("y"))
268166
269 for i, c1 in enumerate(instances):167 for i, c1 in enumerate(instances):
270 self.assert_match(c1, c1, True)168 self.assert_match(c1, c1, True)
@@ -272,94 +170,237 @@
272 self.assert_match(c1, c2, False)170 self.assert_match(c1, c2, False)
273171
274 def assert_can_satisfy(172 def assert_can_satisfy(
275 self, machine_strs, unit_strs, expected, provider="provider"):173 self, machine_strs, unit_strs, expected):
276 machine = Constraints.from_strs(provider, machine_strs)174 machine = dummy_cs.parse(machine_strs)
277 machine = machine.with_series("shiny")175 machine = machine.with_series("shiny")
278 unit = Constraints.from_strs(provider, unit_strs)176 unit = dummy_cs.parse(unit_strs)
279 unit = unit.with_series("shiny")177 unit = unit.with_series("shiny")
280 self.assertEquals(machine.can_satisfy(unit), expected)178 self.assertEquals(machine.can_satisfy(unit), expected)
281179
282 if provider != "provider":
283 # Check that a different provider doesn't detect any problem,
284 # because it's ignoring all off-provider constraints.
285 machine = Constraints.from_strs("provider", machine_strs)
286 machine = machine.with_series("shiny")
287 unit = Constraints.from_strs("provider", unit_strs)
288 unit = unit.with_series("shiny")
289 self.assertEquals(machine.can_satisfy(unit), True)
290
291
292 def test_can_satisfy(self):180 def test_can_satisfy(self):
293 self.assert_can_satisfy([], [], True)181 self.assert_can_satisfy([], [], True)
294182
295 self.assert_can_satisfy(["arch=arm"], [], True)183 self.assert_can_satisfy(["arch=arm"], [], False)
184 self.assert_can_satisfy(["arch=amd64"], [], True)
296 self.assert_can_satisfy([], ["arch=arm"], False)185 self.assert_can_satisfy([], ["arch=arm"], False)
297 self.assert_can_satisfy(["arch=i386"], ["arch=arm"], False)186 self.assert_can_satisfy(["arch=i386"], ["arch=arm"], False)
298 self.assert_can_satisfy(["arch=arm"], ["arch=amd64"], False)187 self.assert_can_satisfy(["arch=arm"], ["arch=amd64"], False)
299 self.assert_can_satisfy(["arch=amd64"], ["arch=amd64"], True)188 self.assert_can_satisfy(["arch=amd64"], ["arch=amd64"], True)
189 self.assert_can_satisfy(["arch=i386"], ["arch=any"], True)
190 self.assert_can_satisfy(["arch=arm"], ["arch=any"], True)
191 self.assert_can_satisfy(["arch=amd64"], ["arch=any"], True)
192 self.assert_can_satisfy(["arch=any"], ["arch=any"], True)
193 self.assert_can_satisfy(["arch=any"], ["arch=i386"], False)
194 self.assert_can_satisfy(["arch=any"], ["arch=amd64"], False)
195 self.assert_can_satisfy(["arch=any"], ["arch=arm"], False)
300196
301 self.assert_can_satisfy(["cpu=64"], [], True)197 self.assert_can_satisfy(["cpu=64"], [], True)
302 self.assert_can_satisfy([], ["cpu=64"], False)198 self.assert_can_satisfy([], ["cpu=64"], False)
303 self.assert_can_satisfy(["cpu=64"], ["cpu=32"], True)199 self.assert_can_satisfy(["cpu=64"], ["cpu=32"], True)
304 self.assert_can_satisfy(["cpu=32"], ["cpu=64"], False)200 self.assert_can_satisfy(["cpu=32"], ["cpu=64"], False)
305 self.assert_can_satisfy(["cpu=64"], ["cpu=64"], True)201 self.assert_can_satisfy(["cpu=64"], ["cpu=64"], True)
202 self.assert_can_satisfy(["cpu=0.01"], ["cpu=any"], True)
203 self.assert_can_satisfy(["cpu=9999"], ["cpu=any"], True)
204 self.assert_can_satisfy(["cpu=any"], ["cpu=any"], True)
205 self.assert_can_satisfy(["cpu=any"], ["cpu=0.01"], False)
206 self.assert_can_satisfy(["cpu=any"], ["cpu=9999"], False)
306207
307 self.assert_can_satisfy(["mem=8G"], [], True)208 self.assert_can_satisfy(["mem=8G"], [], True)
308 self.assert_can_satisfy([], ["mem=8G"], False)209 self.assert_can_satisfy([], ["mem=8G"], False)
309 self.assert_can_satisfy(["mem=8G"], ["mem=4G"], True)210 self.assert_can_satisfy(["mem=8G"], ["mem=4G"], True)
310 self.assert_can_satisfy(["mem=4G"], ["mem=8G"], False)211 self.assert_can_satisfy(["mem=4G"], ["mem=8G"], False)
311 self.assert_can_satisfy(["mem=8G"], ["mem=8G"], True)212 self.assert_can_satisfy(["mem=8G"], ["mem=8G"], True)
312213 self.assert_can_satisfy(["mem=2M"], ["mem=any"], True)
313 self.assert_can_satisfy(214 self.assert_can_satisfy(["mem=256T"], ["mem=any"], True)
314 # orchestra-name clears default cpu/mem values.215 self.assert_can_satisfy(["mem=any"], ["mem=any"], True)
315 # This may be a problem.216 self.assert_can_satisfy(["mem=any"], ["mem=2M"], False)
316 ["orchestra-name=henry"], [], False, "orchestra")217 self.assert_can_satisfy(["mem=any"], ["mem=256T"], False)
317 self.assert_can_satisfy(218
318 [], ["orchestra-name=henry"], False, "orchestra")219
319 self.assert_can_satisfy(220class ConstraintSetTest(TestCase):
320 ["orchestra-name=henry"], ["orchestra-name=jane"], False,221
321 "orchestra")222 def allow_names(self, *names):
322 self.assert_can_satisfy(223 all_names = ["ubuntu-series", "provider-type"]
323 ["orchestra-name=jane"], ["orchestra-name=henry"], False,224 all_names.extend(names)
324 "orchestra")225 from juju.machine import constraints
325 self.assert_can_satisfy(226 self.patch(constraints, "ALL_NAMES", all_names)
326 ["orchestra-name=henry"], ["orchestra-name=henry"], True,227
327 "orchestra")228 def test_register_unknown_name(self):
328229 e = self.assertRaises(
329 self.assert_can_satisfy(230 AssertionError, ConstraintSet("provider").register, "nonsense")
330 ["orchestra-classes=a,b"], [], True, "orchestra")231 self.assertEquals(str(e), "please update ALL_NAMES")
331 self.assert_can_satisfy(232
332 [], ["orchestra-classes=a,b"], False, "orchestra")233 def test_register_known_names(self):
333 self.assert_can_satisfy(234 self.allow_names("foo", "blob")
334 ["orchestra-classes=a,b"], ["orchestra-classes=a"], True,235 cs = ConstraintSet("provider")
335 "orchestra")236 cs.register("foo")
336 self.assert_can_satisfy(237 cs.register("blob")
337 ["orchestra-classes=a"], ["orchestra-classes=a,b"], False,238 c1 = cs.parse(["foo=bar"]).with_series("series")
338 "orchestra")239 self.assertEquals(c1["foo"], "bar")
339 self.assert_can_satisfy(240 self.assertEquals(c1["blob"], None)
340 ["orchestra-classes=a,b"], ["orchestra-classes=a,b"], True,241 c2 = cs.parse(["foo=bar"]).with_series("series")
341 "orchestra")242 self.assertTrue(c1.can_satisfy(c2))
342 self.assert_can_satisfy(243 self.assertTrue(c2.can_satisfy(c1))
343 ["orchestra-classes=a,b"], ["orchestra-classes=a,c"], False,244
344 "orchestra")245 def test_unregistered_name(self):
345246 self.allow_names("foo", "bar", "baz")
346 self.assert_can_satisfy(["ec2-zone=a"], [], True, "ec2")247 cs = ConstraintSet("provider")
347 self.assert_can_satisfy([], ["ec2-zone=a"], False, "ec2")248 cs.register("bar")
348 self.assert_can_satisfy(["ec2-zone=a"], ["ec2-zone=b"], False, "ec2")249 output = self.capture_logging()
349 self.assert_can_satisfy(["ec2-zone=b"], ["ec2-zone=a"], False, "ec2")250 constraints = cs.parse(["foo=1", "bar=2", "baz=3"])
350 self.assert_can_satisfy(["ec2-zone=a"], ["ec2-zone=a"], True, "ec2")251 self.assertIn("ignored irrelevant 'foo' constraint", output.getvalue())
351252 self.assertIn("ignored irrelevant 'baz' constraint", output.getvalue())
352 self.assert_can_satisfy(253 self.assertEquals(constraints["bar"], "2")
353 # ec2-instance-type clears default cpu/mem values.254
354 # This may be a problem.255 def test_register_invisible(self):
355 ["ec2-instance-type=m1.small"], [], False, "ec2")256 self.allow_names("foo")
356 self.assert_can_satisfy([], ["ec2-instance-type=m1.small"], False, "ec2")257 cs = ConstraintSet("provider")
357 self.assert_can_satisfy(258 cs.register("foo", visible=False)
358 ["ec2-instance-type=m1.large"], ["ec2-instance-type=m1.small"],259 e = self.assertRaises(ConstraintError, cs.parse, ["foo=bar"])
359 False, "ec2")260 self.assertEquals(str(e), "Cannot set computed constraint: 'foo'")
360 self.assert_can_satisfy(261
361 ["ec2-instance-type=m1.small"], ["ec2-instance-type=m1.large"],262 def test_register_comparer(self):
362 False, "ec2")263 self.allow_names("foo")
363 self.assert_can_satisfy(264 cs = ConstraintSet("provider")
364 ["ec2-instance-type=m1.small"], ["ec2-instance-type=m1.small"],265 cs.register("foo", comparer=operator.ne)
365 True, "ec2")266 c1 = cs.parse(["foo=bar"]).with_series("series")
267 c2 = cs.parse(["foo=bar"]).with_series("series")
268 self.assertFalse(c1.can_satisfy(c2))
269 self.assertFalse(c2.can_satisfy(c1))
270 c3 = cs.parse(["foo=baz"]).with_series("series")
271 self.assertTrue(c1.can_satisfy(c3))
272 self.assertTrue(c3.can_satisfy(c1))
273
274 def test_register_default_and_converter(self):
275 self.allow_names("foo")
276 cs = ConstraintSet("provider")
277 cs.register("foo", default="star", converter=lambda s: "death-" + s)
278 c1 = cs.parse([])
279 self.assertEquals(c1["foo"], "death-star")
280 c1 = cs.parse(["foo=clock"])
281 self.assertEquals(c1["foo"], "death-clock")
282
283 def test_convert_wraps_ValueError(self):
284 self.allow_names("foo", "bar")
285
286 def raiser(e):
287 raise e
288 cs = ConstraintSet("provider")
289 cs.register("foo", converter=lambda s: raiser(ValueError(s)))
290 cs.register("bar", converter=lambda s: raiser(KeyError(s)))
291 self.assertRaises(ConstraintError, cs.parse, ["foo=1"])
292 self.assertRaises(KeyError, cs.parse, ["bar=1"])
293
294 def test_register_conflicts(self):
295 self.allow_names("foo", "bar", "baz", "qux")
296 cs = ConstraintSet("provider")
297 cs.register("foo")
298 cs.register("bar")
299 cs.register("baz")
300 cs.register("qux")
301 cs.parse(["foo=1", "bar=2", "baz=3", "qux=4"])
302
303 def assert_ambiguous(strs):
304 e = self.assertRaises(ConstraintError, cs.parse, strs)
305 self.assertTrue(str(e).startswith("Ambiguous constraints"))
306
307 cs.register_conflicts(["foo"], ["bar", "baz", "qux"])
308 assert_ambiguous(["foo=1", "bar=2"])
309 assert_ambiguous(["foo=1", "baz=3"])
310 assert_ambiguous(["foo=1", "qux=4"])
311 cs.parse(["foo=1"])
312 cs.parse(["bar=2", "baz=3", "qux=4"])
313
314 cs.register_conflicts(["bar", "baz"], ["qux"])
315 assert_ambiguous(["bar=2", "qux=4"])
316 assert_ambiguous(["baz=3", "qux=4"])
317 cs.parse(["foo=1"])
318 cs.parse(["bar=2", "baz=3"])
319 cs.parse(["qux=4"])
320
321 def test_register_generics_no_instance_types(self):
322 cs = ConstraintSet("provider")
323 cs.register_generics([])
324 c1 = cs.parse([])
325 self.assertEquals(c1["arch"], "amd64")
326 self.assertEquals(c1["cpu"], 1.0)
327 self.assertEquals(c1["mem"], 512.0)
328 self.assertFalse("instance-type" in c1)
329
330 c2 = cs.parse(["arch=any", "cpu=0", "mem=8G"])
331 self.assertEquals(c2["arch"], None)
332 self.assertEquals(c2["cpu"], 0.0)
333 self.assertEquals(c2["mem"], 8192.0)
334 self.assertFalse("instance-type" in c2)
335
336 def test_register_generics_with_instance_types(self):
337 cs = ConstraintSet("provider")
338 cs.register_generics(["a1.big", "c7.peculiar"])
339 c1 = cs.parse([])
340 self.assertEquals(c1["arch"], "amd64")
341 self.assertEquals(c1["cpu"], 1.0)
342 self.assertEquals(c1["mem"], 512.0)
343 self.assertEquals(c1["instance-type"], None)
344
345 c2 = cs.parse(["arch=any", "cpu=0", "mem=8G"])
346 self.assertEquals(c2["arch"], None)
347 self.assertEquals(c2["cpu"], 0.0)
348 self.assertEquals(c2["mem"], 8192.0)
349 self.assertEquals(c2["instance-type"], None)
350
351 c3 = cs.parse(["instance-type=c7.peculiar", "arch=i386"])
352 self.assertEquals(c3["arch"], "i386")
353 self.assertEquals(c3["cpu"], None)
354 self.assertEquals(c3["mem"], None)
355 self.assertEquals(c3["instance-type"], "c7.peculiar")
356
357 def assert_ambiguous(strs):
358 e = self.assertRaises(ConstraintError, cs.parse, strs)
359 self.assertTrue(str(e).startswith("Ambiguous constraints"))
360
361 assert_ambiguous(["cpu=1", "instance-type=c7.peculiar"])
362 assert_ambiguous(["mem=1024", "instance-type=c7.peculiar"])
363
364 c4 = cs.parse([])
365 c4.update(c2)
366 self.assertEquals(c4["arch"], None)
367 self.assertEquals(c4["cpu"], 0.0)
368 self.assertEquals(c4["mem"], 8192.0)
369 self.assertEquals(c4["instance-type"], None)
370
371 c5 = cs.parse(["instance-type=a1.big"])
372 c5.update(cs.parse(["arch=i386"]))
373 self.assertEquals(c5["arch"], "i386")
374 self.assertEquals(c5["cpu"], None)
375 self.assertEquals(c5["mem"], None)
376 self.assertEquals(c5["instance-type"], "a1.big")
377
378 c6 = cs.parse(["instance-type=a1.big"])
379 c6.update(cs.parse(["cpu=20"]))
380 self.assertEquals(c6["arch"], "amd64")
381 self.assertEquals(c6["cpu"], 20.0)
382 self.assertEquals(c6["mem"], None)
383 self.assertEquals(c6["instance-type"], None)
384
385 c7 = cs.parse(["instance-type="])
386 self.assertEquals(c7["arch"], "amd64")
387 self.assertEquals(c7["cpu"], 1.0)
388 self.assertEquals(c7["mem"], 512.0)
389 self.assertEquals(c7["instance-type"], None)
390
391 c8 = cs.parse(["instance-type=any"])
392 self.assertEquals(c8["arch"], "amd64")
393 self.assertEquals(c8["cpu"], 1.0)
394 self.assertEquals(c8["mem"], 512.0)
395 self.assertEquals(c8["instance-type"], None)
396
397 def test_load_validates(self):
398 self.allow_names("foo")
399 cs = ConstraintSet("provider")
400
401 def blam(s):
402 raise ValueError(s)
403
404 cs.register("foo", converter=blam)
405 e = self.assertRaises(ConstraintError, cs.load, {"foo": "bar"})
406 self.assertEquals(str(e), "Bad 'foo' constraint 'bar': bar")
366407
=== modified file 'juju/machine/unit.py'
--- juju/machine/unit.py 2012-02-21 12:14:11 +0000
+++ juju/machine/unit.py 2012-03-30 16:56:32 +0000
@@ -1,6 +1,5 @@
1import os1import os
2import shutil2import shutil
3import sys
4import logging3import logging
54
6import juju5import juju
76
=== modified file 'juju/providers/common/base.py'
--- juju/providers/common/base.py 2011-11-19 05:37:09 +0000
+++ juju/providers/common/base.py 2012-03-30 16:56:32 +0000
@@ -1,9 +1,10 @@
1import copy1import copy
2from operator import itemgetter2from operator import itemgetter
33
4from twisted.internet.defer import inlineCallbacks, returnValue4from twisted.internet.defer import inlineCallbacks, returnValue, succeed
55
6from juju.environment.errors import EnvironmentsConfigError6from juju.environment.errors import EnvironmentsConfigError
7from juju.machine.constraints import ConstraintSet
7from juju.state.placement import UNASSIGNED_POLICY8from juju.state.placement import UNASSIGNED_POLICY
89
910
@@ -30,10 +31,13 @@
3031
31 You may want to override the following methods, but you should be careful32 You may want to override the following methods, but you should be careful
32 to call :class:`MachineProviderBase`'s implementation (or be very sure you33 to call :class:`MachineProviderBase`'s implementation (or be very sure you
33 don't need to:34 don't need to):
3435
35 * :meth:`__init__`36 * :meth:`__init__`
36 * :meth:`get_serialization_data`37 * :meth:`get_serialization_data`
38 * :meth:`get_legacy_config_keys`
39 * :meth:`get_placement_policy`
40 * :meth:`get_constraint_set`
3741
38 You probably shouldn't override anything else.42 You probably shouldn't override anything else.
39 """43 """
@@ -48,11 +52,16 @@
48 self.environment_name = environment_name52 self.environment_name = environment_name
49 self.config = config53 self.config = config
5054
55 def get_constraint_set(self):
56 """Return the set of constraints that are valid for this provider."""
57 return succeed(ConstraintSet(self.provider_type))
58
59 def get_legacy_config_keys(self):
60 """Return any deprecated config keys that are set."""
61 return set() & set(self.config)
62
51 def get_placement_policy(self):63 def get_placement_policy(self):
52 """Get the unit placement policy for the provider.64 """Get the unit placement policy for the provider."""
53
54 :param preference: A user specified plcaement policy preference
55 """
56 return self.config.get("placement", UNASSIGNED_POLICY)65 return self.config.get("placement", UNASSIGNED_POLICY)
5766
58 def get_serialization_data(self):67 def get_serialization_data(self):
@@ -151,9 +160,9 @@
151 """160 """
152 return ZookeeperConnect(self).run(share=share)161 return ZookeeperConnect(self).run(share=share)
153162
154 def bootstrap(self):163 def bootstrap(self, constraints):
155 """Bootstrap an juju server in the provider."""164 """Bootstrap an juju server in the provider."""
156 return Bootstrap(self).run()165 return Bootstrap(self, constraints).run()
157166
158 def get_machine(self, instance_id):167 def get_machine(self, instance_id):
159 """Retrieve a provider machine by instance id.168 """Retrieve a provider machine by instance id.
160169
=== modified file 'juju/providers/common/bootstrap.py'
--- juju/providers/common/bootstrap.py 2011-09-22 13:23:00 +0000
+++ juju/providers/common/bootstrap.py 2012-03-30 16:56:32 +0000
@@ -1,5 +1,7 @@
1from cStringIO import StringIO1from cStringIO import StringIO
22
3from twisted.internet.defer import inlineCallbacks, returnValue
4
3from juju.errors import EnvironmentNotFound, ProviderError5from juju.errors import EnvironmentNotFound, ProviderError
46
5from .utils import log7from .utils import log
@@ -10,8 +12,9 @@
10class Bootstrap(object):12class Bootstrap(object):
11 """Generic bootstrap operation class."""13 """Generic bootstrap operation class."""
1214
13 def __init__(self, provider):15 def __init__(self, provider, constraints):
14 self._provider = provider16 self._provider = provider
17 self._constraints = constraints
1518
16 def run(self):19 def run(self):
17 """Get an existing zookeeper, or launch a new one.20 """Get an existing zookeeper, or launch a new one.
@@ -47,6 +50,9 @@
47 "Bootstrap aborted because file storage is not writable: %s"50 "Bootstrap aborted because file storage is not writable: %s"
48 % str(failure.value))51 % str(failure.value))
4952
53 @inlineCallbacks
50 def _launch_machine(self, unused):54 def _launch_machine(self, unused):
51 log.debug("Launching juju bootstrap instance.")55 log.debug("Launching juju bootstrap instance.")
52 return self._provider.start_machine({"machine-id": "0"}, master=True)56 machines = yield self._provider.start_machine(
57 {"machine-id": "0", "constraints": self._constraints}, master=True)
58 returnValue(machines)
5359
=== modified file 'juju/providers/common/cloudinit.py'
--- juju/providers/common/cloudinit.py 2012-02-21 19:28:28 +0000
+++ juju/providers/common/cloudinit.py 2012-03-30 16:56:32 +0000
@@ -1,4 +1,6 @@
1from base64 import b64encode
1from subprocess import Popen, PIPE2from subprocess import Popen, PIPE
3from yaml import safe_dump
24
3from juju.errors import CloudInitError5from juju.errors import CloudInitError
4from juju.lib.upstart import UpstartService6from juju.lib.upstart import UpstartService
@@ -29,14 +31,15 @@
29 return scripts31 return scripts
3032
3133
32def _zookeeper_scripts(instance_id, secret, provider_type):34def _zookeeper_scripts(instance_id, secret, constraints, provider_type):
33 return [35 return [
34 "juju-admin initialize"36 "juju-admin initialize"
35 " --instance-id=%s"37 " --instance-id=%s"
36 " --admin-identity=%s"38 " --admin-identity=%s"
39 " --constraints-data=%s"
37 " --provider-type=%s"40 " --provider-type=%s"
38 % (instance_id, make_identity("admin:%s" % secret),41 % (instance_id, make_identity("admin:%s" % secret),
39 provider_type)]42 b64encode(safe_dump(constraints.data)), provider_type)]
4043
4144
42def _machine_scripts(machine_id, zookeeper_hosts):45def _machine_scripts(machine_id, zookeeper_hosts):
@@ -143,6 +146,7 @@
143 self._zookeeper = False146 self._zookeeper = False
144 self._zookeeper_hosts = []147 self._zookeeper_hosts = []
145 self._zookeeper_secret = None148 self._zookeeper_secret = None
149 self._constraints = None
146 self._origin, self._origin_url = get_default_origin()150 self._origin, self._origin_url = get_default_origin()
147151
148 def add_ssh_key(self, key):152 def add_ssh_key(self, key):
@@ -236,6 +240,13 @@
236 """240 """
237 self._zookeeper_secret = secret241 self._zookeeper_secret = secret
238242
243 def set_constraints(self, constraints):
244 """Specify the initial machine's constraints.
245
246 You only need to set this if this machine will be a zookeeper instance.
247 """
248 self._constraints = constraints
249
239 def render(self):250 def render(self):
240 """Get content for a cloud-init file with appropriate specifications.251 """Get content for a cloud-init file with appropriate specifications.
241252
@@ -265,6 +276,7 @@
265 if self._zookeeper:276 if self._zookeeper:
266 require("_instance_id", "set_instance_id_accessor")277 require("_instance_id", "set_instance_id_accessor")
267 require("_zookeeper_secret", "set_zookeeper_secret")278 require("_zookeeper_secret", "set_zookeeper_secret")
279 require("_constraints", "set_constraints")
268 else:280 else:
269 require("_zookeeper_hosts", "set_zookeeper_machines")281 require("_zookeeper_hosts", "set_zookeeper_machines")
270 if missing:282 if missing:
@@ -299,6 +311,7 @@
299 scripts.extend(_zookeeper_scripts(311 scripts.extend(_zookeeper_scripts(
300 self._instance_id,312 self._instance_id,
301 self._zookeeper_secret,313 self._zookeeper_secret,
314 self._constraints,
302 self._provider_type))315 self._provider_type))
303 scripts.extend(_machine_scripts(316 scripts.extend(_machine_scripts(
304 self._machine_id, self._join_zookeeper_hosts()))317 self._machine_id, self._join_zookeeper_hosts()))
305318
=== modified file 'juju/providers/common/launch.py'
--- juju/providers/common/launch.py 2011-09-30 04:52:20 +0000
+++ juju/providers/common/launch.py 2012-03-30 16:56:32 +0000
@@ -1,4 +1,6 @@
1from twisted.internet.defer import inlineCallbacks, returnValue1from twisted.internet.defer import fail, inlineCallbacks, returnValue
2
3from juju.errors import ProviderError
24
3from .cloudinit import CloudInit5from .cloudinit import CloudInit
4from .utils import get_user_authorized_keys6from .utils import get_user_authorized_keys
@@ -24,10 +26,27 @@
24 .. automethod:: _create_cloud_init26 .. automethod:: _create_cloud_init
25 """27 """
2628
27 def __init__(self, provider, master=False, constraints=None):29 def __init__(self, provider, constraints, master=False):
28 self._provider = provider30 self._provider = provider
31 self._constraints = constraints
29 self._master = master32 self._master = master
30 self._constraints = constraints or {}33
34 @classmethod
35 def launch(cls, provider, machine_data, master):
36 """Create and run a machine launch operation.
37
38 Exists for the convenience of the `MachineProvider` implementations
39 which actually use the "constraints" key in machine_data, which would
40 otherwise duplicate code.
41 """
42 if "machine-id" not in machine_data:
43 return fail(ProviderError(
44 "Cannot launch a machine without specifying a machine-id"))
45 if "constraints" not in machine_data:
46 return fail(ProviderError(
47 "Cannot launch a machine without specifying constraints"))
48 launcher = cls(provider, machine_data["constraints"], master)
49 return launcher.run(machine_data["machine-id"])
3150
32 @inlineCallbacks51 @inlineCallbacks
33 def run(self, machine_id):52 def run(self, machine_id):
@@ -88,6 +107,7 @@
88 if self._master:107 if self._master:
89 cloud_init.enable_bootstrap()108 cloud_init.enable_bootstrap()
90 cloud_init.set_zookeeper_secret(config["admin-secret"])109 cloud_init.set_zookeeper_secret(config["admin-secret"])
110 cloud_init.set_constraints(self._constraints)
91 return cloud_init111 return cloud_init
92112
93 def _on_new_zookeepers(self, machines):113 def _on_new_zookeepers(self, machines):
94114
=== modified file 'juju/providers/common/tests/data/cloud_init_bootstrap'
--- juju/providers/common/tests/data/cloud_init_bootstrap 2012-02-21 19:28:28 +0000
+++ juju/providers/common/tests/data/cloud_init_bootstrap 2012-03-30 16:56:32 +0000
@@ -6,8 +6,8 @@
6output: {all: '| tee -a /var/log/cloud-init-output.log'}6output: {all: '| tee -a /var/log/cloud-init-output.log'}
7packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws, python-zookeeper,7packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws, python-zookeeper,
8 default-jre-headless, zookeeper, zookeeperd, juju]8 default-jre-headless, zookeeper, zookeeperd, juju]
9runcmd: [sudo mkdir -p /var/lib/juju, sudo mkdir -p9runcmd: [sudo mkdir -p /var/lib/juju, sudo mkdir -p /var/log/juju, 'juju-admin initialize
10 /var/log/juju, 'juju-admin initialize --instance-id=token --admin-identity=admin:19vlzY4Vc3q4Ew5OsCwKYqrq1HI=10 --instance-id=token --admin-identity=admin:19vlzY4Vc3q4Ew5OsCwKYqrq1HI= --constraints-data=e2NwdTogJzIwJywgcHJvdmlkZXItdHlwZTogZHVtbXksIHVidW50dS1zZXJpZXM6IGFzdG9uaXNoaW5nfQo=
11 --provider-type=dummy', 'cat >> /etc/init/juju-machine-agent.conf <<EOF11 --provider-type=dummy', 'cat >> /etc/init/juju-machine-agent.conf <<EOF
1212
13 description "Juju machine agent"13 description "Juju machine agent"
1414
=== modified file 'juju/providers/common/tests/data/cloud_init_bootstrap_zookeepers'
--- juju/providers/common/tests/data/cloud_init_bootstrap_zookeepers 2012-02-21 19:28:28 +0000
+++ juju/providers/common/tests/data/cloud_init_bootstrap_zookeepers 2012-03-30 16:56:32 +0000
@@ -6,8 +6,8 @@
6output: {all: '| tee -a /var/log/cloud-init-output.log'}6output: {all: '| tee -a /var/log/cloud-init-output.log'}
7packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws, python-zookeeper,7packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws, python-zookeeper,
8 default-jre-headless, zookeeper, zookeeperd, juju]8 default-jre-headless, zookeeper, zookeeperd, juju]
9runcmd: [sudo mkdir -p /var/lib/juju, sudo mkdir -p9runcmd: [sudo mkdir -p /var/lib/juju, sudo mkdir -p /var/log/juju, 'juju-admin initialize
10 /var/log/juju, 'juju-admin initialize --instance-id=token --admin-identity=admin:19vlzY4Vc3q4Ew5OsCwKYqrq1HI=10 --instance-id=token --admin-identity=admin:19vlzY4Vc3q4Ew5OsCwKYqrq1HI= --constraints-data=e2NwdTogJzIwJywgcHJvdmlkZXItdHlwZTogZHVtbXksIHVidW50dS1zZXJpZXM6IGFzdG9uaXNoaW5nfQo=
11 --provider-type=dummy', 'cat >> /etc/init/juju-machine-agent.conf <<EOF11 --provider-type=dummy', 'cat >> /etc/init/juju-machine-agent.conf <<EOF
1212
13 description "Juju machine agent"13 description "Juju machine agent"
1414
=== modified file 'juju/providers/common/tests/data/cloud_init_distro'
--- juju/providers/common/tests/data/cloud_init_distro 2012-02-21 19:28:28 +0000
+++ juju/providers/common/tests/data/cloud_init_distro 2012-03-30 16:56:32 +0000
@@ -4,9 +4,10 @@
4machine-data: {juju-provider-type: dummy, juju-zookeeper-hosts: 'cotswold:2181,longleat:2181',4machine-data: {juju-provider-type: dummy, juju-zookeeper-hosts: 'cotswold:2181,longleat:2181',
5 machine-id: passport}5 machine-id: passport}
6output: {all: '| tee -a /var/log/cloud-init-output.log'}6output: {all: '| tee -a /var/log/cloud-init-output.log'}
7packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws, python-zookeeper, juju]7packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws, python-zookeeper,
8runcmd: [sudo mkdir -p /var/lib/juju, sudo mkdir -p8 juju]
9 /var/log/juju, 'cat >> /etc/init/juju-machine-agent.conf <<EOF9runcmd: [sudo mkdir -p /var/lib/juju, sudo mkdir -p /var/log/juju, 'cat >> /etc/init/juju-machine-agent.conf
10 <<EOF
1011
11 description "Juju machine agent"12 description "Juju machine agent"
1213
1314
=== modified file 'juju/providers/common/tests/data/cloud_init_ppa'
--- juju/providers/common/tests/data/cloud_init_ppa 2012-02-21 19:28:28 +0000
+++ juju/providers/common/tests/data/cloud_init_ppa 2012-03-30 16:56:32 +0000
@@ -6,9 +6,10 @@
6machine-data: {juju-provider-type: dummy, juju-zookeeper-hosts: 'cotswold:2181,longleat:2181',6machine-data: {juju-provider-type: dummy, juju-zookeeper-hosts: 'cotswold:2181,longleat:2181',
7 machine-id: passport}7 machine-id: passport}
8output: {all: '| tee -a /var/log/cloud-init-output.log'}8output: {all: '| tee -a /var/log/cloud-init-output.log'}
9packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws, python-zookeeper, juju]9packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws, python-zookeeper,
10runcmd: [sudo mkdir -p /var/lib/juju, sudo mkdir -p10 juju]
11 /var/log/juju, 'cat >> /etc/init/juju-machine-agent.conf <<EOF11runcmd: [sudo mkdir -p /var/lib/juju, sudo mkdir -p /var/log/juju, 'cat >> /etc/init/juju-machine-agent.conf
12 <<EOF
1213
13 description "Juju machine agent"14 description "Juju machine agent"
1415
1516
=== modified file 'juju/providers/common/tests/test_base.py'
--- juju/providers/common/tests/test_base.py 2011-09-27 02:14:56 +0000
+++ juju/providers/common/tests/test_base.py 2012-03-30 16:56:32 +0000
@@ -63,6 +63,10 @@
63 self.assertEqual(63 self.assertEqual(
64 provider.get_placement_policy(), "local")64 provider.get_placement_policy(), "local")
6565
66 def test_get_legacy_config_keys(self):
67 provider = DummyProvider()
68 self.assertEqual(provider.get_legacy_config_keys(), set())
69
66 def test_get_machine_error(self):70 def test_get_machine_error(self):
67 provider = DummyProvider()71 provider = DummyProvider()
68 provider.get_machines = self.mocker.mock()72 provider.get_machines = self.mocker.mock()
6973
=== modified file 'juju/providers/common/tests/test_bootstrap.py'
--- juju/providers/common/tests/test_bootstrap.py 2011-09-15 18:50:23 +0000
+++ juju/providers/common/tests/test_bootstrap.py 2012-03-30 16:56:32 +0000
@@ -1,10 +1,11 @@
1import logging1import logging
2import tempfile2import tempfile
33
4from twisted.internet.defer import fail, succeed4from twisted.internet.defer import fail, succeed, inlineCallbacks
55
6from juju.errors import EnvironmentNotFound, ProviderError6from juju.errors import EnvironmentNotFound, ProviderError
7from juju.lib.testing import TestCase7from juju.lib.testing import TestCase
8from juju.machine.tests.test_constraints import dummy_cs
8from juju.providers.common.base import MachineProviderBase9from juju.providers.common.base import MachineProviderBase
9from juju.providers.dummy import DummyMachine, FileStorage10from juju.providers.dummy import DummyMachine, FileStorage
1011
@@ -27,7 +28,11 @@
2728
28class DummyProvider(MachineProviderBase):29class DummyProvider(MachineProviderBase):
2930
30 def __init__(self, file_storage, zookeeper):31 provider_type = "dummy"
32 config = {"default-series": "splendid"}
33
34 def __init__(self, test, file_storage, zookeeper):
35 self._test = test
31 self._file_storage = file_storage36 self._file_storage = file_storage
32 self._zookeeper = zookeeper37 self._zookeeper = zookeeper
3338
@@ -41,24 +46,34 @@
41 return succeed([self._zookeeper])46 return succeed([self._zookeeper])
42 return fail(EnvironmentNotFound())47 return fail(EnvironmentNotFound())
4348
44 def start_machine(self, machine_id, master=False):49 def start_machine(self, machine_data, master=False):
45 assert master is True50 self._test.assertTrue(master)
51 self._test.assertEquals(machine_data["machine-id"], "0")
52 constraints = machine_data["constraints"]
53 self._test.assertEquals(constraints["provider-type"], "dummy")
54 self._test.assertEquals(constraints["ubuntu-series"], "splendid")
55 self._test.assertEquals(constraints["arch"], "arm")
46 return [DummyMachine("i-keepzoos")]56 return [DummyMachine("i-keepzoos")]
4757
4858
49class BootstrapTest(TestCase):59class BootstrapTest(TestCase):
5060
61 @inlineCallbacks
62 def setUp(self):
63 yield super(BootstrapTest, self).setUp()
64 self.constraints = dummy_cs.parse(["arch=arm"]).with_series("splendid")
65
51 def test_unknown_error(self):66 def test_unknown_error(self):
52 provider = DummyProvider(None, SomeError())67 provider = DummyProvider(self, None, SomeError())
53 d = provider.bootstrap()68 d = provider.bootstrap(self.constraints)
54 self.assertFailure(d, SomeError)69 self.assertFailure(d, SomeError)
55 return d70 return d
5671
57 def test_zookeeper_exists(self):72 def test_zookeeper_exists(self):
58 log = self.capture_logging("juju.common", level=logging.DEBUG)73 log = self.capture_logging("juju.common", level=logging.DEBUG)
59 provider = DummyProvider(74 provider = DummyProvider(
60 WorkingFileStorage(), DummyMachine("i-alreadykeepzoos"))75 self, WorkingFileStorage(), DummyMachine("i-alreadykeepzoos"))
61 d = provider.bootstrap()76 d = provider.bootstrap(self.constraints)
6277
63 def verify(machines):78 def verify(machines):
64 (machine,) = machines79 (machine,) = machines
@@ -73,8 +88,8 @@
73 return d88 return d
7489
75 def test_bad_storage(self):90 def test_bad_storage(self):
76 provider = DummyProvider(UnwritableFileStorage(), None)91 provider = DummyProvider(self, UnwritableFileStorage(), None)
77 d = provider.bootstrap()92 d = provider.bootstrap(self.constraints)
78 self.assertFailure(d, ProviderError)93 self.assertFailure(d, ProviderError)
7994
80 def verify(error):95 def verify(error):
@@ -87,8 +102,8 @@
87102
88 def test_create_zookeeper(self):103 def test_create_zookeeper(self):
89 log = self.capture_logging("juju.common", level=logging.DEBUG)104 log = self.capture_logging("juju.common", level=logging.DEBUG)
90 provider = DummyProvider(WorkingFileStorage(), None)105 provider = DummyProvider(self, WorkingFileStorage(), None)
91 d = provider.bootstrap()106 d = provider.bootstrap(self.constraints)
92107
93 def verify(machines):108 def verify(machines):
94 (machine,) = machines109 (machine,) = machines
95110
=== modified file 'juju/providers/common/tests/test_cloudinit.py'
--- juju/providers/common/tests/test_cloudinit.py 2012-02-22 09:23:16 +0000
+++ juju/providers/common/tests/test_cloudinit.py 2012-03-30 16:56:32 +0000
@@ -5,6 +5,7 @@
55
6from juju.errors import CloudInitError6from juju.errors import CloudInitError
7from juju.lib.testing import TestCase7from juju.lib.testing import TestCase
8from juju.machine.tests.test_constraints import dummy_cs
8from juju.providers.common.cloudinit import (9from juju.providers.common.cloudinit import (
9 CloudInit, parse_juju_origin, get_default_origin)10 CloudInit, parse_juju_origin, get_default_origin)
10from juju.providers.dummy import DummyMachine11from juju.providers.dummy import DummyMachine
@@ -34,6 +35,8 @@
34 cloud_init.set_provider_type("dummy")35 cloud_init.set_provider_type("dummy")
35 cloud_init.set_instance_id_accessor("token")36 cloud_init.set_instance_id_accessor("token")
36 cloud_init.set_zookeeper_secret("seekrit")37 cloud_init.set_zookeeper_secret("seekrit")
38 cloud_init.set_constraints(
39 dummy_cs.parse(["cpu=20"]).with_series("astonishing"))
37 cloud_init.set_juju_source(distro=True)40 cloud_init.set_juju_source(distro=True)
38 if with_zookeepers:41 if with_zookeepers:
39 cloud_init.set_zookeeper_machines([42 cloud_init.set_zookeeper_machines([
@@ -64,7 +67,7 @@
64 str(error),67 str(error),
65 "Incomplete cloud-init: you need to call add_ssh_key, "68 "Incomplete cloud-init: you need to call add_ssh_key, "
66 "set_machine_id, set_provider_type, set_instance_id_accessor, "69 "set_machine_id, set_provider_type, set_instance_id_accessor, "
67 "set_zookeeper_secret")70 "set_zookeeper_secret, set_constraints")
6871
69 def test_source_validate(self):72 def test_source_validate(self):
70 bad_choices = (73 bad_choices = (
7174
=== modified file 'juju/providers/common/tests/test_launch.py'
--- juju/providers/common/tests/test_launch.py 2011-09-19 23:33:37 +0000
+++ juju/providers/common/tests/test_launch.py 2012-03-30 16:56:32 +0000
@@ -1,6 +1,6 @@
1import tempfile1import tempfile
22
3from twisted.internet.defer import fail, succeed3from twisted.internet.defer import fail, succeed, inlineCallbacks, returnValue
44
5from juju.errors import EnvironmentNotFound5from juju.errors import EnvironmentNotFound
6from juju.lib.testing import TestCase6from juju.lib.testing import TestCase
@@ -9,10 +9,12 @@
9from juju.providers.dummy import DummyMachine, FileStorage9from juju.providers.dummy import DummyMachine, FileStorage
1010
1111
12def launch_machine(test, master, zookeeper):12def get_classes(test, master, machine_id, zookeeper):
1313
14 class DummyProvider(MachineProviderBase):14 class DummyProvider(MachineProviderBase):
1515
16 provider_type = "dummy"
17
16 def __init__(self):18 def __init__(self):
17 self.config = {"admin-secret": "BLAH"}19 self.config = {"admin-secret": "BLAH"}
18 self._file_storage = FileStorage(tempfile.mkdtemp())20 self._file_storage = FileStorage(tempfile.mkdtemp())
@@ -27,54 +29,73 @@
2729
28 class DummyLaunchMachine(LaunchMachine):30 class DummyLaunchMachine(LaunchMachine):
2931
30 def start_machine(self, machine_id, zookeepers):32 def start_machine(self, actual_machine_id, zookeepers):
31 test.assertEquals(machine_id, "1234")33 test.assertEquals(actual_machine_id, machine_id)
32 test.assertEquals(zookeepers, filter(None, [zookeeper]))34 test.assertEquals(zookeepers, filter(None, [zookeeper]))
33 test.assertEquals(self._master, master)35 test.assertEquals(self._master, master)
34 test.assertEquals(self._constraints, {})36 test.assertEquals(self._constraints, self.expect_constraints)
35 return succeed([DummyMachine("i-malive")])37 return succeed([DummyMachine("i-malive")])
3638
39 return DummyProvider, DummyLaunchMachine
40
41
42@inlineCallbacks
43def launch_machine(test, master, zookeeper):
44 DummyProvider, DummyLaunchMachine = get_classes(
45 test, master, "1234", zookeeper)
46
37 provider = DummyProvider()47 provider = DummyProvider()
38 d = DummyLaunchMachine(provider, master).run("1234")48 cs = yield provider.get_constraint_set()
39 return provider, d49 constraints = cs.parse([])
50 launch = DummyLaunchMachine(provider, constraints, master)
51 launch.expect_constraints = constraints
52 machines = yield launch.run("1234")
53 returnValue((provider, machines))
4054
4155
42class LaunchMachineTest(TestCase):56class LaunchMachineTest(TestCase):
4357
58 @inlineCallbacks
44 def assert_success(self, master, zookeeper):59 def assert_success(self, master, zookeeper):
45 provider, d = launch_machine(self, master, zookeeper)60 provider, machines = yield launch_machine(self, master, zookeeper)
4661 (machine,) = machines
47 def verify(machines):62 self.assertTrue(isinstance(machine, DummyMachine))
48 (machine,) = machines63 self.assertEquals(machine.instance_id, "i-malive")
49 self.assertTrue(isinstance(machine, DummyMachine))64 returnValue(provider)
50 self.assertEquals(machine.instance_id, "i-malive")
51 return provider
52 d.addCallback(verify)
53 return d
5465
55 def test_start_nonzookeeper_no_zookeepers(self):66 def test_start_nonzookeeper_no_zookeepers(self):
56 """Starting a non-zookeeper without a zookeeper is an error"""67 """Starting a non-zookeeper without a zookeeper is an error"""
57 unused, d = launch_machine(self, False, None)68 return self.assertFailure(
58 self.assertFailure(d, EnvironmentNotFound)69 launch_machine(self, False, None), EnvironmentNotFound)
59 return d
6070
71 @inlineCallbacks
61 def test_start_zookeeper_no_zookeepers(self):72 def test_start_zookeeper_no_zookeepers(self):
62 """A new zookeeper should be recorded in provider state"""73 """A new zookeeper should be recorded in provider state"""
63 d = self.assert_success(True, None)74 provider = yield self.assert_success(True, None)
6475 provider_state = yield provider.load_state()
65 def verify(provider):76 self.assertEquals(
66 provider_state = yield provider.load_state()77 provider_state, {"zookeeper-instances": ["i-malive"]})
67 self.assertEquals(78
68 provider_state, {"zookeeper-instances": ["i-malive"]})79 @inlineCallbacks
69 d.addCallback(verify)
70 return d
71
72 def test_works_with_zookeeper(self):80 def test_works_with_zookeeper(self):
73 """Creating a non-zookeeper machine should not alter provider state"""81 """Creating a non-zookeeper machine should not alter provider state"""
74 d = self.assert_success(False, DummyMachine("i-keepzoos"))82 provider = yield self.assert_success(False, DummyMachine("i-keepzoos"))
7583 provider_state = yield provider.load_state()
76 def verify(provider):84 self.assertEquals(provider_state, False)
77 provider_state = yield provider.load_state()85
78 self.assertEquals(provider_state, False)86 @inlineCallbacks
79 d.addCallback(verify)87 def test_convenience_launch(self):
80 return d88 DummyProvider, DummyLaunchMachine = get_classes(
89 self, True, "999", None)
90
91 provider = DummyProvider()
92 cs = yield provider.get_constraint_set()
93 constraints = cs.parse(["arch=arm", "mem=1G"])
94 DummyLaunchMachine.expect_constraints = constraints
95 machine_data = {
96 "machine-id": "999", "constraints": constraints}
97 machines = yield DummyLaunchMachine.launch(
98 provider, machine_data, True)
99 (machine,) = machines
100 self.assertTrue(isinstance(machine, DummyMachine))
101 self.assertEquals(machine.instance_id, "i-malive")
81102
=== modified file 'juju/providers/dummy.py'
--- juju/providers/dummy.py 2011-09-28 09:48:30 +0000
+++ juju/providers/dummy.py 2012-03-30 16:56:32 +0000
@@ -11,6 +11,7 @@
1111
1212
13from juju.machine import ProviderMachine13from juju.machine import ProviderMachine
14from juju.machine.constraints import ConstraintSet
14from juju.state.placement import UNASSIGNED_POLICY15from juju.state.placement import UNASSIGNED_POLICY
15from juju.providers.common.files import FileStorage16from juju.providers.common.files import FileStorage
1617
@@ -34,6 +35,9 @@
34 self._state = None35 self._state = None
35 self._storage = None36 self._storage = None
3637
38 def get_legacy_config_keys(self):
39 return set(("some-legacy-key",)) & set(self.config)
40
37 def get_placement_policy(self):41 def get_placement_policy(self):
38 """Get the unit placement policy for the provider.42 """Get the unit placement policy for the provider.
3943
@@ -41,6 +45,11 @@
41 """45 """
42 return self.config.get("placement", UNASSIGNED_POLICY)46 return self.config.get("placement", UNASSIGNED_POLICY)
4347
48 def get_constraint_set(self):
49 cs = ConstraintSet(self.provider_type)
50 cs.register_generics([])
51 return succeed(cs)
52
44 @property53 @property
45 def provider_type(self):54 def provider_type(self):
46 return "dummy"55 return "dummy"
@@ -91,7 +100,7 @@
91 return succeed(machine)100 return succeed(machine)
92 return fail(MachinesNotFound([instance_id]))101 return fail(MachinesNotFound([instance_id]))
93102
94 def bootstrap(self):103 def bootstrap(self, constraints):
95 """104 """
96 Bootstrap juju on the machine provider.105 Bootstrap juju on the machine provider.
97 """106 """
98107
=== modified file 'juju/providers/ec2/__init__.py'
--- juju/providers/ec2/__init__.py 2011-09-19 17:17:00 +0000
+++ juju/providers/ec2/__init__.py 2012-03-30 16:56:32 +0000
@@ -1,7 +1,7 @@
1import os1import os
2import re2import re
33
4from twisted.internet.defer import fail, inlineCallbacks, returnValue4from twisted.internet.defer import inlineCallbacks, returnValue
55
6from txaws.ec2.exception import EC2Error6from txaws.ec2.exception import EC2Error
7from txaws.service import AWSServiceRegion7from txaws.service import AWSServiceRegion
@@ -16,7 +16,7 @@
16from .securitygroup import (16from .securitygroup import (
17 open_provider_port, close_provider_port, get_provider_opened_ports,17 open_provider_port, close_provider_port, get_provider_opened_ports,
18 remove_security_groups, destroy_environment_security_group)18 remove_security_groups, destroy_environment_security_group)
19from .utils import get_region_uri19from .utils import convert_zone, get_region_uri, DEFAULT_REGION, INSTANCE_TYPES
2020
2121
22class MachineProvider(MachineProviderBase):22class MachineProvider(MachineProviderBase):
@@ -26,7 +26,7 @@
26 super(MachineProvider, self).__init__(environment_name, config)26 super(MachineProvider, self).__init__(environment_name, config)
2727
28 if not config.get("ec2-uri"):28 if not config.get("ec2-uri"):
29 ec2_uri = get_region_uri(config.get("region", "us-east-1"))29 ec2_uri = get_region_uri(config.get("region", DEFAULT_REGION))
30 else:30 else:
31 ec2_uri = config.get("ec2-uri")31 ec2_uri = config.get("ec2-uri")
3232
@@ -42,6 +42,30 @@
42 def provider_type(self):42 def provider_type(self):
43 return "ec2"43 return "ec2"
4444
45 @property
46 def using_amazon(self):
47 return "ec2-uri" not in self.config
48
49 @inlineCallbacks
50 def get_constraint_set(self):
51 """Return the set of constraints that are valid for this provider."""
52 cs = yield super(MachineProvider, self).get_constraint_set()
53 if self.using_amazon:
54 # Expose EC2 instance types/zones on AWS itelf, not private clouds.
55 cs.register_generics(INSTANCE_TYPES.keys())
56 cs.register("ec2-zone", converter=convert_zone)
57 returnValue(cs)
58
59 def get_legacy_config_keys(self):
60 """Return any deprecated config keys that are set"""
61 legacy = super(MachineProvider, self).get_legacy_config_keys()
62 if self.using_amazon:
63 # In the absence of a generic instance-type/image-id mechanism,
64 # these keys remain valid on private clouds.
65 amazon_legacy = set(("default-image-id", "default-instance-type"))
66 legacy.update(amazon_legacy.intersection(self.config))
67 return legacy
68
45 def get_serialization_data(self):69 def get_serialization_data(self):
46 """Get provider configuration suitable for serialization.70 """Get provider configuration suitable for serialization.
4771
@@ -67,12 +91,7 @@
67 and run a provisioning agent, in addition to running a machine91 and run a provisioning agent, in addition to running a machine
68 agent.92 agent.
69 """93 """
70 if "machine-id" not in machine_data:94 return EC2LaunchMachine.launch(self, machine_data, master)
71 return fail(ProviderError(
72 "Cannot launch a machine without specifying a machine-id"))
73 machine_id = machine_data["machine-id"]
74 constraints = machine_data.get("constraints", {})
75 return EC2LaunchMachine(self, master, constraints).run(machine_id)
7695
77 @inlineCallbacks96 @inlineCallbacks
78 def get_machines(self, instance_ids=()):97 def get_machines(self, instance_ids=()):
7998
=== modified file 'juju/providers/ec2/launch.py'
--- juju/providers/ec2/launch.py 2011-09-19 20:38:45 +0000
+++ juju/providers/ec2/launch.py 2012-03-30 16:56:32 +0000
@@ -6,7 +6,7 @@
6from juju.providers.common.launch import LaunchMachine6from juju.providers.common.launch import LaunchMachine
77
8from .machine import machine_from_instance8from .machine import machine_from_instance
9from .utils import get_image_id, log9from .utils import get_machine_spec, log, DEFAULT_REGION
1010
1111
12class EC2LaunchMachine(LaunchMachine):12class EC2LaunchMachine(LaunchMachine):
@@ -34,17 +34,21 @@
34 "$(curl http://169.254.169.254/1.0/meta-data/instance-id)")34 "$(curl http://169.254.169.254/1.0/meta-data/instance-id)")
35 user_data = cloud_init.render()35 user_data = cloud_init.render()
3636
37 instance_type = self._provider.config.get(37 availability_zone = self._constraints["ec2-zone"]
38 "default-instance-type", "m1.small")38 if availability_zone is not None:
39 image_id = yield get_image_id(self._provider.config, self._constraints)39 region = self._provider.config.get("region", DEFAULT_REGION)
40 availability_zone = region + availability_zone
41 spec = yield get_machine_spec(self._provider.config, self._constraints)
40 security_groups = yield self._ensure_groups(machine_id)42 security_groups = yield self._ensure_groups(machine_id)
4143
44 log.debug("Launching with machine spec %s", spec)
42 instances = yield self._provider.ec2.run_instances(45 instances = yield self._provider.ec2.run_instances(
43 image_id=image_id,
44 instance_type=instance_type,
45 min_count=1,46 min_count=1,
46 max_count=1,47 max_count=1,
48 image_id=spec.image_id,
49 instance_type=spec.instance_type,
47 security_groups=security_groups,50 security_groups=security_groups,
51 availability_zone=availability_zone,
48 user_data=user_data)52 user_data=user_data)
4953
50 returnValue([machine_from_instance(i) for i in instances])54 returnValue([machine_from_instance(i) for i in instances])
5155
=== modified file 'juju/providers/ec2/tests/common.py'
--- juju/providers/ec2/tests/common.py 2011-09-29 05:35:04 +0000
+++ juju/providers/ec2/tests/common.py 2012-03-30 16:56:32 +0000
@@ -1,6 +1,6 @@
1from yaml import dump1from yaml import dump
22
3from twisted.internet.defer import fail, succeed3from twisted.internet.defer import fail, succeed, inlineCallbacks, returnValue
44
5from txaws.s3.client import S3Client5from txaws.s3.client import S3Client
6from txaws.s3.exception import S3Error6from txaws.s3.exception import S3Error
@@ -14,6 +14,13 @@
1414
15MATCH_GROUP = MATCH(lambda x: x.startswith("juju-moon"))15MATCH_GROUP = MATCH(lambda x: x.startswith("juju-moon"))
1616
17_constraints_provider = MachineProvider("", {})
18
19@inlineCallbacks
20def get_constraints(strs, series="splendid"):
21 cs = yield _constraints_provider.get_constraint_set()
22 returnValue(cs.parse(strs).with_series(series))
23
1724
18class EC2TestMixin(object):25class EC2TestMixin(object):
1926
@@ -26,7 +33,8 @@
26 "admin-secret": "magic-beans",33 "admin-secret": "magic-beans",
27 "access-key": "0f62e973d5f8",34 "access-key": "0f62e973d5f8",
28 "secret-key": "3e5a7c653f59",35 "secret-key": "3e5a7c653f59",
29 "control-bucket": self.env_name}36 "control-bucket": self.env_name,
37 "default-series": "splendid"}
3038
31 def get_provider(self):39 def get_provider(self):
32 """Return the ec2 machine provider.40 """Return the ec2 machine provider.
@@ -81,7 +89,7 @@
8189
82class EC2MachineLaunchMixin(object):90class EC2MachineLaunchMixin(object):
8391
84 def _mock_launch_utils(self, ami_name="ami-default", **get_ami_kwargs):92 def _mock_launch_utils(self, ami_name="ami-default", get_ami_args=()):
85 get_public_key = self.mocker.replace(93 get_public_key = self.mocker.replace(
86 "juju.providers.common.utils.get_user_authorized_keys")94 "juju.providers.common.utils.get_user_authorized_keys")
8795
@@ -91,16 +99,12 @@
91 get_public_key(MATCH(match_config))99 get_public_key(MATCH(match_config))
92 self.mocker.result("zebra")100 self.mocker.result("zebra")
93101
94 if not get_ami_kwargs:102 get_ami_args = get_ami_args or (
95 return103 "splendid", "amd64", "us-east-1", False)
96 get_ami = self.mocker.replace(104 get_ami = self.mocker.replace(
97 "juju.providers.ec2.utils.get_current_ami")105 "juju.providers.ec2.utils.get_current_ami")
98 get_ami(KWARGS)106 get_ami(*get_ami_args)
99107 self.mocker.result(succeed(ami_name))
100 def check_kwargs(**kwargs):
101 self.assertEquals(kwargs, get_ami_kwargs)
102 return succeed(ami_name)
103 self.mocker.call(check_kwargs)
104108
105 def _mock_create_group(self):109 def _mock_create_group(self):
106 group_name = "juju-%s" % self.env_name110 group_name = "juju-%s" % self.env_name
107111
=== modified file 'juju/providers/ec2/tests/data/bootstrap_cloud_init'
--- juju/providers/ec2/tests/data/bootstrap_cloud_init 2012-02-21 19:28:28 +0000
+++ juju/providers/ec2/tests/data/bootstrap_cloud_init 2012-03-30 16:56:32 +0000
@@ -5,10 +5,10 @@
5output: {all: '| tee -a /var/log/cloud-init-output.log'}5output: {all: '| tee -a /var/log/cloud-init-output.log'}
6packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws, python-zookeeper,6packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws, python-zookeeper,
7 default-jre-headless, zookeeper, zookeeperd, juju]7 default-jre-headless, zookeeper, zookeeperd, juju]
8runcmd: [sudo mkdir -p /var/lib/juju, sudo mkdir -p8runcmd: [sudo mkdir -p /var/lib/juju, sudo mkdir -p /var/log/juju, 'juju-admin initialize
9 /var/log/juju, 'juju-admin initialize --instance-id=$(curl http://169.254.169.254/1.0/meta-data/instance-id)9 --instance-id=$(curl http://169.254.169.254/1.0/meta-data/instance-id) --admin-identity=admin:JbJ6sDGV37EHzbG9FPvttk64cmg=
10 --admin-identity=admin:JbJ6sDGV37EHzbG9FPvttk64cmg= --provider-type=ec2', 'cat10 --constraints-data=e2NwdTogbnVsbCwgaW5zdGFuY2UtdHlwZTogbTEuc21hbGwsIG1lbTogbnVsbCwgcHJvdmlkZXItdHlwZTogZWMyLCB1YnVudHUtc2VyaWVzOiBzcGxlbmRpZH0K
11 >> /etc/init/juju-machine-agent.conf <<EOF11 --provider-type=ec2', 'cat >> /etc/init/juju-machine-agent.conf <<EOF
1212
13 description "Juju machine agent"13 description "Juju machine agent"
1414
1515
=== modified file 'juju/providers/ec2/tests/test_bootstrap.py'
--- juju/providers/ec2/tests/test_bootstrap.py 2011-09-30 04:52:20 +0000
+++ juju/providers/ec2/tests/test_bootstrap.py 2012-03-30 16:56:32 +0000
@@ -3,7 +3,7 @@
33
4import yaml4import yaml
55
6from twisted.internet.defer import succeed6from twisted.internet.defer import succeed, inlineCallbacks
77
8from txaws.ec2.model import SecurityGroup8from txaws.ec2.model import SecurityGroup
99
@@ -11,7 +11,7 @@
11from juju.lib.testing import TestCase11from juju.lib.testing import TestCase
12from juju.providers.ec2.machine import EC2ProviderMachine12from juju.providers.ec2.machine import EC2ProviderMachine
1313
14from .common import EC2TestMixin, EC2MachineLaunchMixin14from .common import EC2TestMixin, EC2MachineLaunchMixin, get_constraints
1515
1616
17DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "data")17DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "data")
@@ -50,8 +50,10 @@
50 max_count=1,50 max_count=1,
51 min_count=1,51 min_count=1,
52 security_groups=["juju-moon", "juju-moon-0"],52 security_groups=["juju-moon", "juju-moon-0"],
53 availability_zone=None,
53 user_data=MATCH(verify_user_data))54 user_data=MATCH(verify_user_data))
5455
56 @inlineCallbacks
55 def test_launch_bootstrap(self):57 def test_launch_bootstrap(self):
56 """The provider bootstrap can launch a bootstrap/zookeeper machine."""58 """The provider bootstrap can launch a bootstrap/zookeeper machine."""
5759
@@ -64,23 +66,20 @@
64 self.mocker.result(succeed([]))66 self.mocker.result(succeed([]))
65 self._mock_create_group()67 self._mock_create_group()
66 self._mock_create_machine_group(0)68 self._mock_create_machine_group(0)
67 self._mock_launch_utils(region="us-east-1")69 self._mock_launch_utils()
68 self._mock_launch()70 self._mock_launch()
69 self.mocker.result(succeed([]))71 self.mocker.result(succeed([]))
70 self._mock_save()72 self._mock_save()
71 self.mocker.replay()73 self.mocker.replay()
7274
73 provider = self.get_provider()75 provider = self.get_provider()
74 deferred = provider.bootstrap()76 constraints = yield get_constraints(["instance-type=m1.small"])
7577 yield provider.bootstrap(constraints)
76 def check_log(result):78 log_text = log.getvalue()
77 log_text = log.getvalue()79 self.assertIn("Launching juju bootstrap instance", log_text)
78 self.assertIn("Launching juju bootstrap instance", log_text)80 self.assertNotIn("previously bootstrapped", log_text)
79 self.assertNotIn("previously bootstrapped", log_text)81
8082 @inlineCallbacks
81 deferred.addCallback(check_log)
82 return deferred
83
84 def test_launch_bootstrap_existing_provider_group(self):83 def test_launch_bootstrap_existing_provider_group(self):
85 """84 """
86 When launching a bootstrap instance the provider will use an existing85 When launching a bootstrap instance the provider will use an existing
@@ -94,15 +93,17 @@
94 self.mocker.result(succeed([93 self.mocker.result(succeed([
95 SecurityGroup("juju-%s" % self.env_name, "")]))94 SecurityGroup("juju-%s" % self.env_name, "")]))
96 self._mock_create_machine_group(0)95 self._mock_create_machine_group(0)
97 self._mock_launch_utils(region="us-east-1")96 self._mock_launch_utils()
98 self._mock_launch()97 self._mock_launch()
99 self.mocker.result(succeed([]))98 self.mocker.result(succeed([]))
100 self._mock_save()99 self._mock_save()
101 self.mocker.replay()100 self.mocker.replay()
102101
103 provider = self.get_provider()102 provider = self.get_provider()
104 return provider.bootstrap()103 constraints = yield get_constraints(["instance-type=m1.small"])
104 yield provider.bootstrap(constraints)
105105
106 @inlineCallbacks
106 def test_run_with_loaded_state(self):107 def test_run_with_loaded_state(self):
107 """108 """
108 If the provider bootstrap is run when there is already a running109 If the provider bootstrap is run when there is already a running
@@ -116,21 +117,18 @@
116 self.mocker.replay()117 self.mocker.replay()
117118
118 log = self.capture_logging("juju.common")119 log = self.capture_logging("juju.common")
119
120 def validate_result(result):
121 self.assertTrue(result)
122 machine = result.pop()
123 self.assertTrue(isinstance(machine, EC2ProviderMachine))
124 self.assertEqual(machine.instance_id, "i-foobar")
125 self.assertEquals(
126 log.getvalue(),
127 "juju environment previously bootstrapped.\n")
128
129 provider = self.get_provider()120 provider = self.get_provider()
130 d = provider.bootstrap()121 constraints = yield get_constraints(["instance-type=m1.small"])
131 d.addCallback(validate_result)122 machines = yield provider.bootstrap(constraints)
132 return d123
133124 (machine,) = machines
125 self.assertTrue(isinstance(machine, EC2ProviderMachine))
126 self.assertEqual(machine.instance_id, "i-foobar")
127 self.assertEquals(
128 log.getvalue(),
129 "juju environment previously bootstrapped.\n")
130
131 @inlineCallbacks
134 def test_run_with_launch(self):132 def test_run_with_launch(self):
135 """133 """
136 The provider bootstrap will launch an instance when run if there134 The provider bootstrap will launch an instance when run if there
@@ -143,16 +141,14 @@
143 self.mocker.result(succeed([141 self.mocker.result(succeed([
144 SecurityGroup("juju-%s" % self.env_name, "")]))142 SecurityGroup("juju-%s" % self.env_name, "")]))
145 self._mock_create_machine_group(0)143 self._mock_create_machine_group(0)
146 self._mock_launch_utils(region="us-east-1")144 self._mock_launch_utils()
147 self._mock_launch()145 self._mock_launch()
148 self.mocker.result(succeed([self.get_instance("i-foobar")]))146 self.mocker.result(succeed([self.get_instance("i-foobar")]))
149 self._mock_save()147 self._mock_save()
150 self.mocker.replay()148 self.mocker.replay()
151149
152 def validate_result(result):
153 (machine,) = result
154 self.assert_machine(machine, "i-foobar", "")
155 provider = self.get_provider()150 provider = self.get_provider()
156 d = provider.bootstrap()151 constraints = yield get_constraints(["instance-type=m1.small"])
157 d.addCallback(validate_result)152 machines = yield provider.bootstrap(constraints)
158 return d153 (machine,) = machines
154 self.assert_machine(machine, "i-foobar", "")
159155
=== modified file 'juju/providers/ec2/tests/test_launch.py'
--- juju/providers/ec2/tests/test_launch.py 2011-09-28 05:22:36 +0000
+++ juju/providers/ec2/tests/test_launch.py 2012-03-30 16:56:32 +0000
@@ -13,15 +13,26 @@
13from juju.lib.testing import TestCase13from juju.lib.testing import TestCase
14from juju.lib.mocker import MATCH14from juju.lib.mocker import MATCH
1515
16from .common import EC2TestMixin, EC2MachineLaunchMixin16from .common import EC2TestMixin, EC2MachineLaunchMixin, get_constraints
1717
18DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "data")18DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "data")
1919
2020
21class EC2MachineLaunchTest(EC2TestMixin, EC2MachineLaunchMixin, TestCase):21class EC2MachineLaunchTest(EC2TestMixin, EC2MachineLaunchMixin, TestCase):
2222
23 @inlineCallbacks
24 def setUp(self):
25 yield super(EC2MachineLaunchTest, self).setUp()
26 self.constraints = yield get_constraints([])
27 self.gen_constraints = yield get_constraints(["cpu=20", "mem=7168"])
28 self.gen_constraints = self.gen_constraints.with_series("dribbly")
29 self.ec2_constraints = yield get_constraints(
30 ["instance-type=cc2.8xlarge", "ec2-zone=b"])
31 self.ec2_constraints = self.ec2_constraints.with_series("vast")
32
23 def _mock_launch(self, instance, expect_ami="ami-default",33 def _mock_launch(self, instance, expect_ami="ami-default",
24 expect_instance_type="m1.small",34 expect_instance_type="m1.small",
35 expect_availability_zone=None,
25 cloud_init="launch_cloud_init"):36 cloud_init="launch_cloud_init"):
2637
27 def verify_user_data(data):38 def verify_user_data(data):
@@ -37,6 +48,7 @@
37 max_count=1,48 max_count=1,
38 min_count=1,49 min_count=1,
39 security_groups=["juju-moon", "juju-moon-1"],50 security_groups=["juju-moon", "juju-moon-1"],
51 availability_zone=expect_availability_zone,
40 user_data=MATCH(verify_user_data))52 user_data=MATCH(verify_user_data))
4153
42 self.mocker.result(succeed([instance]))54 self.mocker.result(succeed([instance]))
@@ -62,7 +74,7 @@
62 self.mocker.result(succeed([]))74 self.mocker.result(succeed([]))
63 self._mock_create_group()75 self._mock_create_group()
64 self._mock_create_machine_group("1")76 self._mock_create_machine_group("1")
65 self._mock_launch_utils(region="us-east-1")77 self._mock_launch_utils()
66 self._mock_get_zookeeper_hosts()78 self._mock_get_zookeeper_hosts()
67 self._mock_launch(self.get_instance("i-foobar"))79 self._mock_launch(self.get_instance("i-foobar"))
68 self.mocker.replay()80 self.mocker.replay()
@@ -71,18 +83,28 @@
71 (machine,) = result83 (machine,) = result
72 self.assert_machine(machine, "i-foobar", "")84 self.assert_machine(machine, "i-foobar", "")
73 provider = self.get_provider()85 provider = self.get_provider()
74 d = provider.start_machine({"machine-id": "1"})86 d = provider.start_machine({
87 "machine-id": "1", "constraints": self.constraints})
75 d.addCallback(verify_result)88 d.addCallback(verify_result)
76 return d89 return d
7790
78 @inlineCallbacks91 @inlineCallbacks
92 def test_provider_launch_requires_constraints(self):
93 self.mocker.replay()
94 provider = self.get_provider()
95 d = provider.start_machine({"machine-id": "1"})
96 e = yield self.assertFailure(d, ProviderError)
97 self.assertEquals(
98 str(e), "Cannot launch a machine without specifying constraints")
99
100 @inlineCallbacks
79 def test_provider_launch_using_branch(self):101 def test_provider_launch_using_branch(self):
80 """Can use a juju branch to launch a machine"""102 """Can use a juju branch to launch a machine"""
81 self.ec2.describe_security_groups()103 self.ec2.describe_security_groups()
82 self.mocker.result(succeed([]))104 self.mocker.result(succeed([]))
83 self._mock_create_group()105 self._mock_create_group()
84 self._mock_create_machine_group("1")106 self._mock_create_machine_group("1")
85 self._mock_launch_utils(region="us-east-1")107 self._mock_launch_utils()
86 self._mock_get_zookeeper_hosts()108 self._mock_get_zookeeper_hosts()
87 self._mock_launch(109 self._mock_launch(
88 self.get_instance("i-foobar"),110 self.get_instance("i-foobar"),
@@ -91,7 +113,8 @@
91113
92 provider = self.get_provider()114 provider = self.get_provider()
93 provider.config["juju-origin"] = "lp:~wizard/juju-juicebar"115 provider.config["juju-origin"] = "lp:~wizard/juju-juicebar"
94 machines = yield provider.start_machine({"machine-id": "1"})116 machines = yield provider.start_machine({
117 "machine-id": "1", "constraints": self.constraints})
95 self.assert_machine(machines[0], "i-foobar", "")118 self.assert_machine(machines[0], "i-foobar", "")
96119
97 @inlineCallbacks120 @inlineCallbacks
@@ -101,7 +124,7 @@
101 self.mocker.result(succeed([]))124 self.mocker.result(succeed([]))
102 self._mock_create_group()125 self._mock_create_group()
103 self._mock_create_machine_group("1")126 self._mock_create_machine_group("1")
104 self._mock_launch_utils(region="us-east-1")127 self._mock_launch_utils()
105 self._mock_get_zookeeper_hosts()128 self._mock_get_zookeeper_hosts()
106 self._mock_launch(129 self._mock_launch(
107 self.get_instance("i-foobar"),130 self.get_instance("i-foobar"),
@@ -110,7 +133,8 @@
110133
111 provider = self.get_provider()134 provider = self.get_provider()
112 provider.config["juju-origin"] = "ppa"135 provider.config["juju-origin"] = "ppa"
113 machines = yield provider.start_machine({"machine-id": "1"})136 machines = yield provider.start_machine({
137 "machine-id": "1", "constraints": self.constraints})
114 self.assert_machine(machines[0], "i-foobar", "")138 self.assert_machine(machines[0], "i-foobar", "")
115139
116 @inlineCallbacks140 @inlineCallbacks
@@ -120,7 +144,7 @@
120 self.mocker.result(succeed([]))144 self.mocker.result(succeed([]))
121 self._mock_create_group()145 self._mock_create_group()
122 self._mock_create_machine_group("1")146 self._mock_create_machine_group("1")
123 self._mock_launch_utils(region="us-east-1")147 self._mock_launch_utils()
124 self._mock_get_zookeeper_hosts()148 self._mock_get_zookeeper_hosts()
125 self._mock_launch(149 self._mock_launch(
126 self.get_instance("i-foobar"),150 self.get_instance("i-foobar"),
@@ -129,7 +153,8 @@
129153
130 provider = self.get_provider()154 provider = self.get_provider()
131 provider.config["juju-origin"] = "distro"155 provider.config["juju-origin"] = "distro"
132 machines = yield provider.start_machine({"machine-id": "1"})156 machines = yield provider.start_machine({
157 "machine-id": "1", "constraints": self.constraints})
133 self.assert_machine(machines[0], "i-foobar", "")158 self.assert_machine(machines[0], "i-foobar", "")
134159
135 @inlineCallbacks160 @inlineCallbacks
@@ -141,17 +166,17 @@
141 self.ec2.describe_security_groups()166 self.ec2.describe_security_groups()
142 self.mocker.result(succeed([security_group]))167 self.mocker.result(succeed([security_group]))
143 self._mock_create_machine_group("1")168 self._mock_create_machine_group("1")
144 self._mock_launch_utils(region="us-east-1")169 self._mock_launch_utils()
145 self._mock_get_zookeeper_hosts()170 self._mock_get_zookeeper_hosts()
146 self._mock_launch(instance)171 self._mock_launch(instance)
147 self.mocker.replay()172 self.mocker.replay()
148173
149 provider = self.get_provider()174 provider = self.get_provider()
150 provided_machines = yield provider.start_machine({"machine-id": "1"})175 machines = yield provider.start_machine({
151 self.assertEqual(len(provided_machines), 1)176 "machine-id": "1", "constraints": self.constraints})
152 self.assertTrue(isinstance(provided_machines[0], EC2ProviderMachine))177 self.assertEqual(len(machines), 1)
153 self.assertEqual(178 self.assertTrue(isinstance(machines[0], EC2ProviderMachine))
154 provided_machines[0].instance_id, instance.instance_id)179 self.assertEqual(machines[0].instance_id, instance.instance_id)
155180
156 @inlineCallbacks181 @inlineCallbacks
157 def test_provider_launch_existing_machine_security_group(self):182 def test_provider_launch_existing_machine_security_group(self):
@@ -165,17 +190,17 @@
165 self._mock_create_group()190 self._mock_create_group()
166 self._mock_delete_machine_group("1") # delete existing sg191 self._mock_delete_machine_group("1") # delete existing sg
167 self._mock_create_machine_group("1") # then recreate192 self._mock_create_machine_group("1") # then recreate
168 self._mock_launch_utils(region="us-east-1")193 self._mock_launch_utils()
169 self._mock_get_zookeeper_hosts()194 self._mock_get_zookeeper_hosts()
170 self._mock_launch(instance)195 self._mock_launch(instance)
171 self.mocker.replay()196 self.mocker.replay()
172197
173 provider = self.get_provider()198 provider = self.get_provider()
174 provided_machines = yield provider.start_machine({"machine-id": "1"})199 machines = yield provider.start_machine({
175 self.assertEqual(len(provided_machines), 1)200 "machine-id": "1", "constraints": self.constraints})
176 self.assertTrue(isinstance(provided_machines[0], EC2ProviderMachine))201 self.assertEqual(len(machines), 1)
177 self.assertEqual(202 self.assertTrue(isinstance(machines[0], EC2ProviderMachine))
178 provided_machines[0].instance_id, instance.instance_id)203 self.assertEqual(machines[0].instance_id, instance.instance_id)
179204
180 @inlineCallbacks205 @inlineCallbacks
181 def test_provider_launch_existing_machine_security_group_is_active(self):206 def test_provider_launch_existing_machine_security_group_is_active(self):
@@ -191,13 +216,14 @@
191 self.mocker.result(succeed([machine_group]))216 self.mocker.result(succeed([machine_group]))
192 self._mock_create_group()217 self._mock_create_group()
193 self._mock_delete_machine_group_was_deleted("1") # sg is gone!218 self._mock_delete_machine_group_was_deleted("1") # sg is gone!
194 self._mock_launch_utils(region="us-east-1")219 self._mock_launch_utils()
195 self._mock_get_zookeeper_hosts()220 self._mock_get_zookeeper_hosts()
196 self.mocker.replay()221 self.mocker.replay()
197222
198 provider = self.get_provider()223 provider = self.get_provider()
199 ex = yield self.assertFailure(224 ex = yield self.assertFailure(
200 provider.start_machine({"machine-id": "1"}),225 provider.start_machine({
226 "machine-id": "1", "constraints": self.constraints}),
201 ProviderInteractionError)227 ProviderInteractionError)
202 self.assertEqual(228 self.assertEqual(
203 str(ex),229 str(ex),
@@ -216,7 +242,8 @@
216 self.mocker.replay()242 self.mocker.replay()
217243
218 provider = self.get_provider()244 provider = self.get_provider()
219 d = provider.start_machine({"machine-id": "1"})245 d = provider.start_machine({
246 "machine-id": "1", "constraints": self.constraints})
220 self.assertFailure(d, EnvironmentNotFound)247 self.assertFailure(d, EnvironmentNotFound)
221 return d248 return d
222249
@@ -231,65 +258,62 @@
231 self.mocker.replay()258 self.mocker.replay()
232259
233 provider = self.get_provider()260 provider = self.get_provider()
234 d = provider.start_machine({"machine-id": "1"})261 d = provider.start_machine({
262 "machine-id": "1", "constraints": self.constraints})
235 self.assertFailure(d, EnvironmentNotFound)263 self.assertFailure(d, EnvironmentNotFound)
236 return d264 return d
237265
238 def test_launch_options_known_instance_type(self):266 def test_launch_options_from_config_region(self):
239 self.ec2.describe_security_groups()267 self.ec2.describe_security_groups()
240 self.mocker.result(succeed([]))268 self.mocker.result(succeed([]))
241 self._mock_create_group()269 self._mock_create_group()
242 self._mock_create_machine_group("1")270 self._mock_create_machine_group("1")
243 self._mock_launch_utils(region="us-east-1")271 self._mock_launch_utils(
244 self._mock_get_zookeeper_hosts()272 ami_name="ami-regional",
245 self._mock_launch(273 get_ami_args=("splendid", "amd64", "somewhere-else-1", False))
246 self.get_instance("i-foobar"), expect_instance_type="m1.ginormous")274 self._mock_get_zookeeper_hosts()
247 self.mocker.replay()275 self._mock_launch(self.get_instance("i-foobar"), "ami-regional")
248
249 provider = self.get_provider()
250 provider.config["default-instance-type"] = "m1.ginormous"
251 return provider.start_machine({"machine-id": "1"})
252
253 def _mock_launch_with_ami_params(self, get_ami_kwargs, expect_ami=None):
254 expect_ami = expect_ami or "ami-default"
255 self.ec2.describe_security_groups()
256 self.mocker.result(succeed([]))
257 self._mock_create_group()
258 self._mock_create_machine_group("1")
259 self._mock_launch_utils(ami_name=expect_ami, **get_ami_kwargs)
260 self._mock_get_zookeeper_hosts()
261 self._mock_launch(self.get_instance("i-foobar"), expect_ami)
262
263 def test_launch_options_known_ami(self):
264 self._mock_launch_with_ami_params({}, expect_ami="ami-different")
265 self.mocker.replay()
266
267 provider = self.get_provider()
268 provider.config["default-image-id"] = "ami-different"
269 return provider.start_machine({"machine-id": "1"})
270
271 def test_launch_options_region(self):
272 self._mock_launch_with_ami_params({"region": "somewhere-else-1"})
273 self.mocker.replay()276 self.mocker.replay()
274277
275 provider = self.get_provider()278 provider = self.get_provider()
276 provider.config["region"] = "somewhere-else-1"279 provider.config["region"] = "somewhere-else-1"
277 return provider.start_machine({"machine-id": "1"})280 return provider.start_machine({
278281 "machine-id": "1", "constraints": self.constraints})
279 def test_launch_options_custom_image_options(self):282
280 self._mock_launch_with_ami_params({283 def test_launch_options_ec2_constraints(self):
281 "region": "us-east-1",284 self.ec2.describe_security_groups()
282 "ubuntu_release": "choleric",285 self.mocker.result(succeed([]))
283 "architecture": "quantum86",286 self._mock_create_group()
284 "persistent_storage": "maybe",287 self._mock_create_machine_group("1")
285 "daily": "perhaps"})288 self._mock_launch_utils(
286 self.mocker.replay()289 ami_name="ami-fancy-cluster",
287290 get_ami_args=("vast", "amd64", "us-east-1", True))
288 provider = self.get_provider()291 self._mock_get_zookeeper_hosts()
289 constraints = {292 self._mock_launch(
290 "ubuntu_release": "choleric",293 self.get_instance("i-foobar"), "ami-fancy-cluster",
291 "architecture": "quantum86",294 expect_instance_type="cc2.8xlarge",
292 "persistent_storage": "maybe",295 expect_availability_zone="us-east-1b")
293 "daily": "perhaps"}296 self.mocker.replay()
294 return provider.start_machine({"machine-id": "1",297
295 "constraints": constraints})298 provider = self.get_provider()
299 return provider.start_machine({
300 "machine-id": "1",
301 "constraints": self.ec2_constraints})
302
303 def test_launch_options_generic_constraints(self):
304 self.ec2.describe_security_groups()
305 self.mocker.result(succeed([]))
306 self._mock_create_group()
307 self._mock_create_machine_group("1")
308 self._mock_launch_utils(
309 get_ami_args=("dribbly", "amd64", "us-east-1", False))
310 self._mock_get_zookeeper_hosts()
311 self._mock_launch(
312 self.get_instance("i-foobar"), "ami-default",
313 expect_instance_type="c1.xlarge")
314 self.mocker.replay()
315
316 provider = self.get_provider()
317 return provider.start_machine({
318 "machine-id": "1",
319 "constraints": self.gen_constraints})
296320
=== modified file 'juju/providers/ec2/tests/test_provider.py'
--- juju/providers/ec2/tests/test_provider.py 2011-09-23 19:47:01 +0000
+++ juju/providers/ec2/tests/test_provider.py 2012-03-30 16:56:32 +0000
@@ -1,7 +1,10 @@
1from twisted.internet.defer import inlineCallbacks
2
3from juju.environment.errors import EnvironmentsConfigError
4from juju.errors import ConstraintError
1from juju.lib.testing import TestCase5from juju.lib.testing import TestCase
2from juju.providers.ec2.files import FileStorage6from juju.providers.ec2.files import FileStorage
3from juju.providers.ec2 import MachineProvider7from juju.providers.ec2 import MachineProvider
4from juju.environment.errors import EnvironmentsConfigError
58
6from .common import EC2TestMixin9from .common import EC2TestMixin
710
@@ -117,6 +120,91 @@
117 serialized = provider.get_serialization_data()120 serialized = provider.get_serialization_data()
118 self.assertEqual(config, serialized)121 self.assertEqual(config, serialized)
119122
123 def test_get_legacy_config_keys(self):
124 provider = MachineProvider(self.env_name, {
125 # Note: these keys *will* at some stage be considered legacy keys;
126 # they're included here to make sure the tests are updated when we
127 # make that change.
128 "default-series": "foo", "placement": "bar"})
129 self.assertEquals(provider.get_legacy_config_keys(), set())
130
131 # These keys are not valid on Amazon EC2...
132 provider.config.update({
133 "default-instance-type": "baz", "default-image-id": "qux"})
134 self.assertEquals(provider.get_legacy_config_keys(), set((
135 "default-instance-type", "default-image-id")))
136
137 # ...but they still are when using private clouds.
138 provider.config.update({"ec2-uri": "anything"})
139 self.assertEquals(provider.get_legacy_config_keys(), set())
140
141
142class ProviderConstraintsTestCase(TestCase):
143
144 def constraint_set(self):
145 provider = MachineProvider("some-ec2-env", {})
146 return provider.get_constraint_set()
147
148 @inlineCallbacks
149 def assert_invalid(self, msg, *strs):
150 cs = yield self.constraint_set()
151 e = self.assertRaises(ConstraintError, cs.parse, strs)
152 self.assertEquals(str(e), msg)
153
154 @inlineCallbacks
155 def test_constraints(self):
156 cs = yield self.constraint_set()
157 self.assertEquals(cs.parse([]), {
158 "provider-type": "ec2",
159 "ubuntu-series": None,
160 "instance-type": None,
161 "ec2-zone": None,
162 "arch": "amd64",
163 "cpu": 1.0,
164 "mem": 512.0})
165 self.assertEquals(cs.parse(["ec2-zone=X", "instance-type=m1.small"]), {
166 "provider-type": "ec2",
167 "ubuntu-series": None,
168 "instance-type": "m1.small",
169 "ec2-zone": "x",
170 "arch": "amd64",
171 "cpu": None,
172 "mem": None})
173
174 yield self.assert_invalid(
175 "Bad 'ec2-zone' constraint '7': expected single ascii letter",
176 "ec2-zone=7")
177 yield self.assert_invalid(
178 "Bad 'ec2-zone' constraint 'blob': expected single ascii letter",
179 "ec2-zone=blob")
180 yield self.assert_invalid(
181 "Bad 'instance-type' constraint 'qq1.moar': unknown instance type",
182 "instance-type=qq1.moar")
183 yield self.assert_invalid(
184 "Ambiguous constraints: 'cpu' overlaps with 'instance-type'",
185 "instance-type=m1.small", "cpu=1")
186 yield self.assert_invalid(
187 "Ambiguous constraints: 'instance-type' overlaps with 'mem'",
188 "instance-type=m1.small", "mem=2G")
189
190 @inlineCallbacks
191 def test_satisfy_zone_constraint(self):
192 cs = yield self.constraint_set()
193 a = cs.parse(["ec2-zone=a"]).with_series("series")
194 b = cs.parse(["ec2-zone=b"]).with_series("series")
195 self.assertTrue(a.can_satisfy(a))
196 self.assertTrue(b.can_satisfy(b))
197 self.assertFalse(a.can_satisfy(b))
198 self.assertFalse(b.can_satisfy(a))
199
200 @inlineCallbacks
201 def test_non_amazon_constraints(self):
202 provider = MachineProvider("some-non-ec2-env", {"ec2-uri": "blah"})
203 cs = yield provider.get_constraint_set()
204 self.assertEquals(cs.parse([]), {
205 "provider-type": "ec2",
206 "ubuntu-series": None})
207
120208
121class FailCreateTest(TestCase):209class FailCreateTest(TestCase):
122210
123211
=== modified file 'juju/providers/ec2/tests/test_utils.py'
--- juju/providers/ec2/tests/test_utils.py 2012-03-26 17:13:53 +0000
+++ juju/providers/ec2/tests/test_utils.py 2012-03-30 16:56:32 +0000
@@ -1,12 +1,16 @@
1import inspect1import inspect
2import os2import os
33
4from twisted.internet.defer import fail, succeed4from twisted.internet.defer import fail, succeed, inlineCallbacks
5from twisted.web.error import Error5from twisted.web.error import Error
66
7from juju.errors import ProviderError
7from juju.lib.testing import TestCase8from juju.lib.testing import TestCase
8from juju.providers import ec29from juju.providers import ec2
9from juju.providers.ec2.utils import get_current_ami, get_image_id10from juju.providers.ec2.utils import (
11 get_current_ami, get_instance_type, get_machine_spec)
12
13from .common import get_constraints
1014
1115
12IMAGE_URI_TEMPLATE = "\16IMAGE_URI_TEMPLATE = "\
@@ -26,7 +30,7 @@
26 page(IMAGE_URI_TEMPLATE % "nutty")30 page(IMAGE_URI_TEMPLATE % "nutty")
27 self.mocker.result(fail(Error("404")))31 self.mocker.result(fail(Error("404")))
28 self.mocker.replay()32 self.mocker.replay()
29 d = get_current_ami(ubuntu_release="nutty")33 d = get_current_ami("nutty", "i386", "us-east-1", False)
30 self.failUnlessFailure(d, LookupError)34 self.failUnlessFailure(d, LookupError)
31 return d35 return d
3236
@@ -39,7 +43,7 @@
39 page(IMAGE_URI_TEMPLATE % "lucid")43 page(IMAGE_URI_TEMPLATE % "lucid")
40 self.mocker.result(succeed(""))44 self.mocker.result(succeed(""))
41 self.mocker.replay()45 self.mocker.replay()
42 d = get_current_ami(ubuntu_release="lucid")46 d = get_current_ami("lucid", "i386", "us-east-1", False)
43 self.failUnlessFailure(d, LookupError)47 self.failUnlessFailure(d, LookupError)
44 return d48 return d
4549
@@ -51,12 +55,20 @@
51 open(os.path.join(IMAGE_DATA_DIR, "lucid.txt")).read()))55 open(os.path.join(IMAGE_DATA_DIR, "lucid.txt")).read()))
5256
53 self.mocker.replay()57 self.mocker.replay()
54 d = get_current_ami(ubuntu_release="lucid")58 d = get_current_ami("lucid", "i386", "us-east-1", False)
5559 d.addCallback(self.assertEquals, "ami-714ba518")
56 def verify_result(result):60 return d
57 self.assertEqual(result, "ami-714ba518")61
5862 def test_current_ami_by_arch(self):
59 d.addCallback(verify_result)63 """The current server machine image can be retrieved by arch."""
64 page = self.mocker.replace("twisted.web.client.getPage")
65 page(IMAGE_URI_TEMPLATE % "lucid")
66 self.mocker.result(
67 succeed(open(
68 os.path.join(IMAGE_DATA_DIR, "lucid.txt")).read()))
69 self.mocker.replay()
70 d = get_current_ami("lucid", "amd64", "us-east-1", False)
71 d.addCallback(self.assertEquals, "ami-4b4ba522")
60 return d72 return d
6173
62 def test_current_ami_by_region(self):74 def test_current_ami_by_region(self):
@@ -67,75 +79,166 @@
67 succeed(open(79 succeed(open(
68 os.path.join(IMAGE_DATA_DIR, "lucid.txt")).read()))80 os.path.join(IMAGE_DATA_DIR, "lucid.txt")).read()))
69 self.mocker.replay()81 self.mocker.replay()
70 d = get_current_ami(ubuntu_release="lucid", region="us-west-1")82 d = get_current_ami("lucid", "i386", "us-west-1", False)
7183 d.addCallback(self.assertEquals, "ami-cb97c68e")
72 def verify_result(result):84 return d
73 self.assertEqual(result, "ami-cb97c68e")85
7486 def test_current_ami_with_virtualisation_info(self):
75 d.addCallback(verify_result)87 """The current server machine image can be retrieved by arch."""
76 return d88 page = self.mocker.replace("twisted.web.client.getPage")
7789 page(IMAGE_URI_TEMPLATE % "natty")
78 def test_current_ami_non_ebs(self):90 self.mocker.result(
79 """91 succeed(open(
80 The get_current_ami function accepts several filtering parameters92 os.path.join(IMAGE_DATA_DIR, "natty.txt")).read()))
81 to guide image selection.93 self.mocker.replay()
82 """94 d = get_current_ami("natty", "amd64", "us-east-1", True)
95 d.addCallback(self.assertEquals, "ami-1cad5275")
96 return d
97
98 def test_hvm_request_on_old_series(self):
83 page = self.mocker.replace("twisted.web.client.getPage")99 page = self.mocker.replace("twisted.web.client.getPage")
84 page(IMAGE_URI_TEMPLATE % "lucid")100 page(IMAGE_URI_TEMPLATE % "lucid")
85 self.mocker.result(succeed(101 self.mocker.result(
86 open(os.path.join(IMAGE_DATA_DIR, "lucid.txt")).read()))102 succeed(open(
103 os.path.join(IMAGE_DATA_DIR, "lucid.txt")).read()))
87 self.mocker.replay()104 self.mocker.replay()
88 d = get_current_ami(ubuntu_release="lucid", persistent_storage=False)105 d = get_current_ami("lucid", "amd64", "us-east-1", True)
89106 self.failUnlessFailure(d, LookupError)
90 def verify_result(result):
91 self.assertEqual(result, "ami-2d4aa444")
92
93 d.addCallback(verify_result)
94 return d107 return d
95108
96109
97class GetImageIdTest(TestCase):110class GetImageIdTest(TestCase):
98111
99 def test_default_image_id(self):112 @inlineCallbacks
100 d = get_image_id({"default-image-id": "ami-burble"}, {})113 def assert_image_id(self, config, constraints, series, arch, region, hvm,
101 d.addCallback(self.assertEquals, "ami-burble")114 instance_type):
102 return d
103
104 def test_no_constraints(self):
105 get_current_ami_m = self.mocker.replace(get_current_ami)115 get_current_ami_m = self.mocker.replace(get_current_ami)
106 get_current_ami_m(region="us-east-1")116 get_current_ami_m(series, arch, region, hvm)
107 self.mocker.result(succeed("ami-giggle"))117 self.mocker.result(succeed("ami-giggle"))
108 self.mocker.replay()118 self.mocker.replay()
109119
110 d = get_image_id({}, {})120 spec = yield get_machine_spec(config, constraints)
111 d.addCallback(self.assertEquals, "ami-giggle")121 self.assertEquals(spec.image_id, "ami-giggle")
112 return d122 self.assertEquals(spec.instance_type, instance_type)
113123
114 def test_default_series(self):124 @inlineCallbacks
115 get_current_ami_m = self.mocker.replace(get_current_ami)125 def test_empty_config(self):
116 get_current_ami_m(region="us-east-1", ubuntu_release="puissant")126 constraints = yield get_constraints(["arch=i386"])
117 self.mocker.result(succeed("ami-pickle"))127 yield self.assert_image_id(
118 self.mocker.replay()128 {}, constraints,
119129 "splendid", "i386", "us-east-1", False,
120 d = get_image_id({"default-series": "puissant"}, {})130 "m1.small")
121 d.addCallback(self.assertEquals, "ami-pickle")131
122 return d132 @inlineCallbacks
123133 def test_series_from_config(self):
124 def test_uses_constraints(self):134 config = {"default-series": "puissant"}
125 get_current_ami_m = self.mocker.replace(get_current_ami)135 constraints = yield get_constraints(["arch=i386"], None)
126 get_current_ami_m(ubuntu_release="serendipitous", architecture="x512",136 yield self.assert_image_id(
127 daily=False, persistent_storage=True,137 config, constraints,
128 region="blah-north-6")138 "puissant", "i386", "us-east-1", False,
129 self.mocker.result(succeed("ami-tinkle"))139 "m1.small")
130 self.mocker.replay()140
131141 @inlineCallbacks
132 constraints = {142 def test_arch_from_instance_constraint(self):
133 "architecture": "x512",143 constraints = yield get_constraints([
134 "ubuntu_release": "serendipitous",144 "instance-type=m1.large", "arch=any"])
135 "persistent_storage": True,145 yield self.assert_image_id(
136 "daily": False}146 {}, constraints,
137 d = get_image_id(147 "splendid", "amd64", "us-east-1", False,
138 {"region": "blah-north-6", "default-series": "overridden"},148 "m1.large")
139 constraints)149
140 d.addCallback(self.assertEquals, "ami-tinkle")150 @inlineCallbacks
141 return d151 def test_arch_from_nothing(self):
152 constraints = yield get_constraints([
153 "arch=any", "cpu=any", "mem=any", "instance-type=any"])
154 yield self.assert_image_id(
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to status/vote changes: