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
1=== added file 'docs/source/internals/constraints-notes.rst'
2--- docs/source/internals/constraints-notes.rst 1970-01-01 00:00:00 +0000
3+++ docs/source/internals/constraints-notes.rst 2012-03-30 16:56:32 +0000
4@@ -0,0 +1,78 @@
5+Notes on the current state of machine constraints
6+=================================================
7+
8+Summary
9+-------
10+
11+EC2 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.
12+
13+We 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.
14+
15+
16+Code overview
17+-------------
18+
19+`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.
20+
21+The 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.
22+
23+Once 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.
24+
25+This 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:
26+
27+A = "orchestra-name=jimbob" (masks arch, cpu, mem, orchestra-classes)
28+B = "arch=arm orchestra-classes=ultralightweight" (masks orchestra-name)
29+
30+Now, 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.
31+
32+It'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.
33+
34+There 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.
35+
36+
37+Usage
38+-----
39+
40+Actual 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.)
41+
42+Basically, 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.
43+
44+Bootstrap 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).
45+
46+
47+Merge pipeline
48+--------------
49+
50+* placement-spec (Just the spec. Despite the MaaS-related inaccuracies, worth merging, IMO.)
51+
52+* constraint-types (Add `juju.machine.constraints` and `juju.machine.constraints_info`. Oh, and `juju.errors.ConstraintError`.)
53+
54+* set-service-constraints (Add `set-constraints` command; add param to `deploy` command; store constraints in service state.)
55+
56+* 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.)
57+
58+* 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.)
59+
60+
61+Known problems
62+--------------
63+
64+* 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.
65+
66+* 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.
67+
68+ * 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).
69+
70+* 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.
71+
72+ * 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.)
73+
74+ 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).
75+
76+* 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).
77+
78+ 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.
79+
80+* 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.
81+
82+* 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.
83
84=== modified file 'juju/agents/provision.py'
85--- juju/agents/provision.py 2012-02-01 15:39:08 +0000
86+++ juju/agents/provision.py 2012-03-30 16:56:32 +0000
87@@ -6,7 +6,7 @@
88 from juju.environment.config import EnvironmentsConfig
89 from juju.errors import ProviderError
90 from juju.lib.twistutils import concurrent_execution_guard
91-from juju.state.errors import MachineStateNotFound, StateChanged, StopWatcher
92+from juju.state.errors import MachineStateNotFound, StopWatcher
93 from juju.state.firewall import FirewallManager
94 from juju.state.machine import MachineStateManager
95 from juju.state.service import ServiceStateManager
96@@ -223,8 +223,9 @@
97 # Verify a machine id has state and is running, else launch it.
98 if instance_id is None or not instance_id in provider_machine_map:
99 log.info("Starting machine id:%s ...", machine_state.id)
100+ constraints = yield machine_state.get_constraints()
101 machines = yield self.provider.start_machine(
102- {"machine-id": machine_state.id})
103+ {"machine-id": machine_state.id, "constraints": constraints})
104 instance_id = machines[0].instance_id
105 yield machine_state.set_instance_id(instance_id)
106
107
108=== modified file 'juju/agents/tests/common.py'
109--- juju/agents/tests/common.py 2011-11-28 12:28:57 +0000
110+++ juju/agents/tests/common.py 2012-03-30 16:56:32 +0000
111@@ -5,30 +5,23 @@
112 from txzookeeper.tests.utils import deleteTree
113
114 from juju.agents.base import TwistedOptionNamespace
115-from juju.environment.config import EnvironmentsConfig
116 from juju.state.tests.common import StateTestBase
117 from juju.tests.common import get_test_zookeeper_address
118
119
120-SAMPLE_ENV = """\
121-environments:
122- myfirstenv:
123- type: dummy
124- foo: bar
125- storage-directory: %s
126-"""
127-
128-
129 class AgentTestBase(StateTestBase):
130
131 agent_class = None
132 juju_directory = None
133+ setup_environment = True
134
135 @inlineCallbacks
136 def setUp(self):
137 self.juju_directory = self.makeDir()
138 yield super(AgentTestBase, self).setUp()
139 assert self.agent_class, "Agent Class must be specified on test"
140+ if self.setup_environment:
141+ yield self.push_default_config()
142 self.agent = self.agent_class()
143 self.options = yield self.get_agent_config()
144 self.agent.configure(self.options)
145@@ -42,15 +35,6 @@
146 deleteTree("/", self.client.handle)
147 self.client.close()
148
149- def get_test_environment_config(self):
150- sample_config = SAMPLE_ENV % self.makeDir()
151- config = EnvironmentsConfig()
152- config.parse(sample_config)
153- return config
154-
155- def get_test_environment(self):
156- return self.get_test_environment_config().get_default()
157-
158 def get_agent_config(self):
159 options = TwistedOptionNamespace()
160 options["juju_directory"] = self.juju_directory
161
162=== modified file 'juju/agents/tests/test_machine.py'
163--- juju/agents/tests/test_machine.py 2012-03-16 16:54:43 +0000
164+++ juju/agents/tests/test_machine.py 2012-03-30 16:56:32 +0000
165@@ -2,8 +2,7 @@
166 import logging
167 import os
168
169-from twisted.internet.defer import (
170- inlineCallbacks, returnValue, succeed, fail, Deferred)
171+from twisted.internet.defer import inlineCallbacks, returnValue, fail, Deferred
172
173 from juju.agents.base import TwistedOptionNamespace
174 from juju.agents.machine import MachineAgent
175@@ -14,8 +13,8 @@
176 from juju.charm.tests import local_charm_id
177 from juju.charm.tests.test_repository import RepositoryTestBase
178 from juju.lib.mocker import MATCH
179-from juju.machine.constraints import Constraints
180-from juju.state.environment import EnvironmentStateManager
181+from juju.machine.tests.test_constraints import (
182+ dummy_constraints, series_constraints)
183 from juju.state.machine import MachineStateManager, MachineState
184 from juju.state.service import ServiceStateManager
185 from juju.tests.common import get_test_zookeeper_address
186@@ -36,13 +35,7 @@
187 yield super(MachineAgentTest, self).setUp()
188
189 self.output = self.capture_logging(level=logging.DEBUG)
190-
191- config = self.get_test_environment_config()
192- environment = config.get_default()
193-
194- # Store the environment to zookeeper
195- environment_state_manager = EnvironmentStateManager(self.client)
196- yield environment_state_manager.set_config_state(config, "myfirstenv")
197+ environment = self.config.get_default()
198
199 # Load the environment with the charm state and charm binary
200 self.provider = environment.get_machine_provider()
201@@ -58,8 +51,7 @@
202 # the machine.
203 self.service_state_manager = ServiceStateManager(self.client)
204 self.service = yield self.service_state_manager.add_service_state(
205- "fatality-blog", self.charm_state,
206- Constraints.from_strs("dummy", []))
207+ "fatality-blog", self.charm_state, dummy_constraints)
208
209 @inlineCallbacks
210 def get_agent_config(self):
211@@ -68,7 +60,7 @@
212 machine_state_manager = MachineStateManager(self.client)
213
214 self.machine_state = yield machine_state_manager.add_machine_state(
215- Constraints.from_strs("dummy", []).with_series("series"))
216+ series_constraints)
217
218 self.change_environment(
219 JUJU_MACHINE_ID="0",
220
221=== modified file 'juju/agents/tests/test_provision.py'
222--- juju/agents/tests/test_provision.py 2012-01-09 16:57:13 +0000
223+++ juju/agents/tests/test_provision.py 2012-03-30 16:56:32 +0000
224@@ -1,18 +1,12 @@
225 import logging
226
227-import zookeeper
228-
229 from twisted.internet.defer import inlineCallbacks, fail, succeed
230 from twisted.internet import reactor
231
232-from txzookeeper.client import ZOO_OPEN_ACL_UNSAFE
233-
234 from juju.agents.provision import ProvisioningAgent
235 from juju.environment.environment import Environment
236-from juju.environment.config import EnvironmentsConfig
237 from juju.environment.errors import EnvironmentsConfigError
238-from juju.environment.tests.test_config import SAMPLE_ENV
239-from juju.machine.constraints import Constraints
240+from juju.machine.tests.test_constraints import dummy_cs, series_constraints
241 from juju.errors import ProviderInteractionError
242 from juju.lib.mocker import MATCH
243 from juju.providers.dummy import DummyMachine
244@@ -37,23 +31,21 @@
245 yield super(ProvisioningTestBase, self).setUp()
246 self.machine_manager = MachineStateManager(self.client)
247
248- def add_machine_state(self):
249+ def add_machine_state(self, constraints=None):
250 return self.machine_manager.add_machine_state(
251- Constraints.from_strs("dummy", []).with_series("series"))
252-
253- def get_serialized_environment(self):
254- config = EnvironmentsConfig()
255- config.parse(SAMPLE_ENV)
256- return config.serialize("myfirstenv")
257+ constraints or series_constraints)
258
259
260 class ProvisioningAgentStartupTest(ProvisioningTestBase):
261
262+ setup_environment = False
263+
264 @inlineCallbacks
265 def setUp(self):
266 yield super(ProvisioningAgentStartupTest, self).setUp()
267 yield self.agent.connect()
268
269+ @inlineCallbacks
270 def test_agent_waits_for_environment(self):
271 """
272 When the agent starts it waits for the /environment node to exist.
273@@ -61,31 +53,20 @@
274 deserialize it into an environment object.
275 """
276 env_loaded_deferred = self.agent.configure_environment()
277-
278- def verify_environment(result):
279- self.assertTrue(isinstance(result, Environment))
280- self.assertEqual(result.name, "myfirstenv")
281-
282- env_loaded_deferred.addCallback(verify_environment)
283-
284- def create_environment_node():
285- self.assertFalse(env_loaded_deferred.called)
286- return self.client.create(
287- "/environment", self.get_serialized_environment())
288-
289- reactor.callLater(0.3, create_environment_node)
290- return env_loaded_deferred
291+ reactor.callLater(
292+ 0.3, self.push_default_config, with_constraints=False)
293+ result = yield env_loaded_deferred
294+ self.assertTrue(isinstance(result, Environment))
295+ self.assertEqual(result.name, "firstenv")
296
297 @inlineCallbacks
298 def test_agent_with_existing_environment(self):
299 """An agent should load an existing environment to configure itself."""
300-
301- yield self.client.create(
302- "/environment", self.get_serialized_environment())
303+ yield self.push_default_config()
304
305 def verify_environment(result):
306 self.assertTrue(isinstance(result, Environment))
307- self.assertEqual(result.name, "myfirstenv")
308+ self.assertEqual(result.name, "firstenv")
309
310 d = self.agent.configure_environment()
311 d.addCallback(verify_environment)
312@@ -103,15 +84,13 @@
313 while the agent is processing the NoNodeException, it should detect
314 this and configure normally.
315 """
316- data = self.get_serialized_environment()
317 exists_and_watch = self.agent.client.exists_and_watch
318
319 mock_client = self.mocker.patch(self.agent.client)
320 mock_client.exists_and_watch("/environment")
321
322 def inject_creation(path):
323- zookeeper.create(
324- self.agent.client.handle, path, data, [ZOO_OPEN_ACL_UNSAFE])
325+ self.push_default_config(with_constraints=False)
326 return exists_and_watch(path)
327
328 self.mocker.call(inject_creation)
329@@ -131,8 +110,6 @@
330 @inlineCallbacks
331 def setUp(self):
332 yield super(ProvisioningAgentTest, self).setUp()
333- yield self.client.create(
334- "/environment", self.get_serialized_environment())
335 self.agent.set_watch_enabled(False)
336 yield self.agent.startService()
337 self.output = self.capture_logging("juju.agents.provision",
338@@ -268,8 +245,7 @@
339 If the environment changes the agent reconfigures itself
340 """
341 provider = self.agent.provider
342- data = self.get_serialized_environment()
343- yield self.client.set("/environment", data)
344+ yield self.push_default_config()
345 yield self.sleep(0.2)
346 self.assertNotIdentical(provider, self.agent.provider)
347
348@@ -314,14 +290,22 @@
349 If there's an error when processing changes, the agent should log
350 the error and continue.
351 """
352- machine_state0 = yield self.add_machine_state()
353- machine_state1 = yield self.add_machine_state()
354+ machine_state0 = yield self.add_machine_state(
355+ dummy_cs.parse(["cpu=10"]).with_series("series"))
356+ machine_state1 = yield self.add_machine_state(
357+ dummy_cs.parse(["cpu=20"]).with_series("series"))
358
359 mock_provider = self.mocker.patch(self.agent.provider)
360- mock_provider.start_machine({"machine-id": 0})
361+ mock_provider.start_machine({
362+ "machine-id": 0, "constraints": {
363+ "arch": "amd64", "cpu": 10, "mem": 512,
364+ "provider-type": "dummy", "ubuntu-series": "series"}})
365 self.mocker.result(fail(ProviderInteractionError()))
366
367- mock_provider.start_machine({"machine-id": 1})
368+ mock_provider.start_machine({
369+ "machine-id": 1, "constraints": {
370+ "arch": "amd64", "cpu": 20, "mem": 512,
371+ "provider-type": "dummy", "ubuntu-series": "series"}})
372 self.mocker.passthrough()
373 self.mocker.replay()
374
375@@ -476,8 +460,6 @@
376 @inlineCallbacks
377 def setUp(self):
378 yield super(FirewallManagerTest, self).setUp()
379- yield self.client.create(
380- "/environment", self.get_serialized_environment())
381 self.agent.set_watch_enabled(False)
382 yield self.agent.startService()
383
384@@ -509,11 +491,14 @@
385 # Modify services, while subsequently poking to ensure service
386 # watch is processed on each modification
387 yield self.add_service("wordpress")
388- yield self.poke_zk()
389+ while len(seen) < 1:
390+ yield self.poke_zk()
391 mysql = yield self.add_service("mysql")
392- yield self.poke_zk()
393+ while len(seen) < 2:
394+ yield self.poke_zk()
395 yield self.service_state_manager.remove_service_state(mysql)
396- yield self.poke_zk()
397+ while len(seen) < 3:
398+ yield self.poke_zk()
399
400 self.assertEqual(
401 seen,
402
403=== modified file 'juju/control/__init__.py'
404--- juju/control/__init__.py 2011-12-15 17:12:26 +0000
405+++ juju/control/__init__.py 2012-03-30 16:56:32 +0000
406@@ -12,6 +12,7 @@
407 import bootstrap
408 import config_get
409 import config_set
410+import constraints_get
411 import constraints_set
412 import debug_hooks
413 import debug_log
414@@ -39,6 +40,7 @@
415 bootstrap,
416 config_get,
417 config_set,
418+ constraints_get,
419 constraints_set,
420 debug_log,
421 debug_hooks,
422
423=== modified file 'juju/control/add_unit.py'
424--- juju/control/add_unit.py 2011-11-29 22:50:38 +0000
425+++ juju/control/add_unit.py 2012-03-30 16:56:32 +0000
426@@ -2,7 +2,8 @@
427
428 from twisted.internet.defer import inlineCallbacks
429
430-from juju.control.utils import get_environment
431+from juju.control import legacy
432+from juju.control.utils import get_environment, sync_environment_state
433 from juju.state.placement import place_unit
434 from juju.state.service import ServiceStateManager
435
436@@ -44,6 +45,10 @@
437 client = yield provider.connect()
438
439 try:
440+ yield legacy.check_environment(
441+ client, provider.get_legacy_config_keys())
442+ yield sync_environment_state(client, config, environment.name)
443+
444 service_manager = ServiceStateManager(client)
445 service_state = yield service_manager.get_service_state(service_name)
446 for i in range(num_units):
447
448=== modified file 'juju/control/bootstrap.py'
449--- juju/control/bootstrap.py 2011-09-22 13:23:00 +0000
450+++ juju/control/bootstrap.py 2012-03-30 16:56:32 +0000
451@@ -1,6 +1,7 @@
452 from twisted.internet.defer import inlineCallbacks
453
454-from juju.control.utils import get_environment
455+from juju.control import legacy
456+from juju.control.utils import expand_constraints, get_environment
457
458
459 def configure_subparser(subparsers):
460@@ -9,6 +10,11 @@
461 sub_parser.add_argument(
462 "--environment", "-e",
463 help="juju environment to operate in.")
464+ sub_parser.add_argument(
465+ "--constraints",
466+ help="default hardware constraints for this environment.",
467+ default=[],
468+ type=expand_constraints)
469 return sub_parser
470
471
472@@ -19,6 +25,14 @@
473 """
474 environment = get_environment(options)
475 provider = environment.get_machine_provider()
476+ legacy_keys = provider.get_legacy_config_keys()
477+ if legacy_keys:
478+ legacy.error(legacy_keys)
479+
480+ constraint_set = yield provider.get_constraint_set()
481+ constraints = constraint_set.parse(options.constraints)
482+ constraints = constraints.with_series(environment.default_series)
483+
484 options.log.info("Bootstrapping environment %r (type: %s)..." % (
485 environment.name, environment.type))
486- yield provider.bootstrap()
487+ yield provider.bootstrap(constraints)
488
489=== added file 'juju/control/constraints_get.py'
490--- juju/control/constraints_get.py 1970-01-01 00:00:00 +0000
491+++ juju/control/constraints_get.py 2012-03-30 16:56:32 +0000
492@@ -0,0 +1,75 @@
493+import argparse
494+import sys
495+import yaml
496+
497+from twisted.internet.defer import inlineCallbacks
498+
499+from juju.control.utils import get_environment, sync_environment_state
500+from juju.state.environment import EnvironmentStateManager
501+from juju.state.machine import MachineStateManager
502+from juju.state.service import ServiceStateManager
503+
504+
505+def configure_subparser(subparsers):
506+ sub_parser = subparsers.add_parser(
507+ "get-constraints",
508+ help=command.__doc__,
509+ formatter_class=argparse.RawDescriptionHelpFormatter,
510+ description=constraints_get.__doc__)
511+
512+ sub_parser.add_argument(
513+ "--environment", "-e",
514+ help="Environment to affect")
515+
516+ sub_parser.add_argument(
517+ "entities",
518+ nargs="*",
519+ help="names of machines, units or services")
520+
521+ return sub_parser
522+
523+
524+def command(options):
525+ """Show currently applicable constraints"""
526+ environment = get_environment(options)
527+ return constraints_get(
528+ options.environments, environment, options.entities, options.log)
529+
530+
531+@inlineCallbacks
532+def constraints_get(env_config, environment, entity_names, log):
533+ """
534+ Show the complete set of applicable constraints for each specified entity.
535+
536+ This will show the final computed values of all constraints (including
537+ internal constraints which cannot be set directly via set-constraints).
538+ """
539+ provider = environment.get_machine_provider()
540+ client = yield provider.connect()
541+ result = {}
542+ try:
543+ yield sync_environment_state(client, env_config, environment.name)
544+ if entity_names:
545+ msm = MachineStateManager(client)
546+ ssm = ServiceStateManager(client)
547+ for name in entity_names:
548+ if name.isdigit():
549+ kind = "machine"
550+ entity = yield msm.get_machine_state(name)
551+ elif "/" in name:
552+ kind = "service unit"
553+ entity = yield ssm.get_unit_state(name)
554+ else:
555+ kind = "service"
556+ entity = yield ssm.get_service_state(name)
557+ log.info("Fetching constraints for %s %s", kind, name)
558+ constraints = yield entity.get_constraints()
559+ result[name] = dict(constraints)
560+ else:
561+ esm = EnvironmentStateManager(client)
562+ log.info("Fetching constraints for environment")
563+ constraints = yield esm.get_constraints()
564+ result = dict(constraints)
565+ yaml.safe_dump(result, sys.stdout)
566+ finally:
567+ yield client.close()
568
569=== modified file 'juju/control/constraints_set.py'
570--- juju/control/constraints_set.py 2012-03-09 09:02:06 +0000
571+++ juju/control/constraints_set.py 2012-03-30 16:56:32 +0000
572@@ -2,8 +2,9 @@
573
574 from twisted.internet.defer import inlineCallbacks
575
576-from juju.control.utils import get_environment
577-from juju.machine.constraints import Constraints
578+from juju.control import legacy
579+from juju.control.utils import get_environment, sync_environment_state
580+from juju.state.environment import EnvironmentStateManager
581 from juju.state.service import ServiceStateManager
582
583
584@@ -34,36 +35,43 @@
585 """Set machine constraints for the environment, or for a named service.
586 """
587 environment = get_environment(options)
588+ env_config = options.environments
589 return constraints_set(
590- environment, options.service, options.constraints)
591+ env_config, environment, options.service, options.constraints)
592
593
594 @inlineCallbacks
595-def constraints_set(environment, service_name, constraint_strs):
596+def constraints_set(env_config, environment, service_name, constraint_strs):
597 """
598 Machine constraints allow you to pick the hardware to which your services
599 will be deployed. Examples:
600
601 $ juju set-constraints --service-name mysql mem=8G cpu=4
602
603- $ juju set-constraints ec2-instance-type=t1.micro
604-
605- "arch", "cpu" and "mem" are always available; other constraints are
606- provider-specific, and will be ignored if specified in an environment of
607- the wrong kind. The recognised constraints are currently:
608-
609- * arch (CPU architecture: x86/amd64/arm; unset by default)
610- * cpu (processing power in Amazon ECU; 1 by default)
611- * mem (memory in [MGT]iB; 512M by default)
612- * ec2-region (us-east-1 by default)
613- * ec2-zone (unset by default)
614- * ec2-instance-type (unset by default)
615- * orchestra-classes (unset by default)
616- * orchestra-name (unset by default)
617+ $ juju set-constraints instance-type=t1.micro
618+
619+ Available constraints vary by provider type, and will be ignored if not
620+ understood by the current environment's provider. The current set of
621+ available constraints across all providers is:
622+
623+ On Amazon EC2:
624+
625+ * arch (CPU architecture: i386/amd64/arm; amd64 by default)
626+ * cpu (processing power in Amazon ECU; 1 by default)
627+ * mem (memory in [MGT]iB; 512M by default)
628+ * instance-type (unset by default)
629+ * ec2-zone (unset by default)
630+
631+ On Orchestra:
632+
633+ * orchestra-classes (unset by default)
634+
635+ On MAAS:
636+
637+ * maas-name (unset by default)
638
639 Service settings, if specified, will override environment settings, which
640- will in turn override the juju defaults of mem=512M, cpu=1,
641- ec2-region=us-east-1.
642+ will in turn override the juju defaults of mem=512M, cpu=1, arch=amd64.
643
644 New constraints set on an entity will completely replace that entity's
645 pre-existing constraints.
646@@ -72,17 +80,22 @@
647 service constraints, just specify "name=" (rather than just not specifying
648 the constraint at all, which will cause it to inherit the environment's
649 value).
650+
651+ To entirely unset a constraint, specify "name=any".
652 """
653- constraints = Constraints.from_strs(environment.type, constraint_strs)
654-
655- if service_name is None:
656- raise NotImplementedError("Environment constraints not implemented")
657-
658 provider = environment.get_machine_provider()
659+ constraint_set = yield provider.get_constraint_set()
660+ constraints = constraint_set.parse(constraint_strs)
661 client = yield provider.connect()
662 try:
663- service_state_manager = ServiceStateManager(client)
664- service = yield service_state_manager.get_service_state(service_name)
665- yield service.set_constraints(constraints)
666+ yield legacy.check_constraints(client, constraint_strs)
667+ yield sync_environment_state(client, env_config, environment.name)
668+ if service_name is None:
669+ esm = EnvironmentStateManager(client)
670+ yield esm.set_constraints(constraints)
671+ else:
672+ ssm = ServiceStateManager(client)
673+ service = yield ssm.get_service_state(service_name)
674+ yield service.set_constraints(constraints)
675 finally:
676 yield client.close()
677
678=== modified file 'juju/control/deploy.py'
679--- juju/control/deploy.py 2012-03-22 16:15:02 +0000
680+++ juju/control/deploy.py 2012-03-30 16:56:32 +0000
681@@ -4,26 +4,20 @@
682
683 from twisted.internet.defer import inlineCallbacks
684
685-from juju.control.utils import get_environment, expand_path
686+from juju.control import legacy
687+from juju.control.utils import (
688+ expand_constraints, expand_path, get_environment, sync_environment_state)
689
690 from juju.charm.errors import ServiceConfigValueError
691 from juju.charm.publisher import CharmPublisher
692 from juju.charm.repository import resolve
693 from juju.errors import CharmError
694-from juju.machine.constraints import Constraints
695 from juju.state.endpoint import RelationEndpoint
696-from juju.state.environment import EnvironmentStateManager
697 from juju.state.placement import place_unit
698 from juju.state.relation import RelationStateManager
699 from juju.state.service import ServiceStateManager
700
701
702-def _expand_constraints(s):
703- if s:
704- return s.split(" ")
705- return []
706-
707-
708 def configure_subparser(subparsers):
709 sub_parser = subparsers.add_parser("deploy", help=command.__doc__,
710 description=deploy.__doc__)
711@@ -50,7 +44,7 @@
712 "--constraints",
713 help="Hardware constraints for the service",
714 default=[],
715- type=_expand_constraints)
716+ type=expand_constraints)
717
718 sub_parser.add_argument(
719 "charm", nargs=None,
720@@ -134,23 +128,23 @@
721 service_options = parse_config_options(
722 config_file, service_name, charm)
723
724- constraints = Constraints.from_strs(environment.type, constraint_strs)
725-
726 charm = yield repo.find(charm_url)
727 charm_id = str(charm_url.with_revision(charm.get_revision()))
728
729 provider = environment.get_machine_provider()
730 placement_policy = provider.get_placement_policy()
731+ constraint_set = yield provider.get_constraint_set()
732+ constraints = constraint_set.parse(constraint_strs)
733 client = yield provider.connect()
734
735 try:
736+ yield legacy.check_constraints(client, constraint_strs)
737+ yield legacy.check_environment(
738+ client, provider.get_legacy_config_keys())
739+ yield sync_environment_state(client, env_config, environment.name)
740+
741+ # Publish the charm to juju
742 storage = yield provider.get_file_storage()
743- service_manager = ServiceStateManager(client)
744- environment_state_manager = EnvironmentStateManager(client)
745- yield environment_state_manager.set_config_state(
746- env_config, environment.name)
747-
748- # Publish the charm to juju
749 publisher = CharmPublisher(client, storage)
750 yield publisher.add_charm(charm_id, charm)
751 result = yield publisher.publish()
752@@ -161,6 +155,7 @@
753 charm_state = result[0]
754
755 # Create the service state
756+ service_manager = ServiceStateManager(client)
757 service_state = yield service_manager.add_service_state(
758 service_name, charm_state, constraints)
759
760
761=== modified file 'juju/control/initialize.py'
762--- juju/control/initialize.py 2011-09-23 20:35:02 +0000
763+++ juju/control/initialize.py 2012-03-30 16:56:32 +0000
764@@ -1,4 +1,6 @@
765+from base64 import b64decode
766 import os
767+import yaml
768
769 from twisted.internet.defer import inlineCallbacks
770
771@@ -12,11 +14,13 @@
772 sub_parser.add_argument(
773 "--instance-id", required=True,
774 help="Provider instance id for the bootstrap node")
775-
776 sub_parser.add_argument(
777 "--admin-identity", required=True,
778 help="Admin access control identity for zookeeper ACLs")
779 sub_parser.add_argument(
780+ "--constraints-data", required=True,
781+ help="Base64-encoded yaml dump of the environment constraints data")
782+ sub_parser.add_argument(
783 "--provider-type", required=True,
784 help="Environment machine provider type")
785 return sub_parser
786@@ -30,10 +34,12 @@
787 zk_address = os.environ.get("ZOOKEEPER_ADDRESS", "127.0.0.1:2181")
788 client = yield ZookeeperClient(zk_address).connect()
789 try:
790+ constraints_data = yaml.load(b64decode(options.constraints_data))
791 hierarchy = StateHierarchy(
792 client,
793 options.admin_identity,
794 options.instance_id,
795+ constraints_data,
796 options.provider_type)
797 yield hierarchy.initialize()
798 finally:
799
800=== added file 'juju/control/legacy.py'
801--- juju/control/legacy.py 1970-01-01 00:00:00 +0000
802+++ juju/control/legacy.py 2012-03-30 16:56:32 +0000
803@@ -0,0 +1,41 @@
804+from twisted.internet.defer import inlineCallbacks
805+
806+from juju.errors import JujuError
807+from juju.state.environment import EnvironmentStateManager
808+
809+_ERROR = """
810+Your environments.yaml contains deprecated keys; they must not be used other
811+than in legacy deployments. The affected keys are:
812+
813+ %s
814+
815+This error can be resolved according to the instructions available at:
816+
817+ https://juju.ubuntu.com/DeprecatedEnvironmentSettings
818+"""
819+
820+
821+def error(keys):
822+ raise JujuError(_ERROR % "\n ".join(sorted(keys)))
823+
824+
825+@inlineCallbacks
826+def check_environment(client, keys):
827+ if not keys:
828+ return
829+ esm = EnvironmentStateManager(client)
830+ if not (yield esm.get_in_legacy_environment()):
831+ error(keys)
832+
833+
834+@inlineCallbacks
835+def check_constraints(client, constraint_strs):
836+ if not constraint_strs:
837+ return
838+ esm = EnvironmentStateManager(client)
839+ if (yield esm.get_in_legacy_environment()):
840+ raise JujuError(
841+ "Constraints are not valid in legacy deployments. To use machine "
842+ "constraints, please deploy your environment again from scratch. "
843+ "You can continue to use this environment as before, but any "
844+ "attempt to set constraints will fail.")
845
846=== modified file 'juju/control/terminate_machine.py'
847--- juju/control/terminate_machine.py 2011-09-15 18:50:23 +0000
848+++ juju/control/terminate_machine.py 2012-03-30 16:56:32 +0000
849@@ -2,6 +2,7 @@
850
851 from twisted.internet.defer import inlineCallbacks
852
853+from juju.control.utils import sync_environment_state
854 from juju.errors import CannotTerminateMachine
855 from juju.environment.errors import EnvironmentsConfigError
856 from juju.state.errors import MachineStateNotFound
857@@ -57,6 +58,7 @@
858 client = yield provider.connect()
859 terminated_machine_ids = []
860 try:
861+ yield sync_environment_state(client, config, environment.name)
862 machine_state_manager = MachineStateManager(client)
863 for machine_id in machine_ids:
864 if machine_id == 0:
865
866=== modified file 'juju/control/tests/test_add_relation.py'
867--- juju/control/tests/test_add_relation.py 2012-02-09 03:52:45 +0000
868+++ juju/control/tests/test_add_relation.py 2012-03-30 16:56:32 +0000
869@@ -1,5 +1,4 @@
870 import logging
871-import yaml
872
873 from twisted.internet.defer import inlineCallbacks
874
875@@ -15,12 +14,6 @@
876 @inlineCallbacks
877 def setUp(self):
878 yield super(ControlAddRelationTest, self).setUp()
879- config = {
880- "environments": {
881- "firstenv": {
882- "type": "dummy", "admin-secret": "homer"}}}
883- self.write_config(yaml.dump(config))
884- self.config.load()
885 self.output = self.capture_logging()
886 self.stderr = self.capture_stream("stderr")
887
888
889=== modified file 'juju/control/tests/test_add_unit.py'
890--- juju/control/tests/test_add_unit.py 2012-02-01 11:27:42 +0000
891+++ juju/control/tests/test_add_unit.py 2012-03-30 16:56:32 +0000
892@@ -1,7 +1,9 @@
893+from yaml import dump
894+
895 from twisted.internet.defer import inlineCallbacks
896-from yaml import dump
897
898 from juju.control import main
899+from juju.state.environment import EnvironmentStateManager
900
901 from .common import MachineControlToolTest
902
903@@ -11,11 +13,6 @@
904 @inlineCallbacks
905 def setUp(self):
906 yield super(ControlAddUnitTest, self).setUp()
907- config = {
908- "environments": {"firstenv": {"type": "dummy"}}}
909-
910- self.write_config(dump(config))
911- self.config.load()
912 self.service_state1 = yield self.add_service_from_charm("mysql")
913 self.service_unit1 = yield self.service_state1.add_unit_state()
914 self.machine_state1 = yield self.add_machine_state()
915@@ -36,9 +33,15 @@
916 finished = self.setup_cli_reactor()
917 self.setup_exit(0)
918 self.mocker.replay()
919+ # trash environment to check syncing
920+ yield self.client.delete("/environment")
921 main(["add-unit", "mysql"])
922 yield finished
923
924+ # verify the env state was synced
925+ esm = EnvironmentStateManager(self.client)
926+ yield esm.get_config()
927+
928 # verify the unit and its machine assignment.
929 unit_names = yield self.service_state1.get_unit_names()
930 self.assertEqual(len(unit_names), 2)
931@@ -121,9 +124,7 @@
932 "environments": {"firstenv": {
933 "placement": "local",
934 "type": "dummy"}}}
935-
936- self.write_config(dump(config))
937- self.config.load()
938+ yield self.push_config("firstenv", config)
939
940 ms0 = yield self.machine_state_manager.get_machine_state(0)
941 yield self.service_unit1.unassign_from_machine()
942@@ -149,3 +150,37 @@
943 self.output.getvalue())
944 # adding a second unit still assigns to machine 0 with local policy
945 yield self.assert_machine_assignments("mysql", [0, 0])
946+
947+ @inlineCallbacks
948+ def test_legacy_option_in_legacy_env(self):
949+ yield self.client.delete("/constraints")
950+
951+ finished = self.setup_cli_reactor()
952+ self.setup_exit(0)
953+ self.mocker.replay()
954+ main(["add-unit", "mysql"])
955+ yield finished
956+
957+ unit_names = yield self.service_state1.get_unit_names()
958+ self.assertEqual(len(unit_names), 2)
959+
960+ @inlineCallbacks
961+ def test_legacy_option_in_fresh_env(self):
962+ local_config = {
963+ "environments": {"firstenv": {
964+ "some-legacy-key": "blah",
965+ "type": "dummy"}}}
966+ self.write_config(dump(local_config))
967+ self.config.load()
968+
969+ finished = self.setup_cli_reactor()
970+ self.setup_exit(0)
971+ self.mocker.replay()
972+ main(["add-unit", "mysql"])
973+ yield finished
974+
975+ output = self.output.getvalue()
976+ self.assertIn(
977+ "Your environments.yaml contains deprecated keys", output)
978+ unit_names = yield self.service_state1.get_unit_names()
979+ self.assertEqual(len(unit_names), 1)
980
981=== modified file 'juju/control/tests/test_bootstrap.py'
982--- juju/control/tests/test_bootstrap.py 2011-09-22 13:23:00 +0000
983+++ juju/control/tests/test_bootstrap.py 2012-03-30 16:56:32 +0000
984@@ -19,32 +19,29 @@
985 config = {
986 "environments": {
987 "firstenv": {
988- "type": "dummy", "admin-secret": "homer"},
989+ "type": "dummy", "default-series": "homer"},
990 "secondenv": {
991- "type": "dummy", "admin-secret": "marge"}}}
992+ "type": "dummy", "default-series": "marge"}}}
993
994 self.write_config(dump(config))
995 finished = self.setup_cli_reactor()
996 self.setup_exit(0)
997
998- envs = set(("firstenv", "secondenv"))
999-
1000- def track_bootstrap_call(self):
1001- envs.remove(self.environment_name)
1002- return succeed(True)
1003-
1004 provider = self.mocker.patch(MachineProvider)
1005-
1006- provider.bootstrap()
1007- self.mocker.call(track_bootstrap_call, with_object=True)
1008+ provider.bootstrap({
1009+ "ubuntu-series": "homer",
1010+ "provider-type": "dummy",
1011+ "arch": "arm",
1012+ "cpu": 2.0,
1013+ "mem": 512.0})
1014+ self.mocker.result(succeed(True))
1015 self.mocker.replay()
1016
1017 self.capture_stream("stderr")
1018- main(["bootstrap", "-e", "firstenv"])
1019+ main(["bootstrap", "-e", "firstenv",
1020+ "--constraints", "arch=arm cpu=2"])
1021 yield finished
1022
1023- self.assertEqual(envs, set(["secondenv"]))
1024-
1025 lines = filter(None, self.log.getvalue().split("\n"))
1026 self.assertEqual(
1027 lines,
1028@@ -96,3 +93,25 @@
1029 msg = "Invalid environment 'thirdenv'"
1030 self.assertIn(msg, self.log.getvalue())
1031 self.assertIn(msg, output.getvalue())
1032+
1033+ @inlineCallbacks
1034+ def test_bootstrap_legacy_config_keys(self):
1035+ """
1036+ If the environment specified does not exist an error message is given.
1037+ """
1038+ config = {
1039+ "environments": {
1040+ "firstenv": {
1041+ "type": "dummy", "some-legacy-key": "blah"}}}
1042+ self.write_config(dump(config))
1043+ finished = self.setup_cli_reactor()
1044+ self.setup_exit(1)
1045+ self.mocker.replay()
1046+
1047+ output = self.capture_stream("stderr")
1048+ main(["bootstrap"])
1049+ yield finished
1050+
1051+ msg = "Your environments.yaml contains deprecated keys"
1052+ self.assertIn(msg, self.log.getvalue())
1053+ self.assertIn(msg, output.getvalue())
1054
1055=== modified file 'juju/control/tests/test_config_get.py'
1056--- juju/control/tests/test_config_get.py 2012-01-30 22:20:37 +0000
1057+++ juju/control/tests/test_config_get.py 2012-03-30 16:56:32 +0000
1058@@ -13,10 +13,6 @@
1059 @inlineCallbacks
1060 def setUp(self):
1061 yield super(ControlJujuGetTest, self).setUp()
1062- config = {
1063- "environments": {"firstenv": {"type": "dummy"}}}
1064- self.write_config(yaml.dump(config))
1065- self.config.load()
1066 self.stderr = self.capture_stream("stderr")
1067
1068 @inlineCallbacks
1069
1070=== modified file 'juju/control/tests/test_config_set.py'
1071--- juju/control/tests/test_config_set.py 2012-03-09 13:24:58 +0000
1072+++ juju/control/tests/test_config_set.py 2012-03-30 16:56:32 +0000
1073@@ -12,10 +12,6 @@
1074 @inlineCallbacks
1075 def setUp(self):
1076 yield super(ControlJujuSetTest, self).setUp()
1077- config = {
1078- "environments": {"firstenv": {"type": "dummy"}}}
1079- self.write_config(dump(config))
1080- self.config.load()
1081 self.service_state = yield self.add_service_from_charm("wordpress")
1082 self.service_unit = yield self.service_state.add_unit_state()
1083 self.environment = self.config.get_default()
1084
1085=== added file 'juju/control/tests/test_constraints_get.py'
1086--- juju/control/tests/test_constraints_get.py 1970-01-01 00:00:00 +0000
1087+++ juju/control/tests/test_constraints_get.py 2012-03-30 16:56:32 +0000
1088@@ -0,0 +1,116 @@
1089+import yaml
1090+
1091+from twisted.internet.defer import inlineCallbacks
1092+
1093+from juju.control import main
1094+from juju.machine.tests.test_constraints import dummy_cs
1095+from juju.state.environment import EnvironmentStateManager
1096+
1097+from .common import MachineControlToolTest
1098+
1099+env_log = "Fetching constraints for environment"
1100+machine_log = "Fetching constraints for machine 1"
1101+service_log = "Fetching constraints for service mysql"
1102+unit_log = "Fetching constraints for service unit mysql/0"
1103+
1104+
1105+class ConstraintsGetTest(MachineControlToolTest):
1106+
1107+ @inlineCallbacks
1108+ def setUp(self):
1109+ yield super(ConstraintsGetTest, self).setUp()
1110+ env_constraints = dummy_cs.parse(["mem=1024"])
1111+ esm = EnvironmentStateManager(self.client)
1112+ yield esm.set_constraints(env_constraints)
1113+ self.expect_env = {
1114+ "arch": "amd64", "cpu": 1.0, "mem": 1024.0,
1115+ "provider-type": "dummy", "ubuntu-series": None}
1116+
1117+ service_constraints = dummy_cs.parse(["cpu=10"])
1118+ service = yield self.add_service_from_charm(
1119+ "mysql", constraints=service_constraints)
1120+ # unit will snapshot the state of service when added
1121+ unit = yield service.add_unit_state()
1122+ self.expect_unit = {
1123+ "arch": "amd64", "cpu": 10.0, "mem": 1024.0,
1124+ "provider-type": "dummy", "ubuntu-series": "series"}
1125+
1126+ # machine gets its own constraints
1127+ machine_constraints = dummy_cs.parse(["cpu=15", "mem=8G"])
1128+ machine = yield self.add_machine_state(
1129+ constraints=machine_constraints.with_series("series"))
1130+ self.expect_machine = {
1131+ "arch": "amd64", "cpu": 15.0, "mem": 8192.0,
1132+ "provider-type": "dummy", "ubuntu-series": "series"}
1133+ yield unit.assign_to_machine(machine)
1134+
1135+ # service gets new constraints, leaves unit untouched
1136+ yield service.set_constraints(dummy_cs.parse(["mem=16G"]))
1137+ self.expect_service = {
1138+ "arch": "amd64", "cpu": 1.0, "mem": 16384.0,
1139+ "provider-type": "dummy", "ubuntu-series": "series"}
1140+
1141+ self.log = self.capture_logging()
1142+ self.stdout = self.capture_stream("stdout")
1143+ self.finished = self.setup_cli_reactor()
1144+ self.setup_exit(0)
1145+ self.mocker.replay()
1146+
1147+ def assert_messages(self, *messages):
1148+ log = self.log.getvalue()
1149+ for message in messages:
1150+ self.assertIn(message, log)
1151+
1152+ @inlineCallbacks
1153+ def test_env(self):
1154+ main(["get-constraints"])
1155+ yield self.finished
1156+ result = yaml.load(self.stdout.getvalue())
1157+ self.assertEquals(result, self.expect_env)
1158+ self.assert_messages(env_log)
1159+
1160+ @inlineCallbacks
1161+ def test_service(self):
1162+ main(["get-constraints", "mysql"])
1163+ yield self.finished
1164+ result = yaml.load(self.stdout.getvalue())
1165+ self.assertEquals(result, {"mysql": self.expect_service})
1166+ self.assert_messages(service_log)
1167+
1168+ @inlineCallbacks
1169+ def test_unit(self):
1170+ main(["get-constraints", "mysql/0"])
1171+ yield self.finished
1172+ result = yaml.load(self.stdout.getvalue())
1173+ self.assertEquals(result, {"mysql/0": self.expect_unit})
1174+ self.assert_messages(unit_log)
1175+
1176+ @inlineCallbacks
1177+ def test_machine(self):
1178+ main(["get-constraints", "1"])
1179+ yield self.finished
1180+ result = yaml.load(self.stdout.getvalue())
1181+ self.assertEquals(result, {"1": self.expect_machine})
1182+ self.assert_messages(machine_log)
1183+
1184+ @inlineCallbacks
1185+ def test_all(self):
1186+ main(["get-constraints", "mysql", "mysql/0", "1"])
1187+ yield self.finished
1188+ result = yaml.load(self.stdout.getvalue())
1189+ expect = {"mysql": self.expect_service,
1190+ "mysql/0": self.expect_unit,
1191+ "1": self.expect_machine}
1192+ self.assertEquals(result, expect)
1193+ self.assert_messages(service_log, unit_log, machine_log)
1194+
1195+ @inlineCallbacks
1196+ def test_syncs_environment(self):
1197+ """If the environment were not synced, it would be impossible to create
1198+ the Constraints, so tool success proves sync."""
1199+ yield self.client.delete("/environment")
1200+ main(["get-constraints", "mysql/0"])
1201+ yield self.finished
1202+ result = yaml.load(self.stdout.getvalue())
1203+ self.assertEquals(result, {"mysql/0": self.expect_unit})
1204+ self.assert_messages(unit_log)
1205
1206=== modified file 'juju/control/tests/test_constraints_set.py'
1207--- juju/control/tests/test_constraints_set.py 2012-03-09 09:02:06 +0000
1208+++ juju/control/tests/test_constraints_set.py 2012-03-30 16:56:32 +0000
1209@@ -1,8 +1,7 @@
1210-from yaml import dump
1211-
1212 from twisted.internet.defer import inlineCallbacks
1213
1214 from juju.control import main
1215+from juju.state.environment import EnvironmentStateManager
1216
1217 from .common import MachineControlToolTest
1218
1219@@ -12,11 +11,6 @@
1220 @inlineCallbacks
1221 def setUp(self):
1222 yield super(ControlSetConstraintsTest, self).setUp()
1223- config = {
1224- "environments": {"firstenv": {"type": "dummy"}}}
1225-
1226- self.write_config(dump(config))
1227- self.config.load()
1228 self.service_state = yield self.add_service_from_charm("mysql")
1229 self.output = self.capture_logging()
1230 self.stderr = self.capture_stream("stderr")
1231@@ -31,7 +25,7 @@
1232
1233 constraints = yield self.service_state.get_constraints()
1234 expect = {
1235- "arch": None, "cpu": 8, "mem": 1024,
1236+ "arch": "amd64", "cpu": 8, "mem": 1024,
1237 "provider-type": "dummy", "ubuntu-series": "series"}
1238 self.assertEquals(constraints, expect)
1239
1240@@ -53,15 +47,32 @@
1241
1242 @inlineCallbacks
1243 def test_environment_constraint(self):
1244- initial_constraints = yield self.service_state.get_constraints()
1245- finished = self.setup_cli_reactor()
1246- self.setup_exit(1)
1247- self.mocker.replay()
1248- main(["set-constraints", "arch=arm"])
1249+ yield self.client.delete("/environment")
1250+ finished = self.setup_cli_reactor()
1251+ self.setup_exit(0)
1252+ self.mocker.replay()
1253+ main(["set-constraints", "arch=arm", "cpu=any"])
1254+ yield finished
1255+
1256+ esm = EnvironmentStateManager(self.client)
1257+ yield esm.get_config()
1258+ constraints = yield esm.get_constraints()
1259+ self.assertEquals(constraints, {
1260+ "ubuntu-series": None,
1261+ "provider-type": "dummy",
1262+ "arch": "arm",
1263+ "cpu": None,
1264+ "mem": 512.0})
1265+
1266+ @inlineCallbacks
1267+ def test_legacy_environment(self):
1268+ yield self.client.delete("/constraints")
1269+ finished = self.setup_cli_reactor()
1270+ self.setup_exit(0)
1271+ self.mocker.replay()
1272+ main(["set-constraints", "arch=arm", "cpu=any"])
1273 yield finished
1274
1275 self.assertIn(
1276- "Environment constraints not implemented", self.stderr.getvalue())
1277-
1278- constraints = yield self.service_state.get_constraints()
1279- self.assertEquals(constraints, initial_constraints)
1280+ "Constraints are not valid in legacy deployments.",
1281+ self.stderr.getvalue())
1282
1283=== modified file 'juju/control/tests/test_control.py'
1284--- juju/control/tests/test_control.py 2011-09-15 19:24:47 +0000
1285+++ juju/control/tests/test_control.py 2012-03-30 16:56:32 +0000
1286@@ -4,7 +4,6 @@
1287
1288 from StringIO import StringIO
1289 from argparse import Namespace
1290-from yaml import dump
1291
1292 from twisted.internet.defer import inlineCallbacks
1293
1294@@ -12,6 +11,7 @@
1295 from juju.control import setup_logging, main, setup_parser
1296 from juju.control.options import ensure_abs_path
1297 from juju.control.command import Commander
1298+from juju.state.tests.common import StateTestBase
1299
1300 from juju.lib.testing import TestCase
1301
1302@@ -36,17 +36,12 @@
1303 self.fail("EnvironmentsConfigError not raised")
1304
1305
1306-class ControlOutputTest(ControlToolTest):
1307+class ControlOutputTest(ControlToolTest, StateTestBase):
1308
1309+ @inlineCallbacks
1310 def setUp(self):
1311- super(ControlOutputTest, self).setUp()
1312- config = {
1313- "environments": {
1314- "firstenv": {
1315- "type": "dummy", "admin-secret": "homer"},
1316- "secondenv": {
1317- "type": "dummy", "admin-secret": "marge"}}}
1318- self.write_config(dump(config))
1319+ yield super(ControlOutputTest, self).setUp()
1320+ yield self.push_default_config()
1321
1322 def test_sans_args_produces_help(self):
1323 """
1324
1325=== modified file 'juju/control/tests/test_debug_hooks.py'
1326--- juju/control/tests/test_debug_hooks.py 2012-02-01 11:27:42 +0000
1327+++ juju/control/tests/test_debug_hooks.py 2012-03-30 16:56:32 +0000
1328@@ -1,8 +1,6 @@
1329 import logging
1330 import os
1331
1332-import yaml
1333-
1334 from twisted.internet.defer import (
1335 inlineCallbacks, returnValue, succeed, Deferred)
1336
1337@@ -22,13 +20,6 @@
1338 @inlineCallbacks
1339 def setUp(self):
1340 yield super(ControlDebugHookTest, self).setUp()
1341- config = {
1342- "environments": {
1343- "firstenv": {
1344- "type": "dummy", "admin-secret": "homer"}}}
1345- self.write_config(yaml.dump(config))
1346- self.config.load()
1347-
1348 self.environment = self.config.get_default()
1349 self.provider = self.environment.get_machine_provider()
1350
1351
1352=== modified file 'juju/control/tests/test_debug_log.py'
1353--- juju/control/tests/test_debug_log.py 2011-09-15 19:24:47 +0000
1354+++ juju/control/tests/test_debug_log.py 2012-03-30 16:56:32 +0000
1355@@ -1,5 +1,4 @@
1356 import json
1357-import yaml
1358
1359 from twisted.internet.defer import inlineCallbacks
1360
1361@@ -13,12 +12,7 @@
1362 @inlineCallbacks
1363 def setUp(self):
1364 yield super(ControlDebugLogTest, self).setUp()
1365- config = {
1366- "environments": {
1367- "firstenv": {
1368- "type": "dummy", "admin-secret": "homer"}}}
1369- self.write_config(yaml.dump(config))
1370- self.config.load()
1371+ yield self.push_default_config()
1372
1373 @inlineCallbacks
1374 def test_replay(self):
1375
1376=== modified file 'juju/control/tests/test_deploy.py'
1377--- juju/control/tests/test_deploy.py 2012-03-09 20:13:36 +0000
1378+++ juju/control/tests/test_deploy.py 2012-03-30 16:56:32 +0000
1379@@ -32,7 +32,6 @@
1380 @inlineCallbacks
1381 def setUp(self):
1382 yield super(ControlDeployTest, self).setUp()
1383-
1384 config = {
1385 "environments": {
1386 "firstenv": {
1387@@ -40,8 +39,7 @@
1388 "admin-secret": "homer",
1389 "placement": "unassigned",
1390 "default-series": "series"}}}
1391- self.write_config(yaml.dump(config))
1392- self.config.load()
1393+ yield self.push_config("firstenv", config)
1394
1395 def test_deploy_multiple_environments_none_specified(self):
1396 """
1397@@ -228,7 +226,7 @@
1398 self.assertEquals(charm_id, "local:series/sample-2")
1399 constraints = yield service_state.get_constraints()
1400 expect_constraints = {
1401- "arch": None, "cpu": 123, "mem": 512,
1402+ "arch": "amd64", "cpu": 123, "mem": 512,
1403 "provider-type": "dummy", "ubuntu-series": "series"}
1404 self.assertEquals(constraints, expect_constraints)
1405
1406@@ -490,9 +488,7 @@
1407 "placement": "local",
1408 "type": "dummy",
1409 "default-series": "series"}}}
1410-
1411- self.write_config(yaml.dump(config))
1412- self.config.load()
1413+ yield self.push_config("firstenv", config)
1414
1415 finished = self.setup_cli_reactor()
1416 self.setup_exit(0)
1417@@ -508,3 +504,66 @@
1418 unit = units[0]
1419 machine_id = yield unit.get_assigned_machine_id()
1420 self.assertEqual(machine_id, 0)
1421+
1422+ @inlineCallbacks
1423+ def test_deploy_legacy_keys_in_legacy_env(self):
1424+ yield self.client.delete("/constraints")
1425+
1426+ finished = self.setup_cli_reactor()
1427+ self.setup_exit(0)
1428+ self.mocker.replay()
1429+
1430+ main(["deploy", "--repository", self.unbundled_repo_path,
1431+ "local:sample", "beekeeper"])
1432+ yield finished
1433+
1434+ service_manager = ServiceStateManager(self.client)
1435+ yield service_manager.get_service_state("beekeeper")
1436+
1437+ @inlineCallbacks
1438+ def test_deploy_legacy_keys_in_fresh_env(self):
1439+ yield self.push_default_config()
1440+ local_config = {
1441+ "environments": {"firstenv": {
1442+ "type": "dummy",
1443+ "some-legacy-key": "blah",
1444+ "default-series": "series"}}}
1445+ self.write_config(yaml.dump(local_config))
1446+ self.config.load()
1447+ finished = self.setup_cli_reactor()
1448+ self.setup_exit(0)
1449+ self.mocker.replay()
1450+ stderr = self.capture_stream("stderr")
1451+
1452+ main(["deploy", "--repository", self.unbundled_repo_path,
1453+ "local:sample", "beekeeper"])
1454+ yield finished
1455+
1456+ self.assertIn(
1457+ "Your environments.yaml contains deprecated keys",
1458+ stderr.getvalue())
1459+ service_manager = ServiceStateManager(self.client)
1460+ yield self.assertFailure(
1461+ service_manager.get_service_state("beekeeper"),
1462+ ServiceStateNotFound)
1463+
1464+ @inlineCallbacks
1465+ def test_deploy_constraints_in_legacy_env(self):
1466+ yield self.client.delete("/constraints")
1467+
1468+ finished = self.setup_cli_reactor()
1469+ self.setup_exit(0)
1470+ self.mocker.replay()
1471+ stderr = self.capture_stream("stderr")
1472+
1473+ main(["deploy", "--repository", self.unbundled_repo_path,
1474+ "local:sample", "beekeeper", "--constraints", "arch=i386"])
1475+ yield finished
1476+
1477+ self.assertIn(
1478+ "Constraints are not valid in legacy deployments.",
1479+ stderr.getvalue())
1480+ service_manager = ServiceStateManager(self.client)
1481+ yield self.assertFailure(
1482+ service_manager.get_service_state("beekeeper"),
1483+ ServiceStateNotFound)
1484
1485=== modified file 'juju/control/tests/test_destroy_service.py'
1486--- juju/control/tests/test_destroy_service.py 2011-09-15 19:24:47 +0000
1487+++ juju/control/tests/test_destroy_service.py 2012-03-30 16:56:32 +0000
1488@@ -1,5 +1,4 @@
1489 from twisted.internet.defer import inlineCallbacks
1490-from yaml import dump
1491
1492 from juju.control import main
1493
1494@@ -15,11 +14,7 @@
1495 @inlineCallbacks
1496 def setUp(self):
1497 yield super(ControlStopServiceTest, self).setUp()
1498- config = {
1499- "environments": {"firstenv": {"type": "dummy"}}}
1500
1501- self.write_config(dump(config))
1502- self.config.load()
1503 self.service_state1 = yield self.add_service_from_charm("mysql")
1504 self.service1_unit = yield self.service_state1.add_unit_state()
1505 self.service_state2 = yield self.add_service_from_charm("wordpress")
1506
1507=== modified file 'juju/control/tests/test_initialize.py'
1508--- juju/control/tests/test_initialize.py 2011-09-23 20:35:02 +0000
1509+++ juju/control/tests/test_initialize.py 2012-03-30 16:56:32 +0000
1510@@ -1,3 +1,6 @@
1511+from base64 import b64encode
1512+from yaml import safe_dump
1513+
1514 from twisted.internet.defer import succeed
1515
1516 from txzookeeper import ZookeeperClient
1517@@ -26,7 +29,27 @@
1518 self.setup_exit(0)
1519 self.mocker.replay()
1520
1521- admin(["initialize",
1522- "--instance-id", "foobar",
1523- "--admin-identity", "admin:genie",
1524+ constraints_data = b64encode(safe_dump({
1525+ "ubuntu-series": "foo", "provider-type": "bar"}))
1526+
1527+ admin(["initialize",
1528+ "--instance-id", "foobar",
1529+ "--admin-identity", "admin:genie",
1530+ "--constraints-data", constraints_data,
1531+ "--provider-type", "dummy"])
1532+
1533+ def test_bad_constraints_data(self):
1534+ """Test that failing to unpack --constraints-data aborts initialize"""
1535+ client = self.mocker.patch(ZookeeperClient)
1536+ self.setup_cli_reactor()
1537+ client.connect()
1538+ self.mocker.result(succeed(client))
1539+ self.capture_stream('stderr')
1540+ self.setup_exit(1)
1541+ self.mocker.replay()
1542+
1543+ admin(["initialize",
1544+ "--instance-id", "foobar",
1545+ "--admin-identity", "admin:genie",
1546+ "--constraints-data", "zaphod's just this guy, you know?",
1547 "--provider-type", "dummy"])
1548
1549=== modified file 'juju/control/tests/test_remove_unit.py'
1550--- juju/control/tests/test_remove_unit.py 2012-02-01 11:27:42 +0000
1551+++ juju/control/tests/test_remove_unit.py 2012-03-30 16:56:32 +0000
1552@@ -2,7 +2,6 @@
1553 import sys
1554
1555 from twisted.internet.defer import inlineCallbacks
1556-from yaml import dump
1557 import zookeeper
1558
1559 from .common import ControlToolTest
1560@@ -18,11 +17,6 @@
1561 @inlineCallbacks
1562 def setUp(self):
1563 yield super(ControlRemoveUnitTest, self).setUp()
1564- config = {
1565- "environments": {"firstenv": {"type": "dummy"}}}
1566-
1567- self.write_config(dump(config))
1568- self.config.load()
1569
1570 self.environment = self.config.get_default()
1571 self.provider = self.environment.get_machine_provider()
1572
1573=== modified file 'juju/control/tests/test_resolved.py'
1574--- juju/control/tests/test_resolved.py 2012-03-27 22:06:24 +0000
1575+++ juju/control/tests/test_resolved.py 2012-03-30 16:56:32 +0000
1576@@ -1,5 +1,4 @@
1577 from twisted.internet.defer import inlineCallbacks, returnValue
1578-from yaml import dump
1579
1580 from juju.control import main
1581 from juju.control.tests.common import ControlToolTest
1582@@ -20,11 +19,6 @@
1583 @inlineCallbacks
1584 def setUp(self):
1585 yield super(ControlResolvedTest, self).setUp()
1586- config = {
1587- "environments": {"firstenv": {"type": "dummy"}}}
1588-
1589- self.write_config(dump(config))
1590- self.config.load()
1591
1592 yield self.add_relation_state("wordpress", "mysql")
1593 yield self.add_relation_state("wordpress", "varnish")
1594
1595=== modified file 'juju/control/tests/test_scp.py'
1596--- juju/control/tests/test_scp.py 2012-02-01 11:27:42 +0000
1597+++ juju/control/tests/test_scp.py 2012-03-30 16:56:32 +0000
1598@@ -19,14 +19,7 @@
1599 @inlineCallbacks
1600 def setUp(self):
1601 yield super(SCPTest, self).setUp()
1602- config = {
1603- "environments": {
1604- "firstenv": {
1605- "type": "dummy", "admin-secret": "homer"}}}
1606- self.write_config(dump(config))
1607 self.setup_exit(0)
1608-
1609- self.config.load()
1610 self.environment = self.config.get_default()
1611 self.provider = self.environment.get_machine_provider()
1612
1613@@ -53,10 +46,6 @@
1614 @inlineCallbacks
1615 def test_scp_unit_name(self):
1616 """Verify scp command is invoked against the host for a unit name."""
1617- mock_environment = self.mocker.patch(Environment)
1618- mock_environment.get_machine_provider()
1619- self.mocker.result(self.provider)
1620-
1621 # Verify expected call against scp
1622 mock_exec = self.mocker.replace(os.execvp)
1623 mock_exec("scp", [
1624@@ -79,6 +68,8 @@
1625 @inlineCallbacks
1626 def test_scp_machine_id(self):
1627 """Verify scp command is invoked against the host for a machine ID."""
1628+ # We need to do this because separate instances of DummyProvider don't
1629+ # share instance state.
1630 mock_environment = self.mocker.patch(Environment)
1631 mock_environment.get_machine_provider()
1632 self.mocker.result(self.provider)
1633@@ -111,10 +102,6 @@
1634
1635 $ juju scp -o "ConnectTimeout 60" foo mysql/0:/foo/bar
1636 """
1637- mock_environment = self.mocker.patch(Environment)
1638- mock_environment.get_machine_provider()
1639- self.mocker.result(self.provider)
1640-
1641 # Verify expected call against scp
1642 mock_exec = self.mocker.replace(os.execvp)
1643 mock_exec("scp", [
1644@@ -141,11 +128,6 @@
1645 @inlineCallbacks
1646 def setUp(self):
1647 yield super(ParseErrorsTest, self).setUp()
1648- config = {
1649- "environments": {"firstenv": {"type": "dummy"}}}
1650-
1651- self.write_config(dump(config))
1652- self.config.load()
1653 self.stderr = self.capture_stream("stderr")
1654
1655 def test_passthrough_args_parse_error(self):
1656
1657=== modified file 'juju/control/tests/test_ssh.py'
1658--- juju/control/tests/test_ssh.py 2012-03-23 02:13:50 +0000
1659+++ juju/control/tests/test_ssh.py 2012-03-30 16:56:32 +0000
1660@@ -23,14 +23,7 @@
1661 @inlineCallbacks
1662 def setUp(self):
1663 yield super(ControlShellTest, self).setUp()
1664- config = {
1665- "environments": {
1666- "firstenv": {
1667- "type": "dummy", "admin-secret": "homer"}}}
1668- self.write_config(dump(config))
1669 self.setup_exit(0)
1670-
1671- self.config.load()
1672 self.environment = self.config.get_default()
1673 self.provider = self.environment.get_machine_provider()
1674
1675@@ -346,10 +339,6 @@
1676 def test_shell_with_unassigned_unit(self):
1677 """If the service unit is not assigned, attempting to
1678 connect, raises an error."""
1679- mock_environment = self.mocker.patch(Environment)
1680- mock_environment.get_machine_provider()
1681- self.mocker.result(self.provider)
1682-
1683 finished = self.setup_cli_reactor()
1684 self.mocker.replay()
1685
1686@@ -384,11 +373,6 @@
1687 @inlineCallbacks
1688 def setUp(self):
1689 yield super(ParseErrorsTest, self).setUp()
1690- config = {
1691- "environments": {"firstenv": {"type": "dummy"}}}
1692-
1693- self.write_config(dump(config))
1694- self.config.load()
1695 self.stderr = self.capture_stream("stderr")
1696
1697 def test_passthrough_args_parse_error(self):
1698
1699=== modified file 'juju/control/tests/test_status.py'
1700--- juju/control/tests/test_status.py 2012-03-29 02:50:29 +0000
1701+++ juju/control/tests/test_status.py 2012-03-30 16:56:32 +0000
1702@@ -46,14 +46,6 @@
1703 yield settings.set_provider_type("dummy")
1704 self.log = self.capture_logging()
1705
1706- config = {
1707- "environments": {
1708- "firstenv": {
1709- "type": "dummy",
1710- "admin-secret": "homer"}}}
1711- self.write_config(yaml.dump(config))
1712- self.config.load()
1713-
1714 self.environment = self.config.get_default()
1715 self.provider = self.environment.get_machine_provider()
1716 self.machine_count = 0
1717
1718=== modified file 'juju/control/tests/test_terminate_machine.py'
1719--- juju/control/tests/test_terminate_machine.py 2012-01-09 16:57:13 +0000
1720+++ juju/control/tests/test_terminate_machine.py 2012-03-30 16:56:32 +0000
1721@@ -1,5 +1,4 @@
1722 import logging
1723-import yaml
1724
1725 from twisted.internet.defer import inlineCallbacks
1726
1727@@ -7,6 +6,7 @@
1728 from juju.control.tests.common import MachineControlToolTest
1729 from juju.errors import CannotTerminateMachine
1730 from juju.state.errors import MachineStateInUse, MachineStateNotFound
1731+from juju.state.environment import EnvironmentStateManager
1732
1733
1734 class ControlTerminateMachineTest(MachineControlToolTest):
1735@@ -14,12 +14,6 @@
1736 @inlineCallbacks
1737 def setUp(self):
1738 yield super(ControlTerminateMachineTest, self).setUp()
1739- config = {
1740- "environments": {
1741- "firstenv": {
1742- "type": "dummy", "admin-secret": "homer"}}}
1743- self.write_config(yaml.dump(config))
1744- self.config.load()
1745 self.output = self.capture_logging()
1746 self.stderr = self.capture_stream("stderr")
1747
1748@@ -113,8 +107,16 @@
1749 yield wordpress_unit_state.unassign_from_machine()
1750 yield mysql_unit_state.unassign_from_machine()
1751 yield self.assert_machine_states([0, 1, 2, 3], [])
1752+
1753+ # trash environment to check syncing
1754+ yield self.client.delete("/environment")
1755 main(["terminate-machine", "1", "3"])
1756 yield wait_on_reactor_stopped
1757+
1758+ # check environment synced
1759+ esm = EnvironmentStateManager(self.client)
1760+ yield esm.get_config()
1761+
1762 self.assertIn(
1763 "Machines terminated: 1, 3", self.output.getvalue())
1764 yield self.assert_machine_states([0, 2], [1, 3])
1765
1766=== modified file 'juju/control/tests/test_upgrade_charm.py'
1767--- juju/control/tests/test_upgrade_charm.py 2012-03-26 17:22:23 +0000
1768+++ juju/control/tests/test_upgrade_charm.py 2012-03-30 16:56:32 +0000
1769@@ -56,11 +56,7 @@
1770 @inlineCallbacks
1771 def setUp(self):
1772 yield super(ControlCharmUpgradeTest, self).setUp()
1773- config = {
1774- "environments": {"firstenv": {"type": "dummy"}}}
1775
1776- self.write_config(dump(config))
1777- self.config.load()
1778 self.service_state1 = yield self.add_service_from_charm("mysql")
1779 self.service_unit1 = yield self.service_state1.add_unit_state()
1780
1781@@ -459,12 +455,6 @@
1782 @inlineCallbacks
1783 def setUp(self):
1784 yield super(RemoteUpgradeCharmTest, self).setUp()
1785- config = {
1786- "environments": {"firstenv": {"type": "dummy"}}}
1787-
1788- self.write_config(dump(config))
1789- self.config.load()
1790-
1791 charm = CharmDirectory(os.path.join(
1792 test_repository_path, "series", "mysql"))
1793 self.charm_state_manager.add_charm_state(
1794
1795=== modified file 'juju/control/tests/test_utils.py'
1796--- juju/control/tests/test_utils.py 2012-03-09 13:24:58 +0000
1797+++ juju/control/tests/test_utils.py 2012-03-30 16:56:32 +0000
1798@@ -24,12 +24,6 @@
1799 @inlineCallbacks
1800 def setUp(self):
1801 yield super(LookupTest, self).setUp()
1802- config = {
1803- "environments": {
1804- "firstenv": {
1805- "type": "dummy", "admin-secret": "homer"}}}
1806- self.write_config(dump(config))
1807- self.config.load()
1808 self.environment = self.config.get_default()
1809 self.provider = self.environment.get_machine_provider()
1810
1811
1812=== modified file 'juju/control/utils.py'
1813--- juju/control/utils.py 2012-02-15 23:32:23 +0000
1814+++ juju/control/utils.py 2012-03-30 16:56:32 +0000
1815@@ -5,6 +5,7 @@
1816
1817 from juju.environment.errors import EnvironmentsConfigError
1818 from juju.state.errors import ServiceUnitStateMachineNotAssigned
1819+from juju.state.environment import EnvironmentStateManager
1820 from juju.state.machine import MachineStateManager
1821 from juju.state.service import ServiceStateManager
1822
1823@@ -20,6 +21,26 @@
1824 return environment
1825
1826
1827+def sync_environment_state(client, config, name):
1828+ """Push the local environment config to zookeeper.
1829+
1830+ This needs to be done:
1831+
1832+ * On any command which can cause the provisioning agent to take action
1833+ against the provider (ie create/destroy a machine), because the PA
1834+ needs to use credentials stored in the environment config to do so.
1835+ * On any command which uses constraints-related code (even if indirectly)
1836+ because Constraints objects are provider-specific, and need to be
1837+ created with the help of a MachineProvider; and the only way state code
1838+ can get a MachineProvider is by getting one from ZK (we certainly don't
1839+ want to thread the relevant provider from juju.control and/or the PA
1840+ itself all the way through the state code). So, we sync, to ensure
1841+ that state code can use an EnvironmentStateManager to get a provider.
1842+ """
1843+ esm = EnvironmentStateManager(client)
1844+ return esm.set_config_state(config, name)
1845+
1846+
1847 @inlineCallbacks
1848 def get_ip_address_for_machine(client, provider, machine_id):
1849 """Returns public DNS name and machine state for the machine id.
1850@@ -59,6 +80,12 @@
1851 return os.path.abspath(os.path.expanduser(p))
1852
1853
1854+def expand_constraints(s):
1855+ if s:
1856+ return s.split(" ")
1857+ return []
1858+
1859+
1860 class ParseError(Exception):
1861 """Used to support returning custom parse errors in passthrough parsing.
1862
1863
1864=== modified file 'juju/environment/config.py'
1865--- juju/environment/config.py 2012-02-29 20:38:01 +0000
1866+++ juju/environment/config.py 2012-03-30 16:56:32 +0000
1867@@ -4,20 +4,10 @@
1868
1869 from juju.environment.environment import Environment
1870 from juju.environment.errors import EnvironmentsConfigError
1871-from juju.errors import (
1872- FileAlreadyExists,
1873- FileNotFound,
1874- )
1875+from juju.errors import FileAlreadyExists, FileNotFound
1876 from juju.lib.schema import (
1877- Constant,
1878- Dict,
1879- KeyDict,
1880- OAuthString,
1881- OneOf,
1882- SchemaError,
1883- SelectDict,
1884- String,
1885- )
1886+ Constant, Dict, KeyDict, OAuthString, OneOf, SchemaError, SelectDict,
1887+ String)
1888
1889
1890 DEFAULT_CONFIG_PATH = "~/.juju/environments.yaml"
1891@@ -31,56 +21,60 @@
1892 default-series: oneiric
1893 """
1894
1895+_EITHER_PLACEMENT = OneOf(Constant("unassigned"), Constant("local"))
1896
1897 SCHEMA = KeyDict({
1898 "default": String(),
1899 "environments": Dict(String(), SelectDict("type", {
1900- "ec2": KeyDict({"control-bucket": String(),
1901- "admin-secret": String(),
1902- "access-key": String(),
1903- "secret-key": String(),
1904- "region": OneOf(
1905- Constant("us-east-1"),
1906- Constant("us-west-1"),
1907- Constant("us-west-2"),
1908- Constant("eu-west-1"),
1909- Constant("sa-east-1"),
1910- Constant("ap-northeast-1"),
1911- Constant("ap-southeast-1")),
1912- "default-instance-type": String(),
1913- "default-ami": String(),
1914- "ec2-uri": String(),
1915- "s3-uri": String(),
1916- "placement": OneOf(
1917- Constant("unassigned"),
1918- Constant("local")),
1919- "default-series": String()},
1920- optional=["access-key", "secret-key",
1921- "default-instance-type", "default-ami",
1922- "region", "ec2-uri", "s3-uri", "placement"]),
1923- "orchestra": KeyDict({"orchestra-server": String(),
1924- "orchestra-user": String(),
1925- "orchestra-pass": String(),
1926- "admin-secret": String(),
1927- "acquired-mgmt-class": String(),
1928- "available-mgmt-class": String(),
1929- "storage-url": String(),
1930- "storage-user": String(),
1931- "storage-pass": String(),
1932- "placement": String(),
1933- "default-series": String()},
1934- optional=["storage-url", "storage-user",
1935- "storage-pass", "placement"]),
1936+ "ec2": KeyDict({
1937+ "control-bucket": String(),
1938+ "admin-secret": String(),
1939+ "access-key": String(),
1940+ "secret-key": String(),
1941+ "region": OneOf(
1942+ Constant("us-east-1"),
1943+ Constant("us-west-1"),
1944+ Constant("us-west-2"),
1945+ Constant("eu-west-1"),
1946+ Constant("sa-east-1"),
1947+ Constant("ap-northeast-1"),
1948+ Constant("ap-southeast-1")),
1949+ "ec2-uri": String(),
1950+ "s3-uri": String(),
1951+ "placement": _EITHER_PLACEMENT,
1952+ "default-series": String()},
1953+ optional=[
1954+ "access-key", "secret-key", "region", "ec2-uri", "s3-uri",
1955+ "placement"]),
1956+ "orchestra": KeyDict({
1957+ "orchestra-server": String(),
1958+ "orchestra-user": String(),
1959+ "orchestra-pass": String(),
1960+ "admin-secret": String(),
1961+ "acquired-mgmt-class": String(),
1962+ "available-mgmt-class": String(),
1963+ "storage-url": String(),
1964+ "storage-user": String(),
1965+ "storage-pass": String(),
1966+ "placement": _EITHER_PLACEMENT,
1967+ "default-series": String()},
1968+ optional=[
1969+ "storage-url", "storage-user", "storage-pass", "placement"]),
1970 "maas": KeyDict({
1971 "maas-server": String(),
1972 "maas-oauth": OAuthString(),
1973 "admin-secret": String(),
1974- }),
1975- "local": KeyDict({"admin-secret": String(),
1976- "data-dir": String(),
1977- "placement": Constant("local"),
1978- "default-series": String()},
1979- optional=["placement"]),
1980+ "placement": _EITHER_PLACEMENT,
1981+ # MAAS currently only provisions precise; any other default-series
1982+ # would just lead to errors down the line.
1983+ "default-series": Constant("precise")},
1984+ optional=["placement"]),
1985+ "local": KeyDict({
1986+ "admin-secret": String(),
1987+ "data-dir": String(),
1988+ "placement": Constant("local"),
1989+ "default-series": String()},
1990+ optional=["placement"]),
1991 "dummy": KeyDict({})}))},
1992 optional=["default"])
1993
1994
1995=== modified file 'juju/environment/tests/test_config.py'
1996--- juju/environment/tests/test_config.py 2011-10-06 22:24:21 +0000
1997+++ juju/environment/tests/test_config.py 2012-03-30 16:56:32 +0000
1998@@ -8,6 +8,7 @@
1999 from juju.environment.environment import Environment
2000 from juju.environment.errors import EnvironmentsConfigError
2001 from juju.errors import FileNotFound, FileAlreadyExists
2002+from juju.state.environment import EnvironmentStateManager
2003
2004 from juju.lib.testing import TestCase
2005
2006@@ -38,6 +39,16 @@
2007 default-series: oneiric
2008 """
2009
2010+SAMPLE_MAAS = """
2011+environments:
2012+ sample:
2013+ type: maas
2014+ maas-server: somewhe.re
2015+ maas-oauth: foo:bar:baz
2016+ admin-secret: garden
2017+ default-series: precise
2018+"""
2019+
2020 SAMPLE_LOCAL = """
2021 ensemble: environments
2022
2023@@ -58,7 +69,7 @@
2024 self.patch(environment, "LSB_RELEASE_PATH", release_path)
2025 self.old_home = os.environ.get("HOME")
2026 self.tmp_home = self.makeDir()
2027- self.change_environment(HOME=self.tmp_home)
2028+ self.change_environment(HOME=self.tmp_home, PATH=os.environ["PATH"])
2029 self.default_path = os.path.join(self.tmp_home,
2030 ".juju/environments.yaml")
2031 self.other_path = os.path.join(self.tmp_home,
2032@@ -76,6 +87,30 @@
2033 with open(path, "w") as file:
2034 file.write(config_text)
2035
2036+ # The following methods expect to be called *after* a subclass has set
2037+ # self.client.
2038+
2039+ def push_config(self, name, config):
2040+ self.write_config(yaml.dump(config))
2041+ self.config.load()
2042+ esm = EnvironmentStateManager(self.client)
2043+ return esm.set_config_state(self.config, name)
2044+
2045+ @inlineCallbacks
2046+ def push_env_constraints(self, *constraint_strs):
2047+ esm = EnvironmentStateManager(self.client)
2048+ constraint_set = yield esm.get_constraint_set()
2049+ yield esm.set_constraints(constraint_set.parse(constraint_strs))
2050+
2051+ @inlineCallbacks
2052+ def push_default_config(self, with_constraints=True):
2053+ config = {
2054+ "environments": {"firstenv": {
2055+ "type": "dummy", "storage-directory": self.makeDir()}}}
2056+ yield self.push_config("firstenv", config)
2057+ if with_constraints:
2058+ yield self.push_env_constraints()
2059+
2060
2061 class EnvironmentsConfigTest(EnvironmentsConfigTestBase):
2062
2063@@ -540,6 +575,9 @@
2064 def test_ec2_sample_config_without_admin_secret(self):
2065 self.assert_ec2_sample_config("admin-secret")
2066
2067+ def test_ec2_sample_config_without_default_series(self):
2068+ self.assert_ec2_sample_config("default-series")
2069+
2070 def test_ec2_sample_config_without_control_buckets(self):
2071 self.assert_ec2_sample_config("control-bucket")
2072
2073@@ -603,7 +641,6 @@
2074
2075 def test_orchestra_respects_default_series(self):
2076 config = yaml.load(SAMPLE_ORCHESTRA)
2077-
2078 config["environments"]["sample"]["default-series"] = "magnificent"
2079 self.write_config(yaml.dump(config), other_path=True)
2080 self.config.load(self.other_path)
2081@@ -613,6 +650,58 @@
2082
2083 def test_orchestra_verifies_placement(self):
2084 config = yaml.load(SAMPLE_ORCHESTRA)
2085+ config["environments"]["sample"]["placement"] = "random"
2086+ self.write_config(yaml.dump(config), other_path=True)
2087+ e = self.assertRaises(
2088+ EnvironmentsConfigError, self.config.load, self.other_path)
2089+ self.assertIn("expected 'unassigned', got 'random'",
2090+ str(e))
2091+
2092+ config["environments"]["sample"]["placement"] = "local"
2093+ self.write_config(yaml.dump(config), other_path=True)
2094+ self.config.load(self.other_path)
2095+
2096+ data = self.config.get_default().placement
2097+ self.assertEqual(data, "local")
2098+
2099+ def test_maas_schema_requires(self):
2100+ requires = "maas-server maas-oauth admin-secret default-series".split()
2101+ for require in requires:
2102+ config = yaml.load(SAMPLE_MAAS)
2103+ del config["environments"]["sample"][require]
2104+ self.write_config(yaml.dump(config), other_path=True)
2105+
2106+ try:
2107+ self.config.load(self.other_path)
2108+ except EnvironmentsConfigError as error:
2109+ self.assertEquals(str(error),
2110+ "Environments configuration error: %s: "
2111+ "environments.sample.%s: "
2112+ "required value not found"
2113+ % (self.other_path, require))
2114+ else:
2115+ self.fail("Did not properly require %s when type == maas"
2116+ % require)
2117+
2118+ def test_maas_default_series(self):
2119+ config = yaml.load(SAMPLE_MAAS)
2120+ config["environments"]["sample"]["default-series"] = "magnificent"
2121+ self.write_config(yaml.dump(config), other_path=True)
2122+ e = self.assertRaises(
2123+ EnvironmentsConfigError, self.config.load, self.other_path)
2124+ self.assertIn(
2125+ "environments.sample.default-series: expected 'precise', got "
2126+ "'magnificent'",
2127+ str(e))
2128+
2129+ def test_maas_verifies_placement(self):
2130+ config = yaml.load(SAMPLE_MAAS)
2131+ config["environments"]["sample"]["placement"] = "random"
2132+ self.write_config(yaml.dump(config), other_path=True)
2133+ e = self.assertRaises(
2134+ EnvironmentsConfigError, self.config.load, self.other_path)
2135+ self.assertIn("expected 'unassigned', got 'random'",
2136+ str(e))
2137
2138 config["environments"]["sample"]["placement"] = "local"
2139 self.write_config(yaml.dump(config), other_path=True)
2140@@ -623,25 +712,17 @@
2141
2142 def test_lxc_requires_data_dir(self):
2143 """lxc dev only supports local placement."""
2144- self.config.write_sample()
2145 config = yaml.load(SAMPLE_LOCAL)
2146-
2147 self.write_config(yaml.dump(config), other_path=True)
2148 error = self.assertRaises(
2149- EnvironmentsConfigError,
2150- self.config.load,
2151- self.other_path)
2152+ EnvironmentsConfigError, self.config.load, self.other_path)
2153 self.assertIn("data-dir: required value not found", str(error))
2154
2155 def test_lxc_verifies_placement(self):
2156 """lxc dev only supports local placement."""
2157- self.config.write_sample()
2158 config = yaml.load(SAMPLE_LOCAL)
2159-
2160 config["environments"]["sample"]["placement"] = "unassigned"
2161 self.write_config(yaml.dump(config), other_path=True)
2162 error = self.assertRaises(
2163- EnvironmentsConfigError,
2164- self.config.load,
2165- self.other_path)
2166+ EnvironmentsConfigError, self.config.load, self.other_path)
2167 self.assertIn("expected 'local', got 'unassigned'", str(error))
2168
2169=== modified file 'juju/hooks/tests/test_invoker.py'
2170--- juju/hooks/tests/test_invoker.py 2012-03-29 02:50:29 +0000
2171+++ juju/hooks/tests/test_invoker.py 2012-03-30 16:56:32 +0000
2172@@ -12,12 +12,12 @@
2173 import juju
2174 from juju import errors
2175 from juju.control.tests.test_status import StatusTestBase
2176+from juju.environment.tests.test_config import EnvironmentsConfigTestBase
2177 from juju.lib.pick import pick_attr
2178 from juju.hooks import invoker
2179 from juju.hooks import commands
2180 from juju.hooks.protocol import UnitSettingsFactory
2181 from juju.lib.mocker import MATCH
2182-from juju.lib.testing import TestCase
2183 from juju.lib.twistutils import get_module_directory
2184 from juju.state import hook
2185 from juju.state.endpoint import RelationEndpoint
2186@@ -132,7 +132,12 @@
2187 return ":".join(search_path)
2188
2189
2190-class InvokerTestBase(TestCase):
2191+class InvokerTestBase(EnvironmentsConfigTestBase):
2192+
2193+ @defer.inlineCallbacks
2194+ def setUp(self):
2195+ yield super(InvokerTestBase, self).setUp()
2196+ yield self.push_default_config()
2197
2198 def update_invoker_env(self, local_unit, remote_unit):
2199 """Update os.env for a hook invocation.
2200
2201=== modified file 'juju/machine/constraints.py'
2202--- juju/machine/constraints.py 2012-03-09 13:24:58 +0000
2203+++ juju/machine/constraints.py 2012-03-30 16:56:32 +0000
2204@@ -1,64 +1,197 @@
2205+import logging
2206 import operator
2207-from string import ascii_lowercase
2208 from UserDict import DictMixin
2209
2210 from juju.errors import ConstraintError, UnknownConstraintError
2211
2212-
2213-class Constraints(object, DictMixin):
2214- """A Constraints object encapsulates a set of machine constraints.
2215-
2216- Constraints objects are expected to be initially constructed using the
2217- `from_strs` method to parse user input; they can subsequently be serialised
2218- as a `data` dict and reconstructed directly from same.
2219-
2220- They also implement a dict interface, which exposes all constraints for
2221- the appropriate provider, and is the expected mode of usage for clients
2222- not concerned with the construction or comparison of Constraints objects.
2223-
2224- A Constraints object only ever contains a single "layer" of data, but can
2225- be combined with other Constraints objects in such a way as to produce a
2226- single object following the rules laid down in internals/placement-spec.
2227-
2228- Constraints objects can be compared, in a limited sense, by using the
2229- `can_satisfy` method.
2230+log = logging.getLogger("juju.machine.constraints")
2231+
2232+# To allow providers to construct ConstraintSets which silently ignore known-
2233+# but-inapplicable constraints (ec2-zone on orchestra, for example), we keep
2234+# a hardcoded global registry here. It will not be hard to remember to update
2235+# this when working with providers, because _ConstraintTypes with unknown
2236+# names cannot be created and will cause test failures so long as the added
2237+# constraint registration code is exercised in the provider tests.
2238+ALL_NAMES = (
2239+ "ubuntu-series", "provider-type",
2240+ "arch", "cpu", "mem", "instance-type",
2241+ "ec2-zone",
2242+ "maas-name",
2243+ "orchestra-classes",
2244+)
2245+
2246+
2247+def _dont_convert(s):
2248+ return s
2249+
2250+
2251+class _ConstraintType(object):
2252+ """Defines a constraint.
2253+
2254+ :param str name: The constraint's name
2255+ :param default: The default value as a str, or None to indicate "unset"
2256+ :param converter: Function to convert str value to "real" value (and
2257+ thereby implicitly validate it; should raise ValueError)
2258+ :param comparer: Function used to determine whether one constraint
2259+ satisfies another
2260+ :param bool visible: If False, indicates a computed constraint which
2261+ should not be settable by a user.
2262+
2263+ Merely creating a Constraint does not activate it; you also need to
2264+ register it with a specific ConstraintSet.
2265 """
2266
2267- def __init__(self, data):
2268- # To avoid inconsistency, all Constraints objects must be constructed
2269- # with the same set of _Constraint~s (and conflicts) in play.
2270- _Constraint.freeze()
2271- assert data.get("provider-type"), "missing provider-type"
2272- for k, v in data.items():
2273- _Constraint.get(k).convert(v)
2274- self._data = data
2275-
2276- @classmethod
2277- def from_strs(cls, provider, strs):
2278- """Create from strings (as used on the command line)"""
2279- data = {"provider-type": provider}
2280- relevant_names = _Constraint.names(provider)
2281+ def __init__(self, name, default, converter, comparer, visible):
2282+ assert name in ALL_NAMES, "please update ALL_NAMES"
2283+ self.name = name
2284+ self.default = default
2285+ self._converter = converter
2286+ self._comparer = comparer
2287+ self.visible = visible
2288+
2289+ def convert(self, s):
2290+ """Convert a string representation of a constraint into a useful form.
2291+ """
2292+ if s is None:
2293+ return
2294+ try:
2295+ return self._converter(s)
2296+ except ValueError as e:
2297+ raise ConstraintError(
2298+ "Bad %r constraint %r: %s" % (self.name, s, e))
2299+
2300+ def can_satisfy(self, candidate, benchmark):
2301+ """Check whether candidate can satisfy benchmark"""
2302+ return self._comparer(candidate, benchmark)
2303+
2304+
2305+class ConstraintSet(object):
2306+ """A ConstraintSet represents all constraints applicable to a provider.
2307+
2308+ Individual providers can construct ConstraintSets which will be used to
2309+ construct Constraints objects directly relevant to that provider."""
2310+
2311+ def __init__(self, provider_type):
2312+ self._provider_type = provider_type
2313+ self._registry = {}
2314+ self._conflicts = {}
2315+
2316+ # These constraints must always be available (but are not user-visible
2317+ # or -settable).
2318+ self.register("ubuntu-series", visible=False)
2319+ self.register("provider-type", visible=False)
2320+
2321+ def register(self, name, default=None, converter=_dont_convert,
2322+ comparer=operator.eq, visible=True):
2323+ """Register a constraint to be handled by this ConstraintSet.
2324+
2325+ :param str name: The constraint's name
2326+ :param default: The default value as a str, or None to indicate "unset"
2327+ :param converter: Function to convert str value to "real" value (and
2328+ thereby implicitly validate it; should raise ValueError)
2329+ :param comparer: Function used to determine whether one constraint
2330+ satisfies another
2331+ :param bool visible: If False, indicates a computed constraint which
2332+ should not be settable by a user.
2333+ """
2334+ self._registry[name] = _ConstraintType(
2335+ name, default, converter, comparer, visible)
2336+ self._conflicts[name] = set()
2337+
2338+ def register_conflicts(self, reds, blues):
2339+ """Set cross-constraint override behaviour.
2340+
2341+ :param reds: list of constraint names which affect all constraints
2342+ specified in `blues`
2343+ :param blues: list of constraint names which affect all constraints
2344+ specified in `reds`
2345+
2346+ When two constraints conflict:
2347+
2348+ * It is an error to set both constraints in the same Constraints.
2349+ * When a Constraints overrides another which specifies a conflicting
2350+ constraint, the value in the overridden Constraints is cleared.
2351+ """
2352+ for red in reds:
2353+ self._conflicts[red].update(blues)
2354+ for blue in blues:
2355+ self._conflicts[blue].update(reds)
2356+
2357+ def register_generics(self, instance_type_names):
2358+ """Register a common set of constraints.
2359+
2360+ This always includes arch, cpu, and mem; and will include instance-type
2361+ if instance_type_names is not empty. This is because we believe
2362+ instance-type to be a broadly applicable concept, even though the only
2363+ provider that registers names here (and hence accepts the constraint)
2364+ is currently EC2.
2365+ """
2366+ self.register("arch", default="amd64", converter=_convert_arch)
2367+ self.register(
2368+ "cpu", default="1", converter=_convert_cpu, comparer=operator.ge)
2369+ self.register(
2370+ "mem", default="512M", converter=_convert_mem,
2371+ comparer=operator.ge)
2372+
2373+ if instance_type_names:
2374+
2375+ def convert(instance_type_name):
2376+ if instance_type_name in instance_type_names:
2377+ return instance_type_name
2378+ raise ValueError("unknown instance type")
2379+
2380+ self.register("instance-type", converter=convert)
2381+ self.register_conflicts(["cpu", "mem"], ["instance-type"])
2382+
2383+ def names(self):
2384+ """Get the names of all registered constraints."""
2385+ return self._registry.keys()
2386+
2387+ def get(self, name):
2388+ """Get the (internal) _ConstraintType object corresponding to `name`.
2389+
2390+ If `name` is known, but not registered in this ConstraintSet, None will
2391+ be returned; if the constraint name is entirely unknown, an
2392+ UnknownConstraintError will be raised.
2393+ """
2394+ try:
2395+ return self._registry[name]
2396+ except KeyError:
2397+ if name not in ALL_NAMES:
2398+ raise UnknownConstraintError(name)
2399+
2400+ def parse(self, strs):
2401+ """Create a Constraints from strings (as used on the command line)"""
2402+ data = {"provider-type": self._provider_type}
2403 for s in strs:
2404 try:
2405 name, value = s.split("=", 1)
2406- constraint = _Constraint.get(name)
2407- except KeyError:
2408- raise UnknownConstraintError(name)
2409+ constraint = self.get(name)
2410+ if constraint is None:
2411+ # A constraint called name does exist for some provider (if
2412+ # it didn't exist anywhere, .get would have raised) but is
2413+ # not relevant for this provider.
2414+ log.warn(
2415+ "ignored irrelevant %r constraint", name)
2416+ continue
2417+ if value == "any":
2418+ value = None
2419+ if value == "":
2420+ value = constraint.default
2421+ constraint.convert(value)
2422 except ValueError as e:
2423 raise ConstraintError(
2424 "Could not interpret %r constraint: %s" % (s, e))
2425- if name not in relevant_names:
2426- continue
2427 if not constraint.visible:
2428 raise ConstraintError(
2429 "Cannot set computed constraint: %r" % name)
2430- data[name] = value or constraint.default
2431+ data[name] = value
2432
2433 conflicts = set()
2434 for name in sorted(data):
2435- for conflict in sorted(_Constraint.get(name).conflicts):
2436- if conflict not in relevant_names:
2437- continue
2438+ if data[name] is None:
2439+ continue
2440+ for conflict in sorted(self._conflicts[name]):
2441 if conflict in data:
2442 raise ConstraintError(
2443 "Ambiguous constraints: %r overlaps with %r"
2444@@ -66,13 +199,54 @@
2445 conflicts.add(conflict)
2446
2447 data.update(dict((conflict, None) for conflict in conflicts))
2448- return Constraints(data)
2449+ return Constraints(self, data)
2450+
2451+ def load(self, data):
2452+ """Convert a data dict to a Constraints"""
2453+ for k, v in data.items():
2454+ self.get(k).convert(v)
2455+ return Constraints(self, data)
2456+
2457+
2458+class Constraints(object, DictMixin):
2459+ """A Constraints object encapsulates a set of machine constraints.
2460+
2461+ Constraints instances should not be constructed directly; please use
2462+ ConstraintSet's parse and load methods instead.
2463+
2464+ They implement a dict interface, which exposes all constraints for the
2465+ appropriate provider, and is the expected mode of usage for clients not
2466+ concerned with the construction or comparison of Constraints objects.
2467+
2468+ A Constraints object only ever contains a single "layer" of data, but can
2469+ be combined with other Constraints objects in such a way as to produce a
2470+ single object following the rules laid down in internals/placement-spec.
2471+
2472+ Constraints objects can be compared, in a limited sense, by using the
2473+ `can_satisfy` method.
2474+ """
2475+
2476+ def __init__(self, available, data):
2477+ self._available = available
2478+ self._data = data
2479+
2480+ def keys(self):
2481+ """DictMixin"""
2482+ return self._available.names()
2483+
2484+ def __getitem__(self, name):
2485+ """DictMixin"""
2486+ if name not in self.keys():
2487+ raise KeyError(name)
2488+ constraint = self._available.get(name)
2489+ raw_value = self.data.get(name, constraint.default)
2490+ return constraint.convert(raw_value)
2491
2492 def with_series(self, series):
2493 """Return a Constraints with the "ubuntu-series" set to `series`"""
2494 data = dict(self._data)
2495 data["ubuntu-series"] = series
2496- return Constraints(data)
2497+ return self._available.load(data)
2498
2499 @property
2500 def complete(self):
2501@@ -108,7 +282,11 @@
2502 # place unit on machine
2503 """
2504 if not (self.complete and other.complete):
2505- raise ConstraintError("Cannot compare incomplete constraints")
2506+ # Incomplete constraints cannot satisfy or be satisfied; we should
2507+ # only ever hit this branch if we're running new code (that knows
2508+ # about constraints) against an old deployment (which will contain
2509+ # at least *some* services/machines which don't have constraints).
2510+ return False
2511
2512 for (name, unit_value) in other.items():
2513 if unit_value is None:
2514@@ -116,168 +294,29 @@
2515 continue
2516 machine_value = self[name]
2517 if machine_value is None:
2518- # The unit *does* care, and the machine value isn't specified,
2519- # so we can't guarantee a match. If we were to update machine
2520- # constraints after provisioning (ie when we knew the values of
2521- # the constraints left unspecified) we'd hit this branch less
2522- # often.
2523- # We may also need to do something clever here to get sensible
2524- # machine reuse on ec2 -- in what circumstances, if ever, is it
2525- # OK to place a unit specced for one instance-type on a machine
2526- # of another type? Does it matter if either or both were derived
2527- # from generic constraints? What about cost?
2528+ # The unit *does* care, and the machine value isn't
2529+ # specified, so we can't guarantee a match. If we were
2530+ # to update machine constraints after provisioning (ie
2531+ # when we knew the values of the constraints left
2532+ # unspecified) we'd hit this branch less often. We
2533+ # may also need to do something clever here to get
2534+ # sensible machine reuse on ec2 -- in what
2535+ # circumstances, if ever, is it OK to place a unit
2536+ # specced for one instance-type on a machine of
2537+ # another type? Does it matter if either or both were
2538+ # derived from generic constraints? What about cost?
2539 return False
2540- if not _Constraint.get(name).can_satisfy(machine_value, unit_value):
2541+ constraint = self._available.get(name)
2542+ if not constraint.can_satisfy(machine_value, unit_value):
2543 # The machine's value is definitely not ok for the unit.
2544 return False
2545
2546 return True
2547
2548- # DictMixin methods
2549-
2550- def keys(self):
2551- return _Constraint.names(self._data.get("provider-type"))
2552-
2553- def __getitem__(self, name):
2554- if name not in self.keys():
2555- raise KeyError(name)
2556- constraint = _Constraint.get(name)
2557- raw_value = self.data.get(name, constraint.default)
2558- return constraint.convert(raw_value)
2559-
2560-
2561-#======================================================================
2562-# Constraint type registration
2563-
2564-def _dont_convert(s):
2565- return s
2566-
2567-
2568-class _Constraint(object):
2569-
2570- _registry = {}
2571- _conflicts = {}
2572- _frozen = False
2573-
2574- def __init__(self, name, default, converter, comparer, provider, visible):
2575- self._name = name
2576- self._default = default
2577- self._converter = converter
2578- self._comparer = comparer
2579- self._provider = provider
2580- self._visible = visible
2581-
2582- @classmethod
2583- def freeze(cls):
2584- """Once called, prevents register and register_conflicts from working.
2585-
2586- Intent is to enforce consistency of Constraints construction and
2587- overriding: if the set of constraints or the registered overlaps
2588- were to change at runtime, the same operations could end up
2589- producing different results.
2590- """
2591- cls._frozen = True
2592-
2593- @classmethod
2594- def register(cls, name, default=None, converter=_dont_convert,
2595- comparer=operator.eq, provider=None, visible=True):
2596- """Define a constraint.
2597-
2598- :param str name: The constraint's name
2599- :param default: The default value as a str, or None to indicate "unset"
2600- :param converter: Function to convert str value to "real" value
2601- :param comparer: Function used to determine whether one constraint
2602- satisfies another
2603- :param provider: The name of the provider for which this constraint is
2604- meaningful (None indicates always meaningful)
2605- :param bool visible: If False, indicates a computed constraint which
2606- should not be settable by a user.
2607-
2608- In service of consistency, is an error to attempt to register a new
2609- constraint once a Constraints object has been created.
2610- """
2611- assert not cls._frozen
2612- inst = cls(name, default, converter, comparer, provider, visible)
2613- cls._registry[name] = inst
2614- cls._conflicts[name] = set()
2615-
2616- @classmethod
2617- def register_conflicts(cls, reds, blues):
2618- """Set cross-constraint override behaviour.
2619-
2620- :param reds: list of constraint names which affect all constraints
2621- specified in `blues`
2622- :param blues: list of constraint names which affect all constraints
2623- specified in `reds`
2624-
2625- When two constraints conflict:
2626-
2627- * It is an error to set both constraints in the same Constraints.
2628- * When a Constraints overrides another which specifies a conflicting
2629- constraint, the value in the overridden Constraints is cleared.
2630-
2631- In service of consistency, is an error to attempt to register any new
2632- conflicts once a Constraints object has been created.
2633- """
2634- assert not cls._frozen
2635- for red in reds:
2636- cls._conflicts[red].update(blues)
2637- for blue in blues:
2638- cls._conflicts[blue].update(reds)
2639-
2640- @classmethod
2641- def get(cls, name):
2642- try:
2643- return cls._registry[name]
2644- except KeyError:
2645- raise UnknownConstraintError(name)
2646-
2647- @classmethod
2648- def names(cls, provider):
2649- return [
2650- name for (name, constraint) in cls._registry.items()
2651- if constraint.provider in set([None, provider])]
2652-
2653- @property
2654- def conflicts(self):
2655- return self._conflicts[self._name]
2656-
2657- @property
2658- def default(self):
2659- return self._default
2660-
2661- @property
2662- def provider(self):
2663- return self._provider
2664-
2665- @property
2666- def visible(self):
2667- return self._visible
2668-
2669- def convert(self, s):
2670- if s is None:
2671- return
2672- try:
2673- return self._converter(s)
2674- except ValueError as e:
2675- raise ConstraintError(
2676- "Bad %r constraint %r: %s" % (self._name, s, e))
2677-
2678- def can_satisfy(self, candidate, benchmark):
2679- return self._comparer(candidate, benchmark)
2680-
2681-
2682-#======================================================================
2683-# Generic (but not user-visible) constraints; always relevant
2684-_Constraint.register("ubuntu-series", visible=False)
2685-_Constraint.register("provider-type", visible=False)
2686-
2687-
2688-#======================================================================
2689-# Generic (user-visible) constraints; always relevant
2690-
2691+
2692+#==============================================================================
2693+# Generic constraint information (used by multiple providers).
2694 _VALID_ARCHS = ("i386", "amd64", "arm")
2695-
2696 _MEGABYTES = 1
2697 _GIGABYTES = _MEGABYTES * 1024
2698 _TERABYTES = _GIGABYTES * 1024
2699@@ -292,7 +331,7 @@
2700
2701 def _convert_cpu(s):
2702 value = float(s)
2703- if value > 0:
2704+ if value >= 0:
2705 return value
2706 raise ValueError("must be non-negative")
2707
2708@@ -302,56 +341,6 @@
2709 value = float(s[:-1]) * _MEM_SUFFIXES[s[-1]]
2710 else:
2711 value = float(s)
2712- if value > 0:
2713+ if value >= 0:
2714 return value
2715 raise ValueError("must be non-negative")
2716-
2717-
2718-_Constraint.register("arch", converter=_convert_arch)
2719-_Constraint.register(
2720- "mem", default="512M", converter=_convert_mem, comparer=operator.ge)
2721-_Constraint.register(
2722- "cpu", default="1", converter=_convert_cpu, comparer=operator.ge)
2723-
2724-
2725-#======================================================================
2726-# Orchestra-only
2727-
2728-def _convert_orchestra_classes(s):
2729- return s.split(",")
2730-
2731-
2732-def _compare_orchestra_classes(candidate, benchmark):
2733- return set(candidate) >= set(benchmark)
2734-
2735-
2736-_Constraint.register(
2737- "orchestra-classes", converter=_convert_orchestra_classes,
2738- comparer=_compare_orchestra_classes, provider="orchestra")
2739-_Constraint.register("orchestra-name", provider="orchestra")
2740-
2741-
2742-#======================================================================
2743-# EC2-only
2744-
2745-def _convert_ec2_zone(s):
2746- if s not in ascii_lowercase:
2747- raise ValueError("expected lowercase ascii char")
2748- return s
2749-
2750-
2751-_Constraint.register("ec2-instance-type", provider="ec2")
2752-_Constraint.register("ec2-zone", converter=_convert_ec2_zone, provider="ec2")
2753-
2754-
2755-#======================================================================
2756-# All conflicts
2757-
2758-_Constraint.register_conflicts(["orchestra-name"], ["orchestra-classes"])
2759-_Constraint.register_conflicts(
2760- ["ec2-instance-type", "orchestra-name"], ["arch", "cpu", "mem"])
2761-
2762-#=====================================================================
2763-# Ensure consistency
2764-
2765-_Constraint.freeze()
2766
2767=== modified file 'juju/machine/tests/test_constraints.py'
2768--- juju/machine/tests/test_constraints.py 2012-03-09 13:24:58 +0000
2769+++ juju/machine/tests/test_constraints.py 2012-03-30 16:56:32 +0000
2770@@ -1,270 +1,168 @@
2771+import operator
2772+
2773 from juju.errors import ConstraintError
2774 from juju.lib.testing import TestCase
2775-from juju.machine.constraints import Constraints
2776+from juju.machine.constraints import Constraints, ConstraintSet
2777
2778+# These objects exist for the convenience of other test files
2779+dummy_cs = ConstraintSet("dummy")
2780+dummy_cs.register_generics([])
2781+dummy_constraints = dummy_cs.parse([])
2782+series_constraints = dummy_constraints.with_series("series")
2783
2784 generic_defaults = {
2785- "arch": None, "cpu": 1, "mem": 512,
2786+ "arch": "amd64", "cpu": 1, "mem": 512,
2787 "ubuntu-series": None, "provider-type": None}
2788 dummy_defaults = dict(generic_defaults, **{
2789 "provider-type": "dummy"})
2790 ec2_defaults = dict(generic_defaults, **{
2791 "provider-type": "ec2",
2792 "ec2-zone": None,
2793- "ec2-instance-type": None})
2794-orchestra_defaults = dict(generic_defaults, **{
2795+ "instance-type": None})
2796+orchestra_defaults = {
2797 "provider-type": "orchestra",
2798- "orchestra-name": None,
2799- "orchestra-classes": None})
2800+ "ubuntu-series": None,
2801+ "orchestra-classes": None}
2802
2803 all_providers = ["dummy", "ec2", "orchestra"]
2804
2805+
2806 class ConstraintsTestCase(TestCase):
2807
2808 def assert_error(self, message, *raises_args):
2809 e = self.assertRaises(ConstraintError, *raises_args)
2810 self.assertEquals(str(e), message)
2811
2812- def assert_roundtrip_equal(self, constraints, expected):
2813+ def assert_roundtrip_equal(self, cs, constraints, expected):
2814+ self.assertEquals(dict(constraints), expected)
2815 self.assertEquals(constraints, expected)
2816- self.assertEquals(Constraints(constraints.data), expected)
2817+ self.assertEquals(dict(cs.load(constraints.data)), expected)
2818+ self.assertEquals(cs.load(constraints.data), expected)
2819
2820
2821 class ConstraintsTest(ConstraintsTestCase):
2822
2823- def test_defaults(self):
2824- constraints = Constraints.from_strs("orchestra", [])
2825- self.assert_roundtrip_equal(constraints, orchestra_defaults)
2826- constraints = Constraints.from_strs("ec2", [])
2827- self.assert_roundtrip_equal(constraints, ec2_defaults)
2828- constraints = Constraints.from_strs("dummy", [])
2829- self.assert_roundtrip_equal(constraints, dummy_defaults)
2830+ def test_equality(self):
2831+ self.assert_roundtrip_equal(
2832+ dummy_cs, dummy_constraints, dummy_defaults)
2833
2834 def test_complete(self):
2835- incomplete_constraints = Constraints.from_strs("womble", [])
2836- self.assertFalse(incomplete_constraints.complete)
2837+ incomplete_constraints = dummy_cs.parse([])
2838 complete_constraints = incomplete_constraints.with_series("wandering")
2839 self.assertTrue(complete_constraints.complete)
2840
2841- def assert_invalid(self, message, providers, *constraint_strs):
2842- for provider in providers:
2843- self.assert_error(
2844- message, Constraints.from_strs, provider, constraint_strs)
2845-
2846- if len(providers) != len(all_providers):
2847- # Check it *is* valid for other providers
2848- for provider in all_providers:
2849- if provider in providers:
2850- continue
2851- Constraints.from_strs(provider, constraint_strs)
2852+ def assert_invalid(self, message, *constraint_strs):
2853+ self.assert_error(
2854+ message, dummy_cs.parse, constraint_strs)
2855
2856 def test_invalid_input(self):
2857 """Reject nonsense constraints"""
2858 self.assert_invalid(
2859 "Could not interpret 'BLAH' constraint: need more than 1 value to "
2860 "unpack",
2861- all_providers, "BLAH")
2862+ "BLAH")
2863 self.assert_invalid(
2864 "Unknown constraint: 'foo'",
2865- all_providers, "foo=", "bar=")
2866+ "foo=", "bar=")
2867
2868 def test_invalid_constraints(self):
2869 """Reject nonsensical constraint values"""
2870 self.assert_invalid(
2871- "Bad 'arch' constraint 'leg': unknown architecture", all_providers,
2872+ "Bad 'arch' constraint 'leg': unknown architecture",
2873 "arch=leg")
2874 self.assert_invalid(
2875- "Bad 'cpu' constraint '-1': must be non-negative", all_providers,
2876+ "Bad 'cpu' constraint '-1': must be non-negative",
2877 "cpu=-1")
2878 self.assert_invalid(
2879 "Bad 'cpu' constraint 'fish': could not convert string to float: "
2880 "fish",
2881- all_providers, "cpu=fish")
2882+ "cpu=fish")
2883 self.assert_invalid(
2884- "Bad 'mem' constraint '-1': must be non-negative", all_providers,
2885+ "Bad 'mem' constraint '-1': must be non-negative",
2886 "mem=-1")
2887 self.assert_invalid(
2888 "Bad 'mem' constraint '4P': invalid literal for float(): 4P",
2889- all_providers, "mem=4P")
2890- self.assert_invalid(
2891- "Bad 'ec2-zone' constraint 'Q': expected lowercase ascii char",
2892- ["ec2"], "ec2-zone=Q")
2893+ "mem=4P")
2894
2895 def test_hidden_constraints(self):
2896 """Reject attempts to explicitly specify computed constraints"""
2897 self.assert_invalid(
2898- "Cannot set computed constraint: 'ubuntu-series'", all_providers,
2899+ "Cannot set computed constraint: 'ubuntu-series'",
2900 "ubuntu-series=cheesy")
2901 self.assert_invalid(
2902- "Cannot set computed constraint: 'provider-type'", all_providers,
2903+ "Cannot set computed constraint: 'provider-type'",
2904 "provider-type=dummy")
2905
2906- def test_overlap_ec2(self):
2907- """Overlapping ec2 constraints should be detected"""
2908- self.assert_invalid(
2909- "Ambiguous constraints: 'arch' overlaps with 'ec2-instance-type'",
2910- ["ec2"], "ec2-instance-type=m1.small", "arch=i386")
2911- self.assert_invalid(
2912- "Ambiguous constraints: 'cpu' overlaps with 'ec2-instance-type'",
2913- ["ec2"], "ec2-instance-type=m1.small", "cpu=1")
2914- self.assert_invalid(
2915- "Ambiguous constraints: 'ec2-instance-type' overlaps with 'mem'",
2916- ["ec2"], "ec2-instance-type=m1.small", "mem=2G")
2917-
2918- def test_overlap_orchestra(self):
2919- """Overlapping orchestra constraints should be detected"""
2920- self.assert_invalid(
2921- "Ambiguous constraints: 'arch' overlaps with 'orchestra-name'",
2922- ["orchestra"], "orchestra-name=herbert", "arch=i386")
2923- self.assert_invalid(
2924- "Ambiguous constraints: 'cpu' overlaps with 'orchestra-name'",
2925- ["orchestra"], "orchestra-name=herbert", "cpu=1")
2926- self.assert_invalid(
2927- "Ambiguous constraints: 'mem' overlaps with 'orchestra-name'",
2928- ["orchestra"], "orchestra-name=herbert", "mem=2G")
2929- self.assert_invalid(
2930- "Ambiguous constraints: 'orchestra-classes' overlaps with "
2931- "'orchestra-name'",
2932- ["orchestra"], "orchestra-name=herbert", "orchestra-classes=x,y")
2933-
2934
2935 class ConstraintsUpdateTest(ConstraintsTestCase):
2936
2937- def assert_constraints(self, provider, strss, expected):
2938- constraints = Constraints.from_strs(provider, strss[0])
2939+ def assert_constraints(self, strss, expected):
2940+ constraints = dummy_cs.parse(strss[0])
2941 for strs in strss[1:]:
2942- constraints.update(Constraints.from_strs(provider, strs))
2943- self.assert_roundtrip_equal(constraints, expected)
2944-
2945- def assert_constraints_dummy(self, strss, expected):
2946+ constraints.update(dummy_cs.parse(strs))
2947 expected = dict(dummy_defaults, **expected)
2948- self.assert_constraints("dummy", strss, expected)
2949+ self.assert_roundtrip_equal(dummy_cs, constraints, expected)
2950
2951- def test_constraints_dummy(self):
2952+ def test_constraints(self):
2953 """Sane constraints dicts are generated for unknown environments"""
2954- self.assert_constraints_dummy([[]], {})
2955- self.assert_constraints_dummy([["arch=arm"]], {"arch": "arm"})
2956- self.assert_constraints_dummy([["cpu=0.1"]], {"cpu": 0.1})
2957- self.assert_constraints_dummy([["mem=128"]], {"mem": 128})
2958- self.assert_constraints_dummy(
2959+ self.assert_constraints([[]], {})
2960+ self.assert_constraints([["cpu=", "mem="]], {})
2961+ self.assert_constraints([["arch=arm"]], {"arch": "arm"})
2962+ self.assert_constraints([["cpu=0.1"]], {"cpu": 0.1})
2963+ self.assert_constraints([["mem=128"]], {"mem": 128})
2964+ self.assert_constraints([["cpu=0"]], {"cpu": 0})
2965+ self.assert_constraints([["mem=0"]], {"mem": 0})
2966+ self.assert_constraints(
2967 [["arch=amd64", "cpu=6", "mem=1.5G"]],
2968- {"arch": "amd64", "cpu": 6, "mem": 1536,})
2969+ {"arch": "amd64", "cpu": 6, "mem": 1536})
2970
2971 def test_overwriting_basic(self):
2972 """Later values shadow earlier values"""
2973- self.assert_constraints_dummy(
2974+ self.assert_constraints(
2975 [["cpu=4", "mem=512"], ["arch=i386", "mem=1G"]],
2976 {"arch": "i386", "cpu": 4, "mem": 1024})
2977
2978 def test_reset(self):
2979 """Empty string resets to juju default"""
2980- self.assert_constraints_dummy(
2981+ self.assert_constraints(
2982 [["arch=arm", "cpu=4", "mem=1024"], ["arch=", "cpu=", "mem="]],
2983- {"arch": None, "cpu": 1, "mem": 512})
2984- self.assert_constraints_dummy(
2985+ {"arch": "amd64", "cpu": 1, "mem": 512})
2986+ self.assert_constraints(
2987 [["arch=", "cpu=", "mem="], ["arch=arm", "cpu=4", "mem=1024"]],
2988 {"arch": "arm", "cpu": 4, "mem": 1024})
2989
2990- def assert_constraints_ec2(self, strss, expected):
2991- expected = dict(ec2_defaults, **expected)
2992- self.assert_constraints("ec2", strss, expected)
2993-
2994- def test_constraints_ec2(self):
2995- """Sane constraints dicts are generated for ec2"""
2996- self.assert_constraints_ec2([[]], {})
2997- self.assert_constraints_ec2([["arch=arm"]], {"arch": "arm"})
2998- self.assert_constraints_ec2([["cpu=128"]], {"cpu": 128})
2999- self.assert_constraints_ec2([["mem=2G"]], {"mem": 2048})
3000- self.assert_constraints_ec2([["ec2-zone=b"]], {"ec2-zone": "b"})
3001- self.assert_constraints_ec2(
3002- [["arch=amd64", "cpu=32", "mem=2G", "ec2-zone=b"]],
3003- {"arch": "amd64", "cpu": 32, "mem": 2048, "ec2-zone": "b"})
3004-
3005- def test_overwriting_ec2_instance_type(self):
3006- """ec2-instance-type interacts correctly with arch, cpu, mem"""
3007- self.assert_constraints_ec2(
3008- [["ec2-instance-type=t1.micro"]],
3009- {"ec2-instance-type": "t1.micro", "cpu": None, "mem": None})
3010- self.assert_constraints_ec2(
3011- [["arch=arm", "cpu=8"], ["ec2-instance-type=t1.micro"]],
3012- {"ec2-instance-type": "t1.micro", "cpu": None, "mem": None})
3013- self.assert_constraints_ec2(
3014- [["ec2-instance-type=t1.micro"], ["arch=arm", "cpu=8"]],
3015- {"ec2-instance-type": None, "arch": "arm", "cpu": 8, "mem": None})
3016-
3017- def assert_constraints_orchestra(self, strss, expected):
3018- expected = dict(orchestra_defaults, **expected)
3019- self.assert_constraints("orchestra", strss, expected)
3020-
3021- def test_constraints_orchestra(self):
3022- """Sane constraints dicts are generated for orchestra"""
3023- self.assert_constraints_orchestra([[]], {})
3024- self.assert_constraints_orchestra([["arch=arm"]], {"arch": "arm"})
3025- self.assert_constraints_orchestra([["cpu=128"]], {"cpu": 128})
3026- self.assert_constraints_orchestra([["mem=0.25T"]], {"mem": 262144})
3027- self.assert_constraints_orchestra(
3028- [["orchestra-classes=x,y"]], {"orchestra-classes": ["x", "y"]})
3029- self.assert_constraints_orchestra(
3030- [["arch=i386", "cpu=2", "mem=768M", "orchestra-classes=a,b"]],
3031- {"arch": "i386", "cpu": 2, "mem": 768,
3032- "orchestra-classes": ["a", "b"]})
3033-
3034- def test_overwriting_orchestra_name(self):
3035- """orchestra-name interacts correctly with arch, cpu, mem"""
3036- self.assert_constraints_orchestra(
3037- [["orchestra-name=baggins"]],
3038- {"orchestra-name": "baggins", "cpu": None, "mem": None})
3039- self.assert_constraints_orchestra(
3040- [["orchestra-name=baggins"], ["arch=arm"]],
3041- {"orchestra-name": None, "arch": "arm", "cpu": None, "mem": None})
3042- self.assert_constraints_orchestra(
3043- [["arch=arm", "cpu=2"], ["orchestra-name=baggins"]],
3044- {"orchestra-name": "baggins", "arch": None, "cpu": None,
3045- "mem": None})
3046-
3047- def test_overwriting_orchestra_classes(self):
3048- """orchestra-classes interacts correctly with orchestra-name"""
3049- self.assert_constraints_orchestra(
3050- [["orchestra-name=baggins"], ["orchestra-classes=c,d", "cpu=2"]],
3051- {"orchestra-classes": ["c", "d"], "cpu": 2, "mem": None})
3052- self.assert_constraints_orchestra(
3053- [["orchestra-classes=zz,top", "arch=amd64", "cpu=2"],
3054- ["orchestra-name=baggins"]],
3055- {"orchestra-name": "baggins", "orchestra-classes": None,
3056- "cpu": None, "mem": None})
3057-
3058
3059 class ConstraintsFulfilmentTest(ConstraintsTestCase):
3060
3061- def completed_constraints(self, provider="provider", series="series"):
3062- return Constraints.from_strs(provider, []).with_series(series)
3063-
3064- def assert_incomparable(self, c1, c2):
3065- self.assert_error(
3066- "Cannot compare incomplete constraints", c1.can_satisfy, c2)
3067- self.assert_error(
3068- "Cannot compare incomplete constraints", c2.can_satisfy, c1)
3069-
3070 def assert_match(self, c1, c2, expected):
3071 self.assertEquals(c1.can_satisfy(c2), expected)
3072 self.assertEquals(c2.can_satisfy(c1), expected)
3073
3074 def test_fulfil_completeness(self):
3075- """can_satisfy needs to be called on and with complete Constraints~s"""
3076- c1 = Constraints({"provider-type": "a"})
3077- c2 = Constraints({"provider-type": "a"})
3078- self.assert_incomparable(c1, c2)
3079- c3 = self.completed_constraints("a")
3080- self.assert_incomparable(c1, c3)
3081- c4 = self.completed_constraints("a")
3082- self.assert_match(c3, c4, True)
3083+ """
3084+ can_satisfy needs to be called on and with complete Constraints~s to
3085+ have any chance of working.
3086+ """
3087+ good = Constraints(
3088+ dummy_cs, {"provider-type": "dummy", "ubuntu-series": "x"})
3089+ self.assert_match(good, good, True)
3090+ bad = [Constraints(dummy_cs, {}),
3091+ Constraints(dummy_cs, {"provider-type": "dummy"}),
3092+ Constraints(dummy_cs, {"ubuntu-series": "x"})]
3093+ for i, bad1 in enumerate(bad):
3094+ self.assert_match(bad1, good, False)
3095+ for bad2 in bad[i:]:
3096+ self.assert_match(bad1, bad2, False)
3097
3098 def test_fulfil_matches(self):
3099+ other_cs = ConstraintSet("other")
3100+ other_cs.register_generics([])
3101+ other_constraints = other_cs.parse([])
3102 instances = (
3103- Constraints({"provider-type": "a", "ubuntu-series": "x"}),
3104- Constraints({"provider-type": "a", "ubuntu-series": "y"}),
3105- Constraints({"provider-type": "b", "ubuntu-series": "x"}),
3106- Constraints({"provider-type": "b", "ubuntu-series": "y"}))
3107+ dummy_constraints.with_series("x"),
3108+ dummy_constraints.with_series("y"),
3109+ other_constraints.with_series("x"),
3110+ other_constraints.with_series("y"))
3111
3112 for i, c1 in enumerate(instances):
3113 self.assert_match(c1, c1, True)
3114@@ -272,94 +170,237 @@
3115 self.assert_match(c1, c2, False)
3116
3117 def assert_can_satisfy(
3118- self, machine_strs, unit_strs, expected, provider="provider"):
3119- machine = Constraints.from_strs(provider, machine_strs)
3120+ self, machine_strs, unit_strs, expected):
3121+ machine = dummy_cs.parse(machine_strs)
3122 machine = machine.with_series("shiny")
3123- unit = Constraints.from_strs(provider, unit_strs)
3124+ unit = dummy_cs.parse(unit_strs)
3125 unit = unit.with_series("shiny")
3126 self.assertEquals(machine.can_satisfy(unit), expected)
3127
3128- if provider != "provider":
3129- # Check that a different provider doesn't detect any problem,
3130- # because it's ignoring all off-provider constraints.
3131- machine = Constraints.from_strs("provider", machine_strs)
3132- machine = machine.with_series("shiny")
3133- unit = Constraints.from_strs("provider", unit_strs)
3134- unit = unit.with_series("shiny")
3135- self.assertEquals(machine.can_satisfy(unit), True)
3136-
3137-
3138 def test_can_satisfy(self):
3139 self.assert_can_satisfy([], [], True)
3140
3141- self.assert_can_satisfy(["arch=arm"], [], True)
3142+ self.assert_can_satisfy(["arch=arm"], [], False)
3143+ self.assert_can_satisfy(["arch=amd64"], [], True)
3144 self.assert_can_satisfy([], ["arch=arm"], False)
3145 self.assert_can_satisfy(["arch=i386"], ["arch=arm"], False)
3146 self.assert_can_satisfy(["arch=arm"], ["arch=amd64"], False)
3147 self.assert_can_satisfy(["arch=amd64"], ["arch=amd64"], True)
3148+ self.assert_can_satisfy(["arch=i386"], ["arch=any"], True)
3149+ self.assert_can_satisfy(["arch=arm"], ["arch=any"], True)
3150+ self.assert_can_satisfy(["arch=amd64"], ["arch=any"], True)
3151+ self.assert_can_satisfy(["arch=any"], ["arch=any"], True)
3152+ self.assert_can_satisfy(["arch=any"], ["arch=i386"], False)
3153+ self.assert_can_satisfy(["arch=any"], ["arch=amd64"], False)
3154+ self.assert_can_satisfy(["arch=any"], ["arch=arm"], False)
3155
3156 self.assert_can_satisfy(["cpu=64"], [], True)
3157 self.assert_can_satisfy([], ["cpu=64"], False)
3158 self.assert_can_satisfy(["cpu=64"], ["cpu=32"], True)
3159 self.assert_can_satisfy(["cpu=32"], ["cpu=64"], False)
3160 self.assert_can_satisfy(["cpu=64"], ["cpu=64"], True)
3161+ self.assert_can_satisfy(["cpu=0.01"], ["cpu=any"], True)
3162+ self.assert_can_satisfy(["cpu=9999"], ["cpu=any"], True)
3163+ self.assert_can_satisfy(["cpu=any"], ["cpu=any"], True)
3164+ self.assert_can_satisfy(["cpu=any"], ["cpu=0.01"], False)
3165+ self.assert_can_satisfy(["cpu=any"], ["cpu=9999"], False)
3166
3167 self.assert_can_satisfy(["mem=8G"], [], True)
3168 self.assert_can_satisfy([], ["mem=8G"], False)
3169 self.assert_can_satisfy(["mem=8G"], ["mem=4G"], True)
3170 self.assert_can_satisfy(["mem=4G"], ["mem=8G"], False)
3171 self.assert_can_satisfy(["mem=8G"], ["mem=8G"], True)
3172-
3173- self.assert_can_satisfy(
3174- # orchestra-name clears default cpu/mem values.
3175- # This may be a problem.
3176- ["orchestra-name=henry"], [], False, "orchestra")
3177- self.assert_can_satisfy(
3178- [], ["orchestra-name=henry"], False, "orchestra")
3179- self.assert_can_satisfy(
3180- ["orchestra-name=henry"], ["orchestra-name=jane"], False,
3181- "orchestra")
3182- self.assert_can_satisfy(
3183- ["orchestra-name=jane"], ["orchestra-name=henry"], False,
3184- "orchestra")
3185- self.assert_can_satisfy(
3186- ["orchestra-name=henry"], ["orchestra-name=henry"], True,
3187- "orchestra")
3188-
3189- self.assert_can_satisfy(
3190- ["orchestra-classes=a,b"], [], True, "orchestra")
3191- self.assert_can_satisfy(
3192- [], ["orchestra-classes=a,b"], False, "orchestra")
3193- self.assert_can_satisfy(
3194- ["orchestra-classes=a,b"], ["orchestra-classes=a"], True,
3195- "orchestra")
3196- self.assert_can_satisfy(
3197- ["orchestra-classes=a"], ["orchestra-classes=a,b"], False,
3198- "orchestra")
3199- self.assert_can_satisfy(
3200- ["orchestra-classes=a,b"], ["orchestra-classes=a,b"], True,
3201- "orchestra")
3202- self.assert_can_satisfy(
3203- ["orchestra-classes=a,b"], ["orchestra-classes=a,c"], False,
3204- "orchestra")
3205-
3206- self.assert_can_satisfy(["ec2-zone=a"], [], True, "ec2")
3207- self.assert_can_satisfy([], ["ec2-zone=a"], False, "ec2")
3208- self.assert_can_satisfy(["ec2-zone=a"], ["ec2-zone=b"], False, "ec2")
3209- self.assert_can_satisfy(["ec2-zone=b"], ["ec2-zone=a"], False, "ec2")
3210- self.assert_can_satisfy(["ec2-zone=a"], ["ec2-zone=a"], True, "ec2")
3211-
3212- self.assert_can_satisfy(
3213- # ec2-instance-type clears default cpu/mem values.
3214- # This may be a problem.
3215- ["ec2-instance-type=m1.small"], [], False, "ec2")
3216- self.assert_can_satisfy([], ["ec2-instance-type=m1.small"], False, "ec2")
3217- self.assert_can_satisfy(
3218- ["ec2-instance-type=m1.large"], ["ec2-instance-type=m1.small"],
3219- False, "ec2")
3220- self.assert_can_satisfy(
3221- ["ec2-instance-type=m1.small"], ["ec2-instance-type=m1.large"],
3222- False, "ec2")
3223- self.assert_can_satisfy(
3224- ["ec2-instance-type=m1.small"], ["ec2-instance-type=m1.small"],
3225- True, "ec2")
3226+ self.assert_can_satisfy(["mem=2M"], ["mem=any"], True)
3227+ self.assert_can_satisfy(["mem=256T"], ["mem=any"], True)
3228+ self.assert_can_satisfy(["mem=any"], ["mem=any"], True)
3229+ self.assert_can_satisfy(["mem=any"], ["mem=2M"], False)
3230+ self.assert_can_satisfy(["mem=any"], ["mem=256T"], False)
3231+
3232+
3233+class ConstraintSetTest(TestCase):
3234+
3235+ def allow_names(self, *names):
3236+ all_names = ["ubuntu-series", "provider-type"]
3237+ all_names.extend(names)
3238+ from juju.machine import constraints
3239+ self.patch(constraints, "ALL_NAMES", all_names)
3240+
3241+ def test_register_unknown_name(self):
3242+ e = self.assertRaises(
3243+ AssertionError, ConstraintSet("provider").register, "nonsense")
3244+ self.assertEquals(str(e), "please update ALL_NAMES")
3245+
3246+ def test_register_known_names(self):
3247+ self.allow_names("foo", "blob")
3248+ cs = ConstraintSet("provider")
3249+ cs.register("foo")
3250+ cs.register("blob")
3251+ c1 = cs.parse(["foo=bar"]).with_series("series")
3252+ self.assertEquals(c1["foo"], "bar")
3253+ self.assertEquals(c1["blob"], None)
3254+ c2 = cs.parse(["foo=bar"]).with_series("series")
3255+ self.assertTrue(c1.can_satisfy(c2))
3256+ self.assertTrue(c2.can_satisfy(c1))
3257+
3258+ def test_unregistered_name(self):
3259+ self.allow_names("foo", "bar", "baz")
3260+ cs = ConstraintSet("provider")
3261+ cs.register("bar")
3262+ output = self.capture_logging()
3263+ constraints = cs.parse(["foo=1", "bar=2", "baz=3"])
3264+ self.assertIn("ignored irrelevant 'foo' constraint", output.getvalue())
3265+ self.assertIn("ignored irrelevant 'baz' constraint", output.getvalue())
3266+ self.assertEquals(constraints["bar"], "2")
3267+
3268+ def test_register_invisible(self):
3269+ self.allow_names("foo")
3270+ cs = ConstraintSet("provider")
3271+ cs.register("foo", visible=False)
3272+ e = self.assertRaises(ConstraintError, cs.parse, ["foo=bar"])
3273+ self.assertEquals(str(e), "Cannot set computed constraint: 'foo'")
3274+
3275+ def test_register_comparer(self):
3276+ self.allow_names("foo")
3277+ cs = ConstraintSet("provider")
3278+ cs.register("foo", comparer=operator.ne)
3279+ c1 = cs.parse(["foo=bar"]).with_series("series")
3280+ c2 = cs.parse(["foo=bar"]).with_series("series")
3281+ self.assertFalse(c1.can_satisfy(c2))
3282+ self.assertFalse(c2.can_satisfy(c1))
3283+ c3 = cs.parse(["foo=baz"]).with_series("series")
3284+ self.assertTrue(c1.can_satisfy(c3))
3285+ self.assertTrue(c3.can_satisfy(c1))
3286+
3287+ def test_register_default_and_converter(self):
3288+ self.allow_names("foo")
3289+ cs = ConstraintSet("provider")
3290+ cs.register("foo", default="star", converter=lambda s: "death-" + s)
3291+ c1 = cs.parse([])
3292+ self.assertEquals(c1["foo"], "death-star")
3293+ c1 = cs.parse(["foo=clock"])
3294+ self.assertEquals(c1["foo"], "death-clock")
3295+
3296+ def test_convert_wraps_ValueError(self):
3297+ self.allow_names("foo", "bar")
3298+
3299+ def raiser(e):
3300+ raise e
3301+ cs = ConstraintSet("provider")
3302+ cs.register("foo", converter=lambda s: raiser(ValueError(s)))
3303+ cs.register("bar", converter=lambda s: raiser(KeyError(s)))
3304+ self.assertRaises(ConstraintError, cs.parse, ["foo=1"])
3305+ self.assertRaises(KeyError, cs.parse, ["bar=1"])
3306+
3307+ def test_register_conflicts(self):
3308+ self.allow_names("foo", "bar", "baz", "qux")
3309+ cs = ConstraintSet("provider")
3310+ cs.register("foo")
3311+ cs.register("bar")
3312+ cs.register("baz")
3313+ cs.register("qux")
3314+ cs.parse(["foo=1", "bar=2", "baz=3", "qux=4"])
3315+
3316+ def assert_ambiguous(strs):
3317+ e = self.assertRaises(ConstraintError, cs.parse, strs)
3318+ self.assertTrue(str(e).startswith("Ambiguous constraints"))
3319+
3320+ cs.register_conflicts(["foo"], ["bar", "baz", "qux"])
3321+ assert_ambiguous(["foo=1", "bar=2"])
3322+ assert_ambiguous(["foo=1", "baz=3"])
3323+ assert_ambiguous(["foo=1", "qux=4"])
3324+ cs.parse(["foo=1"])
3325+ cs.parse(["bar=2", "baz=3", "qux=4"])
3326+
3327+ cs.register_conflicts(["bar", "baz"], ["qux"])
3328+ assert_ambiguous(["bar=2", "qux=4"])
3329+ assert_ambiguous(["baz=3", "qux=4"])
3330+ cs.parse(["foo=1"])
3331+ cs.parse(["bar=2", "baz=3"])
3332+ cs.parse(["qux=4"])
3333+
3334+ def test_register_generics_no_instance_types(self):
3335+ cs = ConstraintSet("provider")
3336+ cs.register_generics([])
3337+ c1 = cs.parse([])
3338+ self.assertEquals(c1["arch"], "amd64")
3339+ self.assertEquals(c1["cpu"], 1.0)
3340+ self.assertEquals(c1["mem"], 512.0)
3341+ self.assertFalse("instance-type" in c1)
3342+
3343+ c2 = cs.parse(["arch=any", "cpu=0", "mem=8G"])
3344+ self.assertEquals(c2["arch"], None)
3345+ self.assertEquals(c2["cpu"], 0.0)
3346+ self.assertEquals(c2["mem"], 8192.0)
3347+ self.assertFalse("instance-type" in c2)
3348+
3349+ def test_register_generics_with_instance_types(self):
3350+ cs = ConstraintSet("provider")
3351+ cs.register_generics(["a1.big", "c7.peculiar"])
3352+ c1 = cs.parse([])
3353+ self.assertEquals(c1["arch"], "amd64")
3354+ self.assertEquals(c1["cpu"], 1.0)
3355+ self.assertEquals(c1["mem"], 512.0)
3356+ self.assertEquals(c1["instance-type"], None)
3357+
3358+ c2 = cs.parse(["arch=any", "cpu=0", "mem=8G"])
3359+ self.assertEquals(c2["arch"], None)
3360+ self.assertEquals(c2["cpu"], 0.0)
3361+ self.assertEquals(c2["mem"], 8192.0)
3362+ self.assertEquals(c2["instance-type"], None)
3363+
3364+ c3 = cs.parse(["instance-type=c7.peculiar", "arch=i386"])
3365+ self.assertEquals(c3["arch"], "i386")
3366+ self.assertEquals(c3["cpu"], None)
3367+ self.assertEquals(c3["mem"], None)
3368+ self.assertEquals(c3["instance-type"], "c7.peculiar")
3369+
3370+ def assert_ambiguous(strs):
3371+ e = self.assertRaises(ConstraintError, cs.parse, strs)
3372+ self.assertTrue(str(e).startswith("Ambiguous constraints"))
3373+
3374+ assert_ambiguous(["cpu=1", "instance-type=c7.peculiar"])
3375+ assert_ambiguous(["mem=1024", "instance-type=c7.peculiar"])
3376+
3377+ c4 = cs.parse([])
3378+ c4.update(c2)
3379+ self.assertEquals(c4["arch"], None)
3380+ self.assertEquals(c4["cpu"], 0.0)
3381+ self.assertEquals(c4["mem"], 8192.0)
3382+ self.assertEquals(c4["instance-type"], None)
3383+
3384+ c5 = cs.parse(["instance-type=a1.big"])
3385+ c5.update(cs.parse(["arch=i386"]))
3386+ self.assertEquals(c5["arch"], "i386")
3387+ self.assertEquals(c5["cpu"], None)
3388+ self.assertEquals(c5["mem"], None)
3389+ self.assertEquals(c5["instance-type"], "a1.big")
3390+
3391+ c6 = cs.parse(["instance-type=a1.big"])
3392+ c6.update(cs.parse(["cpu=20"]))
3393+ self.assertEquals(c6["arch"], "amd64")
3394+ self.assertEquals(c6["cpu"], 20.0)
3395+ self.assertEquals(c6["mem"], None)
3396+ self.assertEquals(c6["instance-type"], None)
3397+
3398+ c7 = cs.parse(["instance-type="])
3399+ self.assertEquals(c7["arch"], "amd64")
3400+ self.assertEquals(c7["cpu"], 1.0)
3401+ self.assertEquals(c7["mem"], 512.0)
3402+ self.assertEquals(c7["instance-type"], None)
3403+
3404+ c8 = cs.parse(["instance-type=any"])
3405+ self.assertEquals(c8["arch"], "amd64")
3406+ self.assertEquals(c8["cpu"], 1.0)
3407+ self.assertEquals(c8["mem"], 512.0)
3408+ self.assertEquals(c8["instance-type"], None)
3409+
3410+ def test_load_validates(self):
3411+ self.allow_names("foo")
3412+ cs = ConstraintSet("provider")
3413+
3414+ def blam(s):
3415+ raise ValueError(s)
3416+
3417+ cs.register("foo", converter=blam)
3418+ e = self.assertRaises(ConstraintError, cs.load, {"foo": "bar"})
3419+ self.assertEquals(str(e), "Bad 'foo' constraint 'bar': bar")
3420
3421=== modified file 'juju/machine/unit.py'
3422--- juju/machine/unit.py 2012-02-21 12:14:11 +0000
3423+++ juju/machine/unit.py 2012-03-30 16:56:32 +0000
3424@@ -1,6 +1,5 @@
3425 import os
3426 import shutil
3427-import sys
3428 import logging
3429
3430 import juju
3431
3432=== modified file 'juju/providers/common/base.py'
3433--- juju/providers/common/base.py 2011-11-19 05:37:09 +0000
3434+++ juju/providers/common/base.py 2012-03-30 16:56:32 +0000
3435@@ -1,9 +1,10 @@
3436 import copy
3437 from operator import itemgetter
3438
3439-from twisted.internet.defer import inlineCallbacks, returnValue
3440+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
3441
3442 from juju.environment.errors import EnvironmentsConfigError
3443+from juju.machine.constraints import ConstraintSet
3444 from juju.state.placement import UNASSIGNED_POLICY
3445
3446
3447@@ -30,10 +31,13 @@
3448
3449 You may want to override the following methods, but you should be careful
3450 to call :class:`MachineProviderBase`'s implementation (or be very sure you
3451- don't need to:
3452+ don't need to):
3453
3454 * :meth:`__init__`
3455 * :meth:`get_serialization_data`
3456+ * :meth:`get_legacy_config_keys`
3457+ * :meth:`get_placement_policy`
3458+ * :meth:`get_constraint_set`
3459
3460 You probably shouldn't override anything else.
3461 """
3462@@ -48,11 +52,16 @@
3463 self.environment_name = environment_name
3464 self.config = config
3465
3466+ def get_constraint_set(self):
3467+ """Return the set of constraints that are valid for this provider."""
3468+ return succeed(ConstraintSet(self.provider_type))
3469+
3470+ def get_legacy_config_keys(self):
3471+ """Return any deprecated config keys that are set."""
3472+ return set() & set(self.config)
3473+
3474 def get_placement_policy(self):
3475- """Get the unit placement policy for the provider.
3476-
3477- :param preference: A user specified plcaement policy preference
3478- """
3479+ """Get the unit placement policy for the provider."""
3480 return self.config.get("placement", UNASSIGNED_POLICY)
3481
3482 def get_serialization_data(self):
3483@@ -151,9 +160,9 @@
3484 """
3485 return ZookeeperConnect(self).run(share=share)
3486
3487- def bootstrap(self):
3488+ def bootstrap(self, constraints):
3489 """Bootstrap an juju server in the provider."""
3490- return Bootstrap(self).run()
3491+ return Bootstrap(self, constraints).run()
3492
3493 def get_machine(self, instance_id):
3494 """Retrieve a provider machine by instance id.
3495
3496=== modified file 'juju/providers/common/bootstrap.py'
3497--- juju/providers/common/bootstrap.py 2011-09-22 13:23:00 +0000
3498+++ juju/providers/common/bootstrap.py 2012-03-30 16:56:32 +0000
3499@@ -1,5 +1,7 @@
3500 from cStringIO import StringIO
3501
3502+from twisted.internet.defer import inlineCallbacks, returnValue
3503+
3504 from juju.errors import EnvironmentNotFound, ProviderError
3505
3506 from .utils import log
3507@@ -10,8 +12,9 @@
3508 class Bootstrap(object):
3509 """Generic bootstrap operation class."""
3510
3511- def __init__(self, provider):
3512+ def __init__(self, provider, constraints):
3513 self._provider = provider
3514+ self._constraints = constraints
3515
3516 def run(self):
3517 """Get an existing zookeeper, or launch a new one.
3518@@ -47,6 +50,9 @@
3519 "Bootstrap aborted because file storage is not writable: %s"
3520 % str(failure.value))
3521
3522+ @inlineCallbacks
3523 def _launch_machine(self, unused):
3524 log.debug("Launching juju bootstrap instance.")
3525- return self._provider.start_machine({"machine-id": "0"}, master=True)
3526+ machines = yield self._provider.start_machine(
3527+ {"machine-id": "0", "constraints": self._constraints}, master=True)
3528+ returnValue(machines)
3529
3530=== modified file 'juju/providers/common/cloudinit.py'
3531--- juju/providers/common/cloudinit.py 2012-02-21 19:28:28 +0000
3532+++ juju/providers/common/cloudinit.py 2012-03-30 16:56:32 +0000
3533@@ -1,4 +1,6 @@
3534+from base64 import b64encode
3535 from subprocess import Popen, PIPE
3536+from yaml import safe_dump
3537
3538 from juju.errors import CloudInitError
3539 from juju.lib.upstart import UpstartService
3540@@ -29,14 +31,15 @@
3541 return scripts
3542
3543
3544-def _zookeeper_scripts(instance_id, secret, provider_type):
3545+def _zookeeper_scripts(instance_id, secret, constraints, provider_type):
3546 return [
3547 "juju-admin initialize"
3548 " --instance-id=%s"
3549 " --admin-identity=%s"
3550+ " --constraints-data=%s"
3551 " --provider-type=%s"
3552 % (instance_id, make_identity("admin:%s" % secret),
3553- provider_type)]
3554+ b64encode(safe_dump(constraints.data)), provider_type)]
3555
3556
3557 def _machine_scripts(machine_id, zookeeper_hosts):
3558@@ -143,6 +146,7 @@
3559 self._zookeeper = False
3560 self._zookeeper_hosts = []
3561 self._zookeeper_secret = None
3562+ self._constraints = None
3563 self._origin, self._origin_url = get_default_origin()
3564
3565 def add_ssh_key(self, key):
3566@@ -236,6 +240,13 @@
3567 """
3568 self._zookeeper_secret = secret
3569
3570+ def set_constraints(self, constraints):
3571+ """Specify the initial machine's constraints.
3572+
3573+ You only need to set this if this machine will be a zookeeper instance.
3574+ """
3575+ self._constraints = constraints
3576+
3577 def render(self):
3578 """Get content for a cloud-init file with appropriate specifications.
3579
3580@@ -265,6 +276,7 @@
3581 if self._zookeeper:
3582 require("_instance_id", "set_instance_id_accessor")
3583 require("_zookeeper_secret", "set_zookeeper_secret")
3584+ require("_constraints", "set_constraints")
3585 else:
3586 require("_zookeeper_hosts", "set_zookeeper_machines")
3587 if missing:
3588@@ -299,6 +311,7 @@
3589 scripts.extend(_zookeeper_scripts(
3590 self._instance_id,
3591 self._zookeeper_secret,
3592+ self._constraints,
3593 self._provider_type))
3594 scripts.extend(_machine_scripts(
3595 self._machine_id, self._join_zookeeper_hosts()))
3596
3597=== modified file 'juju/providers/common/launch.py'
3598--- juju/providers/common/launch.py 2011-09-30 04:52:20 +0000
3599+++ juju/providers/common/launch.py 2012-03-30 16:56:32 +0000
3600@@ -1,4 +1,6 @@
3601-from twisted.internet.defer import inlineCallbacks, returnValue
3602+from twisted.internet.defer import fail, inlineCallbacks, returnValue
3603+
3604+from juju.errors import ProviderError
3605
3606 from .cloudinit import CloudInit
3607 from .utils import get_user_authorized_keys
3608@@ -24,10 +26,27 @@
3609 .. automethod:: _create_cloud_init
3610 """
3611
3612- def __init__(self, provider, master=False, constraints=None):
3613+ def __init__(self, provider, constraints, master=False):
3614 self._provider = provider
3615+ self._constraints = constraints
3616 self._master = master
3617- self._constraints = constraints or {}
3618+
3619+ @classmethod
3620+ def launch(cls, provider, machine_data, master):
3621+ """Create and run a machine launch operation.
3622+
3623+ Exists for the convenience of the `MachineProvider` implementations
3624+ which actually use the "constraints" key in machine_data, which would
3625+ otherwise duplicate code.
3626+ """
3627+ if "machine-id" not in machine_data:
3628+ return fail(ProviderError(
3629+ "Cannot launch a machine without specifying a machine-id"))
3630+ if "constraints" not in machine_data:
3631+ return fail(ProviderError(
3632+ "Cannot launch a machine without specifying constraints"))
3633+ launcher = cls(provider, machine_data["constraints"], master)
3634+ return launcher.run(machine_data["machine-id"])
3635
3636 @inlineCallbacks
3637 def run(self, machine_id):
3638@@ -88,6 +107,7 @@
3639 if self._master:
3640 cloud_init.enable_bootstrap()
3641 cloud_init.set_zookeeper_secret(config["admin-secret"])
3642+ cloud_init.set_constraints(self._constraints)
3643 return cloud_init
3644
3645 def _on_new_zookeepers(self, machines):
3646
3647=== modified file 'juju/providers/common/tests/data/cloud_init_bootstrap'
3648--- juju/providers/common/tests/data/cloud_init_bootstrap 2012-02-21 19:28:28 +0000
3649+++ juju/providers/common/tests/data/cloud_init_bootstrap 2012-03-30 16:56:32 +0000
3650@@ -6,8 +6,8 @@
3651 output: {all: '| tee -a /var/log/cloud-init-output.log'}
3652 packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws, python-zookeeper,
3653 default-jre-headless, zookeeper, zookeeperd, juju]
3654-runcmd: [sudo mkdir -p /var/lib/juju, sudo mkdir -p
3655- /var/log/juju, 'juju-admin initialize --instance-id=token --admin-identity=admin:19vlzY4Vc3q4Ew5OsCwKYqrq1HI=
3656+runcmd: [sudo mkdir -p /var/lib/juju, sudo mkdir -p /var/log/juju, 'juju-admin initialize
3657+ --instance-id=token --admin-identity=admin:19vlzY4Vc3q4Ew5OsCwKYqrq1HI= --constraints-data=e2NwdTogJzIwJywgcHJvdmlkZXItdHlwZTogZHVtbXksIHVidW50dS1zZXJpZXM6IGFzdG9uaXNoaW5nfQo=
3658 --provider-type=dummy', 'cat >> /etc/init/juju-machine-agent.conf <<EOF
3659
3660 description "Juju machine agent"
3661
3662=== modified file 'juju/providers/common/tests/data/cloud_init_bootstrap_zookeepers'
3663--- juju/providers/common/tests/data/cloud_init_bootstrap_zookeepers 2012-02-21 19:28:28 +0000
3664+++ juju/providers/common/tests/data/cloud_init_bootstrap_zookeepers 2012-03-30 16:56:32 +0000
3665@@ -6,8 +6,8 @@
3666 output: {all: '| tee -a /var/log/cloud-init-output.log'}
3667 packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws, python-zookeeper,
3668 default-jre-headless, zookeeper, zookeeperd, juju]
3669-runcmd: [sudo mkdir -p /var/lib/juju, sudo mkdir -p
3670- /var/log/juju, 'juju-admin initialize --instance-id=token --admin-identity=admin:19vlzY4Vc3q4Ew5OsCwKYqrq1HI=
3671+runcmd: [sudo mkdir -p /var/lib/juju, sudo mkdir -p /var/log/juju, 'juju-admin initialize
3672+ --instance-id=token --admin-identity=admin:19vlzY4Vc3q4Ew5OsCwKYqrq1HI= --constraints-data=e2NwdTogJzIwJywgcHJvdmlkZXItdHlwZTogZHVtbXksIHVidW50dS1zZXJpZXM6IGFzdG9uaXNoaW5nfQo=
3673 --provider-type=dummy', 'cat >> /etc/init/juju-machine-agent.conf <<EOF
3674
3675 description "Juju machine agent"
3676
3677=== modified file 'juju/providers/common/tests/data/cloud_init_distro'
3678--- juju/providers/common/tests/data/cloud_init_distro 2012-02-21 19:28:28 +0000
3679+++ juju/providers/common/tests/data/cloud_init_distro 2012-03-30 16:56:32 +0000
3680@@ -4,9 +4,10 @@
3681 machine-data: {juju-provider-type: dummy, juju-zookeeper-hosts: 'cotswold:2181,longleat:2181',
3682 machine-id: passport}
3683 output: {all: '| tee -a /var/log/cloud-init-output.log'}
3684-packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws, python-zookeeper, juju]
3685-runcmd: [sudo mkdir -p /var/lib/juju, sudo mkdir -p
3686- /var/log/juju, 'cat >> /etc/init/juju-machine-agent.conf <<EOF
3687+packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws, python-zookeeper,
3688+ juju]
3689+runcmd: [sudo mkdir -p /var/lib/juju, sudo mkdir -p /var/log/juju, 'cat >> /etc/init/juju-machine-agent.conf
3690+ <<EOF
3691
3692 description "Juju machine agent"
3693
3694
3695=== modified file 'juju/providers/common/tests/data/cloud_init_ppa'
3696--- juju/providers/common/tests/data/cloud_init_ppa 2012-02-21 19:28:28 +0000
3697+++ juju/providers/common/tests/data/cloud_init_ppa 2012-03-30 16:56:32 +0000
3698@@ -6,9 +6,10 @@
3699 machine-data: {juju-provider-type: dummy, juju-zookeeper-hosts: 'cotswold:2181,longleat:2181',
3700 machine-id: passport}
3701 output: {all: '| tee -a /var/log/cloud-init-output.log'}
3702-packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws, python-zookeeper, juju]
3703-runcmd: [sudo mkdir -p /var/lib/juju, sudo mkdir -p
3704- /var/log/juju, 'cat >> /etc/init/juju-machine-agent.conf <<EOF
3705+packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws, python-zookeeper,
3706+ juju]
3707+runcmd: [sudo mkdir -p /var/lib/juju, sudo mkdir -p /var/log/juju, 'cat >> /etc/init/juju-machine-agent.conf
3708+ <<EOF
3709
3710 description "Juju machine agent"
3711
3712
3713=== modified file 'juju/providers/common/tests/test_base.py'
3714--- juju/providers/common/tests/test_base.py 2011-09-27 02:14:56 +0000
3715+++ juju/providers/common/tests/test_base.py 2012-03-30 16:56:32 +0000
3716@@ -63,6 +63,10 @@
3717 self.assertEqual(
3718 provider.get_placement_policy(), "local")
3719
3720+ def test_get_legacy_config_keys(self):
3721+ provider = DummyProvider()
3722+ self.assertEqual(provider.get_legacy_config_keys(), set())
3723+
3724 def test_get_machine_error(self):
3725 provider = DummyProvider()
3726 provider.get_machines = self.mocker.mock()
3727
3728=== modified file 'juju/providers/common/tests/test_bootstrap.py'
3729--- juju/providers/common/tests/test_bootstrap.py 2011-09-15 18:50:23 +0000
3730+++ juju/providers/common/tests/test_bootstrap.py 2012-03-30 16:56:32 +0000
3731@@ -1,10 +1,11 @@
3732 import logging
3733 import tempfile
3734
3735-from twisted.internet.defer import fail, succeed
3736+from twisted.internet.defer import fail, succeed, inlineCallbacks
3737
3738 from juju.errors import EnvironmentNotFound, ProviderError
3739 from juju.lib.testing import TestCase
3740+from juju.machine.tests.test_constraints import dummy_cs
3741 from juju.providers.common.base import MachineProviderBase
3742 from juju.providers.dummy import DummyMachine, FileStorage
3743
3744@@ -27,7 +28,11 @@
3745
3746 class DummyProvider(MachineProviderBase):
3747
3748- def __init__(self, file_storage, zookeeper):
3749+ provider_type = "dummy"
3750+ config = {"default-series": "splendid"}
3751+
3752+ def __init__(self, test, file_storage, zookeeper):
3753+ self._test = test
3754 self._file_storage = file_storage
3755 self._zookeeper = zookeeper
3756
3757@@ -41,24 +46,34 @@
3758 return succeed([self._zookeeper])
3759 return fail(EnvironmentNotFound())
3760
3761- def start_machine(self, machine_id, master=False):
3762- assert master is True
3763+ def start_machine(self, machine_data, master=False):
3764+ self._test.assertTrue(master)
3765+ self._test.assertEquals(machine_data["machine-id"], "0")
3766+ constraints = machine_data["constraints"]
3767+ self._test.assertEquals(constraints["provider-type"], "dummy")
3768+ self._test.assertEquals(constraints["ubuntu-series"], "splendid")
3769+ self._test.assertEquals(constraints["arch"], "arm")
3770 return [DummyMachine("i-keepzoos")]
3771
3772
3773 class BootstrapTest(TestCase):
3774
3775+ @inlineCallbacks
3776+ def setUp(self):
3777+ yield super(BootstrapTest, self).setUp()
3778+ self.constraints = dummy_cs.parse(["arch=arm"]).with_series("splendid")
3779+
3780 def test_unknown_error(self):
3781- provider = DummyProvider(None, SomeError())
3782- d = provider.bootstrap()
3783+ provider = DummyProvider(self, None, SomeError())
3784+ d = provider.bootstrap(self.constraints)
3785 self.assertFailure(d, SomeError)
3786 return d
3787
3788 def test_zookeeper_exists(self):
3789 log = self.capture_logging("juju.common", level=logging.DEBUG)
3790 provider = DummyProvider(
3791- WorkingFileStorage(), DummyMachine("i-alreadykeepzoos"))
3792- d = provider.bootstrap()
3793+ self, WorkingFileStorage(), DummyMachine("i-alreadykeepzoos"))
3794+ d = provider.bootstrap(self.constraints)
3795
3796 def verify(machines):
3797 (machine,) = machines
3798@@ -73,8 +88,8 @@
3799 return d
3800
3801 def test_bad_storage(self):
3802- provider = DummyProvider(UnwritableFileStorage(), None)
3803- d = provider.bootstrap()
3804+ provider = DummyProvider(self, UnwritableFileStorage(), None)
3805+ d = provider.bootstrap(self.constraints)
3806 self.assertFailure(d, ProviderError)
3807
3808 def verify(error):
3809@@ -87,8 +102,8 @@
3810
3811 def test_create_zookeeper(self):
3812 log = self.capture_logging("juju.common", level=logging.DEBUG)
3813- provider = DummyProvider(WorkingFileStorage(), None)
3814- d = provider.bootstrap()
3815+ provider = DummyProvider(self, WorkingFileStorage(), None)
3816+ d = provider.bootstrap(self.constraints)
3817
3818 def verify(machines):
3819 (machine,) = machines
3820
3821=== modified file 'juju/providers/common/tests/test_cloudinit.py'
3822--- juju/providers/common/tests/test_cloudinit.py 2012-02-22 09:23:16 +0000
3823+++ juju/providers/common/tests/test_cloudinit.py 2012-03-30 16:56:32 +0000
3824@@ -5,6 +5,7 @@
3825
3826 from juju.errors import CloudInitError
3827 from juju.lib.testing import TestCase
3828+from juju.machine.tests.test_constraints import dummy_cs
3829 from juju.providers.common.cloudinit import (
3830 CloudInit, parse_juju_origin, get_default_origin)
3831 from juju.providers.dummy import DummyMachine
3832@@ -34,6 +35,8 @@
3833 cloud_init.set_provider_type("dummy")
3834 cloud_init.set_instance_id_accessor("token")
3835 cloud_init.set_zookeeper_secret("seekrit")
3836+ cloud_init.set_constraints(
3837+ dummy_cs.parse(["cpu=20"]).with_series("astonishing"))
3838 cloud_init.set_juju_source(distro=True)
3839 if with_zookeepers:
3840 cloud_init.set_zookeeper_machines([
3841@@ -64,7 +67,7 @@
3842 str(error),
3843 "Incomplete cloud-init: you need to call add_ssh_key, "
3844 "set_machine_id, set_provider_type, set_instance_id_accessor, "
3845- "set_zookeeper_secret")
3846+ "set_zookeeper_secret, set_constraints")
3847
3848 def test_source_validate(self):
3849 bad_choices = (
3850
3851=== modified file 'juju/providers/common/tests/test_launch.py'
3852--- juju/providers/common/tests/test_launch.py 2011-09-19 23:33:37 +0000
3853+++ juju/providers/common/tests/test_launch.py 2012-03-30 16:56:32 +0000
3854@@ -1,6 +1,6 @@
3855 import tempfile
3856
3857-from twisted.internet.defer import fail, succeed
3858+from twisted.internet.defer import fail, succeed, inlineCallbacks, returnValue
3859
3860 from juju.errors import EnvironmentNotFound
3861 from juju.lib.testing import TestCase
3862@@ -9,10 +9,12 @@
3863 from juju.providers.dummy import DummyMachine, FileStorage
3864
3865
3866-def launch_machine(test, master, zookeeper):
3867+def get_classes(test, master, machine_id, zookeeper):
3868
3869 class DummyProvider(MachineProviderBase):
3870
3871+ provider_type = "dummy"
3872+
3873 def __init__(self):
3874 self.config = {"admin-secret": "BLAH"}
3875 self._file_storage = FileStorage(tempfile.mkdtemp())
3876@@ -27,54 +29,73 @@
3877
3878 class DummyLaunchMachine(LaunchMachine):
3879
3880- def start_machine(self, machine_id, zookeepers):
3881- test.assertEquals(machine_id, "1234")
3882+ def start_machine(self, actual_machine_id, zookeepers):
3883+ test.assertEquals(actual_machine_id, machine_id)
3884 test.assertEquals(zookeepers, filter(None, [zookeeper]))
3885 test.assertEquals(self._master, master)
3886- test.assertEquals(self._constraints, {})
3887+ test.assertEquals(self._constraints, self.expect_constraints)
3888 return succeed([DummyMachine("i-malive")])
3889
3890+ return DummyProvider, DummyLaunchMachine
3891+
3892+
3893+@inlineCallbacks
3894+def launch_machine(test, master, zookeeper):
3895+ DummyProvider, DummyLaunchMachine = get_classes(
3896+ test, master, "1234", zookeeper)
3897+
3898 provider = DummyProvider()
3899- d = DummyLaunchMachine(provider, master).run("1234")
3900- return provider, d
3901+ cs = yield provider.get_constraint_set()
3902+ constraints = cs.parse([])
3903+ launch = DummyLaunchMachine(provider, constraints, master)
3904+ launch.expect_constraints = constraints
3905+ machines = yield launch.run("1234")
3906+ returnValue((provider, machines))
3907
3908
3909 class LaunchMachineTest(TestCase):
3910
3911+ @inlineCallbacks
3912 def assert_success(self, master, zookeeper):
3913- provider, d = launch_machine(self, master, zookeeper)
3914-
3915- def verify(machines):
3916- (machine,) = machines
3917- self.assertTrue(isinstance(machine, DummyMachine))
3918- self.assertEquals(machine.instance_id, "i-malive")
3919- return provider
3920- d.addCallback(verify)
3921- return d
3922+ provider, machines = yield launch_machine(self, master, zookeeper)
3923+ (machine,) = machines
3924+ self.assertTrue(isinstance(machine, DummyMachine))
3925+ self.assertEquals(machine.instance_id, "i-malive")
3926+ returnValue(provider)
3927
3928 def test_start_nonzookeeper_no_zookeepers(self):
3929 """Starting a non-zookeeper without a zookeeper is an error"""
3930- unused, d = launch_machine(self, False, None)
3931- self.assertFailure(d, EnvironmentNotFound)
3932- return d
3933+ return self.assertFailure(
3934+ launch_machine(self, False, None), EnvironmentNotFound)
3935
3936+ @inlineCallbacks
3937 def test_start_zookeeper_no_zookeepers(self):
3938 """A new zookeeper should be recorded in provider state"""
3939- d = self.assert_success(True, None)
3940-
3941- def verify(provider):
3942- provider_state = yield provider.load_state()
3943- self.assertEquals(
3944- provider_state, {"zookeeper-instances": ["i-malive"]})
3945- d.addCallback(verify)
3946- return d
3947-
3948+ provider = yield self.assert_success(True, None)
3949+ provider_state = yield provider.load_state()
3950+ self.assertEquals(
3951+ provider_state, {"zookeeper-instances": ["i-malive"]})
3952+
3953+ @inlineCallbacks
3954 def test_works_with_zookeeper(self):
3955 """Creating a non-zookeeper machine should not alter provider state"""
3956- d = self.assert_success(False, DummyMachine("i-keepzoos"))
3957-
3958- def verify(provider):
3959- provider_state = yield provider.load_state()
3960- self.assertEquals(provider_state, False)
3961- d.addCallback(verify)
3962- return d
3963+ provider = yield self.assert_success(False, DummyMachine("i-keepzoos"))
3964+ provider_state = yield provider.load_state()
3965+ self.assertEquals(provider_state, False)
3966+
3967+ @inlineCallbacks
3968+ def test_convenience_launch(self):
3969+ DummyProvider, DummyLaunchMachine = get_classes(
3970+ self, True, "999", None)
3971+
3972+ provider = DummyProvider()
3973+ cs = yield provider.get_constraint_set()
3974+ constraints = cs.parse(["arch=arm", "mem=1G"])
3975+ DummyLaunchMachine.expect_constraints = constraints
3976+ machine_data = {
3977+ "machine-id": "999", "constraints": constraints}
3978+ machines = yield DummyLaunchMachine.launch(
3979+ provider, machine_data, True)
3980+ (machine,) = machines
3981+ self.assertTrue(isinstance(machine, DummyMachine))
3982+ self.assertEquals(machine.instance_id, "i-malive")
3983
3984=== modified file 'juju/providers/dummy.py'
3985--- juju/providers/dummy.py 2011-09-28 09:48:30 +0000
3986+++ juju/providers/dummy.py 2012-03-30 16:56:32 +0000
3987@@ -11,6 +11,7 @@
3988
3989
3990 from juju.machine import ProviderMachine
3991+from juju.machine.constraints import ConstraintSet
3992 from juju.state.placement import UNASSIGNED_POLICY
3993 from juju.providers.common.files import FileStorage
3994
3995@@ -34,6 +35,9 @@
3996 self._state = None
3997 self._storage = None
3998
3999+ def get_legacy_config_keys(self):
4000+ return set(("some-legacy-key",)) & set(self.config)
4001+
4002 def get_placement_policy(self):
4003 """Get the unit placement policy for the provider.
4004
4005@@ -41,6 +45,11 @@
4006 """
4007 return self.config.get("placement", UNASSIGNED_POLICY)
4008
4009+ def get_constraint_set(self):
4010+ cs = ConstraintSet(self.provider_type)
4011+ cs.register_generics([])
4012+ return succeed(cs)
4013+
4014 @property
4015 def provider_type(self):
4016 return "dummy"
4017@@ -91,7 +100,7 @@
4018 return succeed(machine)
4019 return fail(MachinesNotFound([instance_id]))
4020
4021- def bootstrap(self):
4022+ def bootstrap(self, constraints):
4023 """
4024 Bootstrap juju on the machine provider.
4025 """
4026
4027=== modified file 'juju/providers/ec2/__init__.py'
4028--- juju/providers/ec2/__init__.py 2011-09-19 17:17:00 +0000
4029+++ juju/providers/ec2/__init__.py 2012-03-30 16:56:32 +0000
4030@@ -1,7 +1,7 @@
4031 import os
4032 import re
4033
4034-from twisted.internet.defer import fail, inlineCallbacks, returnValue
4035+from twisted.internet.defer import inlineCallbacks, returnValue
4036
4037 from txaws.ec2.exception import EC2Error
4038 from txaws.service import AWSServiceRegion
4039@@ -16,7 +16,7 @@
4040 from .securitygroup import (
4041 open_provider_port, close_provider_port, get_provider_opened_ports,
4042 remove_security_groups, destroy_environment_security_group)
4043-from .utils import get_region_uri
4044+from .utils import convert_zone, get_region_uri, DEFAULT_REGION, INSTANCE_TYPES
4045
4046
4047 class MachineProvider(MachineProviderBase):
4048@@ -26,7 +26,7 @@
4049 super(MachineProvider, self).__init__(environment_name, config)
4050
4051 if not config.get("ec2-uri"):
4052- ec2_uri = get_region_uri(config.get("region", "us-east-1"))
4053+ ec2_uri = get_region_uri(config.get("region", DEFAULT_REGION))
4054 else:
4055 ec2_uri = config.get("ec2-uri")
4056
4057@@ -42,6 +42,30 @@
4058 def provider_type(self):
4059 return "ec2"
4060
4061+ @property
4062+ def using_amazon(self):
4063+ return "ec2-uri" not in self.config
4064+
4065+ @inlineCallbacks
4066+ def get_constraint_set(self):
4067+ """Return the set of constraints that are valid for this provider."""
4068+ cs = yield super(MachineProvider, self).get_constraint_set()
4069+ if self.using_amazon:
4070+ # Expose EC2 instance types/zones on AWS itelf, not private clouds.
4071+ cs.register_generics(INSTANCE_TYPES.keys())
4072+ cs.register("ec2-zone", converter=convert_zone)
4073+ returnValue(cs)
4074+
4075+ def get_legacy_config_keys(self):
4076+ """Return any deprecated config keys that are set"""
4077+ legacy = super(MachineProvider, self).get_legacy_config_keys()
4078+ if self.using_amazon:
4079+ # In the absence of a generic instance-type/image-id mechanism,
4080+ # these keys remain valid on private clouds.
4081+ amazon_legacy = set(("default-image-id", "default-instance-type"))
4082+ legacy.update(amazon_legacy.intersection(self.config))
4083+ return legacy
4084+
4085 def get_serialization_data(self):
4086 """Get provider configuration suitable for serialization.
4087
4088@@ -67,12 +91,7 @@
4089 and run a provisioning agent, in addition to running a machine
4090 agent.
4091 """
4092- if "machine-id" not in machine_data:
4093- return fail(ProviderError(
4094- "Cannot launch a machine without specifying a machine-id"))
4095- machine_id = machine_data["machine-id"]
4096- constraints = machine_data.get("constraints", {})
4097- return EC2LaunchMachine(self, master, constraints).run(machine_id)
4098+ return EC2LaunchMachine.launch(self, machine_data, master)
4099
4100 @inlineCallbacks
4101 def get_machines(self, instance_ids=()):
4102
4103=== modified file 'juju/providers/ec2/launch.py'
4104--- juju/providers/ec2/launch.py 2011-09-19 20:38:45 +0000
4105+++ juju/providers/ec2/launch.py 2012-03-30 16:56:32 +0000
4106@@ -6,7 +6,7 @@
4107 from juju.providers.common.launch import LaunchMachine
4108
4109 from .machine import machine_from_instance
4110-from .utils import get_image_id, log
4111+from .utils import get_machine_spec, log, DEFAULT_REGION
4112
4113
4114 class EC2LaunchMachine(LaunchMachine):
4115@@ -34,17 +34,21 @@
4116 "$(curl http://169.254.169.254/1.0/meta-data/instance-id)")
4117 user_data = cloud_init.render()
4118
4119- instance_type = self._provider.config.get(
4120- "default-instance-type", "m1.small")
4121- image_id = yield get_image_id(self._provider.config, self._constraints)
4122+ availability_zone = self._constraints["ec2-zone"]
4123+ if availability_zone is not None:
4124+ region = self._provider.config.get("region", DEFAULT_REGION)
4125+ availability_zone = region + availability_zone
4126+ spec = yield get_machine_spec(self._provider.config, self._constraints)
4127 security_groups = yield self._ensure_groups(machine_id)
4128
4129+ log.debug("Launching with machine spec %s", spec)
4130 instances = yield self._provider.ec2.run_instances(
4131- image_id=image_id,
4132- instance_type=instance_type,
4133 min_count=1,
4134 max_count=1,
4135+ image_id=spec.image_id,
4136+ instance_type=spec.instance_type,
4137 security_groups=security_groups,
4138+ availability_zone=availability_zone,
4139 user_data=user_data)
4140
4141 returnValue([machine_from_instance(i) for i in instances])
4142
4143=== modified file 'juju/providers/ec2/tests/common.py'
4144--- juju/providers/ec2/tests/common.py 2011-09-29 05:35:04 +0000
4145+++ juju/providers/ec2/tests/common.py 2012-03-30 16:56:32 +0000
4146@@ -1,6 +1,6 @@
4147 from yaml import dump
4148
4149-from twisted.internet.defer import fail, succeed
4150+from twisted.internet.defer import fail, succeed, inlineCallbacks, returnValue
4151
4152 from txaws.s3.client import S3Client
4153 from txaws.s3.exception import S3Error
4154@@ -14,6 +14,13 @@
4155
4156 MATCH_GROUP = MATCH(lambda x: x.startswith("juju-moon"))
4157
4158+_constraints_provider = MachineProvider("", {})
4159+
4160+@inlineCallbacks
4161+def get_constraints(strs, series="splendid"):
4162+ cs = yield _constraints_provider.get_constraint_set()
4163+ returnValue(cs.parse(strs).with_series(series))
4164+
4165
4166 class EC2TestMixin(object):
4167
4168@@ -26,7 +33,8 @@
4169 "admin-secret": "magic-beans",
4170 "access-key": "0f62e973d5f8",
4171 "secret-key": "3e5a7c653f59",
4172- "control-bucket": self.env_name}
4173+ "control-bucket": self.env_name,
4174+ "default-series": "splendid"}
4175
4176 def get_provider(self):
4177 """Return the ec2 machine provider.
4178@@ -81,7 +89,7 @@
4179
4180 class EC2MachineLaunchMixin(object):
4181
4182- def _mock_launch_utils(self, ami_name="ami-default", **get_ami_kwargs):
4183+ def _mock_launch_utils(self, ami_name="ami-default", get_ami_args=()):
4184 get_public_key = self.mocker.replace(
4185 "juju.providers.common.utils.get_user_authorized_keys")
4186
4187@@ -91,16 +99,12 @@
4188 get_public_key(MATCH(match_config))
4189 self.mocker.result("zebra")
4190
4191- if not get_ami_kwargs:
4192- return
4193+ get_ami_args = get_ami_args or (
4194+ "splendid", "amd64", "us-east-1", False)
4195 get_ami = self.mocker.replace(
4196 "juju.providers.ec2.utils.get_current_ami")
4197- get_ami(KWARGS)
4198-
4199- def check_kwargs(**kwargs):
4200- self.assertEquals(kwargs, get_ami_kwargs)
4201- return succeed(ami_name)
4202- self.mocker.call(check_kwargs)
4203+ get_ami(*get_ami_args)
4204+ self.mocker.result(succeed(ami_name))
4205
4206 def _mock_create_group(self):
4207 group_name = "juju-%s" % self.env_name
4208
4209=== modified file 'juju/providers/ec2/tests/data/bootstrap_cloud_init'
4210--- juju/providers/ec2/tests/data/bootstrap_cloud_init 2012-02-21 19:28:28 +0000
4211+++ juju/providers/ec2/tests/data/bootstrap_cloud_init 2012-03-30 16:56:32 +0000
4212@@ -5,10 +5,10 @@
4213 output: {all: '| tee -a /var/log/cloud-init-output.log'}
4214 packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws, python-zookeeper,
4215 default-jre-headless, zookeeper, zookeeperd, juju]
4216-runcmd: [sudo mkdir -p /var/lib/juju, sudo mkdir -p
4217- /var/log/juju, 'juju-admin initialize --instance-id=$(curl http://169.254.169.254/1.0/meta-data/instance-id)
4218- --admin-identity=admin:JbJ6sDGV37EHzbG9FPvttk64cmg= --provider-type=ec2', 'cat
4219- >> /etc/init/juju-machine-agent.conf <<EOF
4220+runcmd: [sudo mkdir -p /var/lib/juju, sudo mkdir -p /var/log/juju, 'juju-admin initialize
4221+ --instance-id=$(curl http://169.254.169.254/1.0/meta-data/instance-id) --admin-identity=admin:JbJ6sDGV37EHzbG9FPvttk64cmg=
4222+ --constraints-data=e2NwdTogbnVsbCwgaW5zdGFuY2UtdHlwZTogbTEuc21hbGwsIG1lbTogbnVsbCwgcHJvdmlkZXItdHlwZTogZWMyLCB1YnVudHUtc2VyaWVzOiBzcGxlbmRpZH0K
4223+ --provider-type=ec2', 'cat >> /etc/init/juju-machine-agent.conf <<EOF
4224
4225 description "Juju machine agent"
4226
4227
4228=== modified file 'juju/providers/ec2/tests/test_bootstrap.py'
4229--- juju/providers/ec2/tests/test_bootstrap.py 2011-09-30 04:52:20 +0000
4230+++ juju/providers/ec2/tests/test_bootstrap.py 2012-03-30 16:56:32 +0000
4231@@ -3,7 +3,7 @@
4232
4233 import yaml
4234
4235-from twisted.internet.defer import succeed
4236+from twisted.internet.defer import succeed, inlineCallbacks
4237
4238 from txaws.ec2.model import SecurityGroup
4239
4240@@ -11,7 +11,7 @@
4241 from juju.lib.testing import TestCase
4242 from juju.providers.ec2.machine import EC2ProviderMachine
4243
4244-from .common import EC2TestMixin, EC2MachineLaunchMixin
4245+from .common import EC2TestMixin, EC2MachineLaunchMixin, get_constraints
4246
4247
4248 DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "data")
4249@@ -50,8 +50,10 @@
4250 max_count=1,
4251 min_count=1,
4252 security_groups=["juju-moon", "juju-moon-0"],
4253+ availability_zone=None,
4254 user_data=MATCH(verify_user_data))
4255
4256+ @inlineCallbacks
4257 def test_launch_bootstrap(self):
4258 """The provider bootstrap can launch a bootstrap/zookeeper machine."""
4259
4260@@ -64,23 +66,20 @@
4261 self.mocker.result(succeed([]))
4262 self._mock_create_group()
4263 self._mock_create_machine_group(0)
4264- self._mock_launch_utils(region="us-east-1")
4265+ self._mock_launch_utils()
4266 self._mock_launch()
4267 self.mocker.result(succeed([]))
4268 self._mock_save()
4269 self.mocker.replay()
4270
4271 provider = self.get_provider()
4272- deferred = provider.bootstrap()
4273-
4274- def check_log(result):
4275- log_text = log.getvalue()
4276- self.assertIn("Launching juju bootstrap instance", log_text)
4277- self.assertNotIn("previously bootstrapped", log_text)
4278-
4279- deferred.addCallback(check_log)
4280- return deferred
4281-
4282+ constraints = yield get_constraints(["instance-type=m1.small"])
4283+ yield provider.bootstrap(constraints)
4284+ log_text = log.getvalue()
4285+ self.assertIn("Launching juju bootstrap instance", log_text)
4286+ self.assertNotIn("previously bootstrapped", log_text)
4287+
4288+ @inlineCallbacks
4289 def test_launch_bootstrap_existing_provider_group(self):
4290 """
4291 When launching a bootstrap instance the provider will use an existing
4292@@ -94,15 +93,17 @@
4293 self.mocker.result(succeed([
4294 SecurityGroup("juju-%s" % self.env_name, "")]))
4295 self._mock_create_machine_group(0)
4296- self._mock_launch_utils(region="us-east-1")
4297+ self._mock_launch_utils()
4298 self._mock_launch()
4299 self.mocker.result(succeed([]))
4300 self._mock_save()
4301 self.mocker.replay()
4302
4303 provider = self.get_provider()
4304- return provider.bootstrap()
4305+ constraints = yield get_constraints(["instance-type=m1.small"])
4306+ yield provider.bootstrap(constraints)
4307
4308+ @inlineCallbacks
4309 def test_run_with_loaded_state(self):
4310 """
4311 If the provider bootstrap is run when there is already a running
4312@@ -116,21 +117,18 @@
4313 self.mocker.replay()
4314
4315 log = self.capture_logging("juju.common")
4316-
4317- def validate_result(result):
4318- self.assertTrue(result)
4319- machine = result.pop()
4320- self.assertTrue(isinstance(machine, EC2ProviderMachine))
4321- self.assertEqual(machine.instance_id, "i-foobar")
4322- self.assertEquals(
4323- log.getvalue(),
4324- "juju environment previously bootstrapped.\n")
4325-
4326 provider = self.get_provider()
4327- d = provider.bootstrap()
4328- d.addCallback(validate_result)
4329- return d
4330-
4331+ constraints = yield get_constraints(["instance-type=m1.small"])
4332+ machines = yield provider.bootstrap(constraints)
4333+
4334+ (machine,) = machines
4335+ self.assertTrue(isinstance(machine, EC2ProviderMachine))
4336+ self.assertEqual(machine.instance_id, "i-foobar")
4337+ self.assertEquals(
4338+ log.getvalue(),
4339+ "juju environment previously bootstrapped.\n")
4340+
4341+ @inlineCallbacks
4342 def test_run_with_launch(self):
4343 """
4344 The provider bootstrap will launch an instance when run if there
4345@@ -143,16 +141,14 @@
4346 self.mocker.result(succeed([
4347 SecurityGroup("juju-%s" % self.env_name, "")]))
4348 self._mock_create_machine_group(0)
4349- self._mock_launch_utils(region="us-east-1")
4350+ self._mock_launch_utils()
4351 self._mock_launch()
4352 self.mocker.result(succeed([self.get_instance("i-foobar")]))
4353 self._mock_save()
4354 self.mocker.replay()
4355
4356- def validate_result(result):
4357- (machine,) = result
4358- self.assert_machine(machine, "i-foobar", "")
4359 provider = self.get_provider()
4360- d = provider.bootstrap()
4361- d.addCallback(validate_result)
4362- return d
4363+ constraints = yield get_constraints(["instance-type=m1.small"])
4364+ machines = yield provider.bootstrap(constraints)
4365+ (machine,) = machines
4366+ self.assert_machine(machine, "i-foobar", "")
4367
4368=== modified file 'juju/providers/ec2/tests/test_launch.py'
4369--- juju/providers/ec2/tests/test_launch.py 2011-09-28 05:22:36 +0000
4370+++ juju/providers/ec2/tests/test_launch.py 2012-03-30 16:56:32 +0000
4371@@ -13,15 +13,26 @@
4372 from juju.lib.testing import TestCase
4373 from juju.lib.mocker import MATCH
4374
4375-from .common import EC2TestMixin, EC2MachineLaunchMixin
4376+from .common import EC2TestMixin, EC2MachineLaunchMixin, get_constraints
4377
4378 DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "data")
4379
4380
4381 class EC2MachineLaunchTest(EC2TestMixin, EC2MachineLaunchMixin, TestCase):
4382
4383+ @inlineCallbacks
4384+ def setUp(self):
4385+ yield super(EC2MachineLaunchTest, self).setUp()
4386+ self.constraints = yield get_constraints([])
4387+ self.gen_constraints = yield get_constraints(["cpu=20", "mem=7168"])
4388+ self.gen_constraints = self.gen_constraints.with_series("dribbly")
4389+ self.ec2_constraints = yield get_constraints(
4390+ ["instance-type=cc2.8xlarge", "ec2-zone=b"])
4391+ self.ec2_constraints = self.ec2_constraints.with_series("vast")
4392+
4393 def _mock_launch(self, instance, expect_ami="ami-default",
4394 expect_instance_type="m1.small",
4395+ expect_availability_zone=None,
4396 cloud_init="launch_cloud_init"):
4397
4398 def verify_user_data(data):
4399@@ -37,6 +48,7 @@
4400 max_count=1,
4401 min_count=1,
4402 security_groups=["juju-moon", "juju-moon-1"],
4403+ availability_zone=expect_availability_zone,
4404 user_data=MATCH(verify_user_data))
4405
4406 self.mocker.result(succeed([instance]))
4407@@ -62,7 +74,7 @@
4408 self.mocker.result(succeed([]))
4409 self._mock_create_group()
4410 self._mock_create_machine_group("1")
4411- self._mock_launch_utils(region="us-east-1")
4412+ self._mock_launch_utils()
4413 self._mock_get_zookeeper_hosts()
4414 self._mock_launch(self.get_instance("i-foobar"))
4415 self.mocker.replay()
4416@@ -71,18 +83,28 @@
4417 (machine,) = result
4418 self.assert_machine(machine, "i-foobar", "")
4419 provider = self.get_provider()
4420- d = provider.start_machine({"machine-id": "1"})
4421+ d = provider.start_machine({
4422+ "machine-id": "1", "constraints": self.constraints})
4423 d.addCallback(verify_result)
4424 return d
4425
4426 @inlineCallbacks
4427+ def test_provider_launch_requires_constraints(self):
4428+ self.mocker.replay()
4429+ provider = self.get_provider()
4430+ d = provider.start_machine({"machine-id": "1"})
4431+ e = yield self.assertFailure(d, ProviderError)
4432+ self.assertEquals(
4433+ str(e), "Cannot launch a machine without specifying constraints")
4434+
4435+ @inlineCallbacks
4436 def test_provider_launch_using_branch(self):
4437 """Can use a juju branch to launch a machine"""
4438 self.ec2.describe_security_groups()
4439 self.mocker.result(succeed([]))
4440 self._mock_create_group()
4441 self._mock_create_machine_group("1")
4442- self._mock_launch_utils(region="us-east-1")
4443+ self._mock_launch_utils()
4444 self._mock_get_zookeeper_hosts()
4445 self._mock_launch(
4446 self.get_instance("i-foobar"),
4447@@ -91,7 +113,8 @@
4448
4449 provider = self.get_provider()
4450 provider.config["juju-origin"] = "lp:~wizard/juju-juicebar"
4451- machines = yield provider.start_machine({"machine-id": "1"})
4452+ machines = yield provider.start_machine({
4453+ "machine-id": "1", "constraints": self.constraints})
4454 self.assert_machine(machines[0], "i-foobar", "")
4455
4456 @inlineCallbacks
4457@@ -101,7 +124,7 @@
4458 self.mocker.result(succeed([]))
4459 self._mock_create_group()
4460 self._mock_create_machine_group("1")
4461- self._mock_launch_utils(region="us-east-1")
4462+ self._mock_launch_utils()
4463 self._mock_get_zookeeper_hosts()
4464 self._mock_launch(
4465 self.get_instance("i-foobar"),
4466@@ -110,7 +133,8 @@
4467
4468 provider = self.get_provider()
4469 provider.config["juju-origin"] = "ppa"
4470- machines = yield provider.start_machine({"machine-id": "1"})
4471+ machines = yield provider.start_machine({
4472+ "machine-id": "1", "constraints": self.constraints})
4473 self.assert_machine(machines[0], "i-foobar", "")
4474
4475 @inlineCallbacks
4476@@ -120,7 +144,7 @@
4477 self.mocker.result(succeed([]))
4478 self._mock_create_group()
4479 self._mock_create_machine_group("1")
4480- self._mock_launch_utils(region="us-east-1")
4481+ self._mock_launch_utils()
4482 self._mock_get_zookeeper_hosts()
4483 self._mock_launch(
4484 self.get_instance("i-foobar"),
4485@@ -129,7 +153,8 @@
4486
4487 provider = self.get_provider()
4488 provider.config["juju-origin"] = "distro"
4489- machines = yield provider.start_machine({"machine-id": "1"})
4490+ machines = yield provider.start_machine({
4491+ "machine-id": "1", "constraints": self.constraints})
4492 self.assert_machine(machines[0], "i-foobar", "")
4493
4494 @inlineCallbacks
4495@@ -141,17 +166,17 @@
4496 self.ec2.describe_security_groups()
4497 self.mocker.result(succeed([security_group]))
4498 self._mock_create_machine_group("1")
4499- self._mock_launch_utils(region="us-east-1")
4500+ self._mock_launch_utils()
4501 self._mock_get_zookeeper_hosts()
4502 self._mock_launch(instance)
4503 self.mocker.replay()
4504
4505 provider = self.get_provider()
4506- provided_machines = yield provider.start_machine({"machine-id": "1"})
4507- self.assertEqual(len(provided_machines), 1)
4508- self.assertTrue(isinstance(provided_machines[0], EC2ProviderMachine))
4509- self.assertEqual(
4510- provided_machines[0].instance_id, instance.instance_id)
4511+ machines = yield provider.start_machine({
4512+ "machine-id": "1", "constraints": self.constraints})
4513+ self.assertEqual(len(machines), 1)
4514+ self.assertTrue(isinstance(machines[0], EC2ProviderMachine))
4515+ self.assertEqual(machines[0].instance_id, instance.instance_id)
4516
4517 @inlineCallbacks
4518 def test_provider_launch_existing_machine_security_group(self):
4519@@ -165,17 +190,17 @@
4520 self._mock_create_group()
4521 self._mock_delete_machine_group("1") # delete existing sg
4522 self._mock_create_machine_group("1") # then recreate
4523- self._mock_launch_utils(region="us-east-1")
4524+ self._mock_launch_utils()
4525 self._mock_get_zookeeper_hosts()
4526 self._mock_launch(instance)
4527 self.mocker.replay()
4528
4529 provider = self.get_provider()
4530- provided_machines = yield provider.start_machine({"machine-id": "1"})
4531- self.assertEqual(len(provided_machines), 1)
4532- self.assertTrue(isinstance(provided_machines[0], EC2ProviderMachine))
4533- self.assertEqual(
4534- provided_machines[0].instance_id, instance.instance_id)
4535+ machines = yield provider.start_machine({
4536+ "machine-id": "1", "constraints": self.constraints})
4537+ self.assertEqual(len(machines), 1)
4538+ self.assertTrue(isinstance(machines[0], EC2ProviderMachine))
4539+ self.assertEqual(machines[0].instance_id, instance.instance_id)
4540
4541 @inlineCallbacks
4542 def test_provider_launch_existing_machine_security_group_is_active(self):
4543@@ -191,13 +216,14 @@
4544 self.mocker.result(succeed([machine_group]))
4545 self._mock_create_group()
4546 self._mock_delete_machine_group_was_deleted("1") # sg is gone!
4547- self._mock_launch_utils(region="us-east-1")
4548+ self._mock_launch_utils()
4549 self._mock_get_zookeeper_hosts()
4550 self.mocker.replay()
4551
4552 provider = self.get_provider()
4553 ex = yield self.assertFailure(
4554- provider.start_machine({"machine-id": "1"}),
4555+ provider.start_machine({
4556+ "machine-id": "1", "constraints": self.constraints}),
4557 ProviderInteractionError)
4558 self.assertEqual(
4559 str(ex),
4560@@ -216,7 +242,8 @@
4561 self.mocker.replay()
4562
4563 provider = self.get_provider()
4564- d = provider.start_machine({"machine-id": "1"})
4565+ d = provider.start_machine({
4566+ "machine-id": "1", "constraints": self.constraints})
4567 self.assertFailure(d, EnvironmentNotFound)
4568 return d
4569
4570@@ -231,65 +258,62 @@
4571 self.mocker.replay()
4572
4573 provider = self.get_provider()
4574- d = provider.start_machine({"machine-id": "1"})
4575+ d = provider.start_machine({
4576+ "machine-id": "1", "constraints": self.constraints})
4577 self.assertFailure(d, EnvironmentNotFound)
4578 return d
4579
4580- def test_launch_options_known_instance_type(self):
4581- self.ec2.describe_security_groups()
4582- self.mocker.result(succeed([]))
4583- self._mock_create_group()
4584- self._mock_create_machine_group("1")
4585- self._mock_launch_utils(region="us-east-1")
4586- self._mock_get_zookeeper_hosts()
4587- self._mock_launch(
4588- self.get_instance("i-foobar"), expect_instance_type="m1.ginormous")
4589- self.mocker.replay()
4590-
4591- provider = self.get_provider()
4592- provider.config["default-instance-type"] = "m1.ginormous"
4593- return provider.start_machine({"machine-id": "1"})
4594-
4595- def _mock_launch_with_ami_params(self, get_ami_kwargs, expect_ami=None):
4596- expect_ami = expect_ami or "ami-default"
4597- self.ec2.describe_security_groups()
4598- self.mocker.result(succeed([]))
4599- self._mock_create_group()
4600- self._mock_create_machine_group("1")
4601- self._mock_launch_utils(ami_name=expect_ami, **get_ami_kwargs)
4602- self._mock_get_zookeeper_hosts()
4603- self._mock_launch(self.get_instance("i-foobar"), expect_ami)
4604-
4605- def test_launch_options_known_ami(self):
4606- self._mock_launch_with_ami_params({}, expect_ami="ami-different")
4607- self.mocker.replay()
4608-
4609- provider = self.get_provider()
4610- provider.config["default-image-id"] = "ami-different"
4611- return provider.start_machine({"machine-id": "1"})
4612-
4613- def test_launch_options_region(self):
4614- self._mock_launch_with_ami_params({"region": "somewhere-else-1"})
4615+ def test_launch_options_from_config_region(self):
4616+ self.ec2.describe_security_groups()
4617+ self.mocker.result(succeed([]))
4618+ self._mock_create_group()
4619+ self._mock_create_machine_group("1")
4620+ self._mock_launch_utils(
4621+ ami_name="ami-regional",
4622+ get_ami_args=("splendid", "amd64", "somewhere-else-1", False))
4623+ self._mock_get_zookeeper_hosts()
4624+ self._mock_launch(self.get_instance("i-foobar"), "ami-regional")
4625 self.mocker.replay()
4626
4627 provider = self.get_provider()
4628 provider.config["region"] = "somewhere-else-1"
4629- return provider.start_machine({"machine-id": "1"})
4630-
4631- def test_launch_options_custom_image_options(self):
4632- self._mock_launch_with_ami_params({
4633- "region": "us-east-1",
4634- "ubuntu_release": "choleric",
4635- "architecture": "quantum86",
4636- "persistent_storage": "maybe",
4637- "daily": "perhaps"})
4638- self.mocker.replay()
4639-
4640- provider = self.get_provider()
4641- constraints = {
4642- "ubuntu_release": "choleric",
4643- "architecture": "quantum86",
4644- "persistent_storage": "maybe",
4645- "daily": "perhaps"}
4646- return provider.start_machine({"machine-id": "1",
4647- "constraints": constraints})
4648+ return provider.start_machine({
4649+ "machine-id": "1", "constraints": self.constraints})
4650+
4651+ def test_launch_options_ec2_constraints(self):
4652+ self.ec2.describe_security_groups()
4653+ self.mocker.result(succeed([]))
4654+ self._mock_create_group()
4655+ self._mock_create_machine_group("1")
4656+ self._mock_launch_utils(
4657+ ami_name="ami-fancy-cluster",
4658+ get_ami_args=("vast", "amd64", "us-east-1", True))
4659+ self._mock_get_zookeeper_hosts()
4660+ self._mock_launch(
4661+ self.get_instance("i-foobar"), "ami-fancy-cluster",
4662+ expect_instance_type="cc2.8xlarge",
4663+ expect_availability_zone="us-east-1b")
4664+ self.mocker.replay()
4665+
4666+ provider = self.get_provider()
4667+ return provider.start_machine({
4668+ "machine-id": "1",
4669+ "constraints": self.ec2_constraints})
4670+
4671+ def test_launch_options_generic_constraints(self):
4672+ self.ec2.describe_security_groups()
4673+ self.mocker.result(succeed([]))
4674+ self._mock_create_group()
4675+ self._mock_create_machine_group("1")
4676+ self._mock_launch_utils(
4677+ get_ami_args=("dribbly", "amd64", "us-east-1", False))
4678+ self._mock_get_zookeeper_hosts()
4679+ self._mock_launch(
4680+ self.get_instance("i-foobar"), "ami-default",
4681+ expect_instance_type="c1.xlarge")
4682+ self.mocker.replay()
4683+
4684+ provider = self.get_provider()
4685+ return provider.start_machine({
4686+ "machine-id": "1",
4687+ "constraints": self.gen_constraints})
4688
4689=== modified file 'juju/providers/ec2/tests/test_provider.py'
4690--- juju/providers/ec2/tests/test_provider.py 2011-09-23 19:47:01 +0000
4691+++ juju/providers/ec2/tests/test_provider.py 2012-03-30 16:56:32 +0000
4692@@ -1,7 +1,10 @@
4693+from twisted.internet.defer import inlineCallbacks
4694+
4695+from juju.environment.errors import EnvironmentsConfigError
4696+from juju.errors import ConstraintError
4697 from juju.lib.testing import TestCase
4698 from juju.providers.ec2.files import FileStorage
4699 from juju.providers.ec2 import MachineProvider
4700-from juju.environment.errors import EnvironmentsConfigError
4701
4702 from .common import EC2TestMixin
4703
4704@@ -117,6 +120,91 @@
4705 serialized = provider.get_serialization_data()
4706 self.assertEqual(config, serialized)
4707
4708+ def test_get_legacy_config_keys(self):
4709+ provider = MachineProvider(self.env_name, {
4710+ # Note: these keys *will* at some stage be considered legacy keys;
4711+ # they're included here to make sure the tests are updated when we
4712+ # make that change.
4713+ "default-series": "foo", "placement": "bar"})
4714+ self.assertEquals(provider.get_legacy_config_keys(), set())
4715+
4716+ # These keys are not valid on Amazon EC2...
4717+ provider.config.update({
4718+ "default-instance-type": "baz", "default-image-id": "qux"})
4719+ self.assertEquals(provider.get_legacy_config_keys(), set((
4720+ "default-instance-type", "default-image-id")))
4721+
4722+ # ...but they still are when using private clouds.
4723+ provider.config.update({"ec2-uri": "anything"})
4724+ self.assertEquals(provider.get_legacy_config_keys(), set())
4725+
4726+
4727+class ProviderConstraintsTestCase(TestCase):
4728+
4729+ def constraint_set(self):
4730+ provider = MachineProvider("some-ec2-env", {})
4731+ return provider.get_constraint_set()
4732+
4733+ @inlineCallbacks
4734+ def assert_invalid(self, msg, *strs):
4735+ cs = yield self.constraint_set()
4736+ e = self.assertRaises(ConstraintError, cs.parse, strs)
4737+ self.assertEquals(str(e), msg)
4738+
4739+ @inlineCallbacks
4740+ def test_constraints(self):
4741+ cs = yield self.constraint_set()
4742+ self.assertEquals(cs.parse([]), {
4743+ "provider-type": "ec2",
4744+ "ubuntu-series": None,
4745+ "instance-type": None,
4746+ "ec2-zone": None,
4747+ "arch": "amd64",
4748+ "cpu": 1.0,
4749+ "mem": 512.0})
4750+ self.assertEquals(cs.parse(["ec2-zone=X", "instance-type=m1.small"]), {
4751+ "provider-type": "ec2",
4752+ "ubuntu-series": None,
4753+ "instance-type": "m1.small",
4754+ "ec2-zone": "x",
4755+ "arch": "amd64",
4756+ "cpu": None,
4757+ "mem": None})
4758+
4759+ yield self.assert_invalid(
4760+ "Bad 'ec2-zone' constraint '7': expected single ascii letter",
4761+ "ec2-zone=7")
4762+ yield self.assert_invalid(
4763+ "Bad 'ec2-zone' constraint 'blob': expected single ascii letter",
4764+ "ec2-zone=blob")
4765+ yield self.assert_invalid(
4766+ "Bad 'instance-type' constraint 'qq1.moar': unknown instance type",
4767+ "instance-type=qq1.moar")
4768+ yield self.assert_invalid(
4769+ "Ambiguous constraints: 'cpu' overlaps with 'instance-type'",
4770+ "instance-type=m1.small", "cpu=1")
4771+ yield self.assert_invalid(
4772+ "Ambiguous constraints: 'instance-type' overlaps with 'mem'",
4773+ "instance-type=m1.small", "mem=2G")
4774+
4775+ @inlineCallbacks
4776+ def test_satisfy_zone_constraint(self):
4777+ cs = yield self.constraint_set()
4778+ a = cs.parse(["ec2-zone=a"]).with_series("series")
4779+ b = cs.parse(["ec2-zone=b"]).with_series("series")
4780+ self.assertTrue(a.can_satisfy(a))
4781+ self.assertTrue(b.can_satisfy(b))
4782+ self.assertFalse(a.can_satisfy(b))
4783+ self.assertFalse(b.can_satisfy(a))
4784+
4785+ @inlineCallbacks
4786+ def test_non_amazon_constraints(self):
4787+ provider = MachineProvider("some-non-ec2-env", {"ec2-uri": "blah"})
4788+ cs = yield provider.get_constraint_set()
4789+ self.assertEquals(cs.parse([]), {
4790+ "provider-type": "ec2",
4791+ "ubuntu-series": None})
4792+
4793
4794 class FailCreateTest(TestCase):
4795
4796
4797=== modified file 'juju/providers/ec2/tests/test_utils.py'
4798--- juju/providers/ec2/tests/test_utils.py 2012-03-26 17:13:53 +0000
4799+++ juju/providers/ec2/tests/test_utils.py 2012-03-30 16:56:32 +0000
4800@@ -1,12 +1,16 @@
4801 import inspect
4802 import os
4803
4804-from twisted.internet.defer import fail, succeed
4805+from twisted.internet.defer import fail, succeed, inlineCallbacks
4806 from twisted.web.error import Error
4807
4808+from juju.errors import ProviderError
4809 from juju.lib.testing import TestCase
4810 from juju.providers import ec2
4811-from juju.providers.ec2.utils import get_current_ami, get_image_id
4812+from juju.providers.ec2.utils import (
4813+ get_current_ami, get_instance_type, get_machine_spec)
4814+
4815+from .common import get_constraints
4816
4817
4818 IMAGE_URI_TEMPLATE = "\
4819@@ -26,7 +30,7 @@
4820 page(IMAGE_URI_TEMPLATE % "nutty")
4821 self.mocker.result(fail(Error("404")))
4822 self.mocker.replay()
4823- d = get_current_ami(ubuntu_release="nutty")
4824+ d = get_current_ami("nutty", "i386", "us-east-1", False)
4825 self.failUnlessFailure(d, LookupError)
4826 return d
4827
4828@@ -39,7 +43,7 @@
4829 page(IMAGE_URI_TEMPLATE % "lucid")
4830 self.mocker.result(succeed(""))
4831 self.mocker.replay()
4832- d = get_current_ami(ubuntu_release="lucid")
4833+ d = get_current_ami("lucid", "i386", "us-east-1", False)
4834 self.failUnlessFailure(d, LookupError)
4835 return d
4836
4837@@ -51,12 +55,20 @@
4838 open(os.path.join(IMAGE_DATA_DIR, "lucid.txt")).read()))
4839
4840 self.mocker.replay()
4841- d = get_current_ami(ubuntu_release="lucid")
4842-
4843- def verify_result(result):
4844- self.assertEqual(result, "ami-714ba518")
4845-
4846- d.addCallback(verify_result)
4847+ d = get_current_ami("lucid", "i386", "us-east-1", False)
4848+ d.addCallback(self.assertEquals, "ami-714ba518")
4849+ return d
4850+
4851+ def test_current_ami_by_arch(self):
4852+ """The current server machine image can be retrieved by arch."""
4853+ page = self.mocker.replace("twisted.web.client.getPage")
4854+ page(IMAGE_URI_TEMPLATE % "lucid")
4855+ self.mocker.result(
4856+ succeed(open(
4857+ os.path.join(IMAGE_DATA_DIR, "lucid.txt")).read()))
4858+ self.mocker.replay()
4859+ d = get_current_ami("lucid", "amd64", "us-east-1", False)
4860+ d.addCallback(self.assertEquals, "ami-4b4ba522")
4861 return d
4862
4863 def test_current_ami_by_region(self):
4864@@ -67,75 +79,166 @@
4865 succeed(open(
4866 os.path.join(IMAGE_DATA_DIR, "lucid.txt")).read()))
4867 self.mocker.replay()
4868- d = get_current_ami(ubuntu_release="lucid", region="us-west-1")
4869-
4870- def verify_result(result):
4871- self.assertEqual(result, "ami-cb97c68e")
4872-
4873- d.addCallback(verify_result)
4874- return d
4875-
4876- def test_current_ami_non_ebs(self):
4877- """
4878- The get_current_ami function accepts several filtering parameters
4879- to guide image selection.
4880- """
4881+ d = get_current_ami("lucid", "i386", "us-west-1", False)
4882+ d.addCallback(self.assertEquals, "ami-cb97c68e")
4883+ return d
4884+
4885+ def test_current_ami_with_virtualisation_info(self):
4886+ """The current server machine image can be retrieved by arch."""
4887+ page = self.mocker.replace("twisted.web.client.getPage")
4888+ page(IMAGE_URI_TEMPLATE % "natty")
4889+ self.mocker.result(
4890+ succeed(open(
4891+ os.path.join(IMAGE_DATA_DIR, "natty.txt")).read()))
4892+ self.mocker.replay()
4893+ d = get_current_ami("natty", "amd64", "us-east-1", True)
4894+ d.addCallback(self.assertEquals, "ami-1cad5275")
4895+ return d
4896+
4897+ def test_hvm_request_on_old_series(self):
4898 page = self.mocker.replace("twisted.web.client.getPage")
4899 page(IMAGE_URI_TEMPLATE % "lucid")
4900- self.mocker.result(succeed(
4901- open(os.path.join(IMAGE_DATA_DIR, "lucid.txt")).read()))
4902+ self.mocker.result(
4903+ succeed(open(
4904+ os.path.join(IMAGE_DATA_DIR, "lucid.txt")).read()))
4905 self.mocker.replay()
4906- d = get_current_ami(ubuntu_release="lucid", persistent_storage=False)
4907-
4908- def verify_result(result):
4909- self.assertEqual(result, "ami-2d4aa444")
4910-
4911- d.addCallback(verify_result)
4912+ d = get_current_ami("lucid", "amd64", "us-east-1", True)
4913+ self.failUnlessFailure(d, LookupError)
4914 return d
4915
4916
4917 class GetImageIdTest(TestCase):
4918
4919- def test_default_image_id(self):
4920- d = get_image_id({"default-image-id": "ami-burble"}, {})
4921- d.addCallback(self.assertEquals, "ami-burble")
4922- return d
4923-
4924- def test_no_constraints(self):
4925+ @inlineCallbacks
4926+ def assert_image_id(self, config, constraints, series, arch, region, hvm,
4927+ instance_type):
4928 get_current_ami_m = self.mocker.replace(get_current_ami)
4929- get_current_ami_m(region="us-east-1")
4930+ get_current_ami_m(series, arch, region, hvm)
4931 self.mocker.result(succeed("ami-giggle"))
4932 self.mocker.replay()
4933
4934- d = get_image_id({}, {})
4935- d.addCallback(self.assertEquals, "ami-giggle")
4936- return d
4937-
4938- def test_default_series(self):
4939- get_current_ami_m = self.mocker.replace(get_current_ami)
4940- get_current_ami_m(region="us-east-1", ubuntu_release="puissant")
4941- self.mocker.result(succeed("ami-pickle"))
4942- self.mocker.replay()
4943-
4944- d = get_image_id({"default-series": "puissant"}, {})
4945- d.addCallback(self.assertEquals, "ami-pickle")
4946- return d
4947-
4948- def test_uses_constraints(self):
4949- get_current_ami_m = self.mocker.replace(get_current_ami)
4950- get_current_ami_m(ubuntu_release="serendipitous", architecture="x512",
4951- daily=False, persistent_storage=True,
4952- region="blah-north-6")
4953- self.mocker.result(succeed("ami-tinkle"))
4954- self.mocker.replay()
4955-
4956- constraints = {
4957- "architecture": "x512",
4958- "ubuntu_release": "serendipitous",
4959- "persistent_storage": True,
4960- "daily": False}
4961- d = get_image_id(
4962- {"region": "blah-north-6", "default-series": "overridden"},
4963- constraints)
4964- d.addCallback(self.assertEquals, "ami-tinkle")
4965- return d
4966+ spec = yield get_machine_spec(config, constraints)
4967+ self.assertEquals(spec.image_id, "ami-giggle")
4968+ self.assertEquals(spec.instance_type, instance_type)
4969+
4970+ @inlineCallbacks
4971+ def test_empty_config(self):
4972+ constraints = yield get_constraints(["arch=i386"])
4973+ yield self.assert_image_id(
4974+ {}, constraints,
4975+ "splendid", "i386", "us-east-1", False,
4976+ "m1.small")
4977+
4978+ @inlineCallbacks
4979+ def test_series_from_config(self):
4980+ config = {"default-series": "puissant"}
4981+ constraints = yield get_constraints(["arch=i386"], None)
4982+ yield self.assert_image_id(
4983+ config, constraints,
4984+ "puissant", "i386", "us-east-1", False,
4985+ "m1.small")
4986+
4987+ @inlineCallbacks
4988+ def test_arch_from_instance_constraint(self):
4989+ constraints = yield get_constraints([
4990+ "instance-type=m1.large", "arch=any"])
4991+ yield self.assert_image_id(
4992+ {}, constraints,
4993+ "splendid", "amd64", "us-east-1", False,
4994+ "m1.large")
4995+
4996+ @inlineCallbacks
4997+ def test_arch_from_nothing(self):
4998+ constraints = yield get_constraints([
4999+ "arch=any", "cpu=any", "mem=any", "instance-type=any"])
5000+ yield self.assert_image_id(
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to status/vote changes: