Merge lp:~fwereade/pyjuju/shadow-trunk-1204 into lp:pyjuju
- shadow-trunk-1204
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Engineering | Pending | ||
Review via email: mp+100195@code.launchpad.net |
Commit message
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.
William Reade (fwereade) wrote : | # |
William Reade (fwereade) wrote : | # |
Reviewers: mp+100195_
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:/
(do not edit description out of merge proposal)
Please review this at https:/
Affected files:
A [revision details]
A docs/source/
M juju/agents/
M juju/agents/
M juju/agents/
M juju/agents/
M juju/control/
M juju/control/
M juju/control/
A juju/control/
M juju/control/
M juju/control/
M juju/control/
A juju/control/
M juju/control/
M juju/control/
M juju/control/
M juju/control/
M juju/control/
M juju/control/
A juju/control/
M juju/control/
M juju/control/
M juju/control/
M juju/control/
M juju/control/
M juju/control/
M juju/control/
M juju/control/
M juju/control/
M juju/control/
M juju/control/
M juju/control/
M juju/control/
M juju/control/
M juju/control/
M juju/control/
M juju/environmen
M juju/environmen
M juju/hooks/
M juju/machine/
M juju/machine/
M juju/machine/
M juju/providers/
M juju/providers/
M juju/providers/
M juju/providers/
M juju/providers/
M juju/providers/
M juju/providers/
M juju/providers/
M juju/providers/
M juju/providers/
M juju/providers/
M juju/providers/
M juju/providers/
M juju/providers/
M juju/providers/
M juju/providers/
M juju/providers/
M juju/providers/
M juju/providers/
M juju/providers/
M juju/providers/
M juju/providers/
M juju...
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.
Preview Diff
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( |
Please take a look.