Merge lp:~jimbaker/pyjuju/expose-provider-ec2 into lp:pyjuju

Proposed by Jim Baker
Status: Merged
Approved by: Gustavo Niemeyer
Approved revision: 330
Merged at revision: 309
Proposed branch: lp:~jimbaker/pyjuju/expose-provider-ec2
Merge into: lp:pyjuju
Prerequisite: lp:~jimbaker/pyjuju/expose-provision-machines-reexpose
Diff against target: 1247 lines (+566/-203)
23 files modified
ensemble/agents/provision.py (+8/-5)
ensemble/agents/tests/test_provision.py (+1/-1)
ensemble/agents/tests/test_unit.py (+1/-2)
ensemble/agents/unit.py (+2/-1)
ensemble/machine/__init__.py (+7/-6)
ensemble/machine/tests/test_machine.py (+0/-2)
ensemble/providers/common/launch.py (+22/-15)
ensemble/providers/common/tests/test_launch.py (+2/-3)
ensemble/providers/dummy.py (+3/-3)
ensemble/providers/ec2/__init__.py (+14/-0)
ensemble/providers/ec2/accessor.py (+19/-14)
ensemble/providers/ec2/launch.py (+70/-78)
ensemble/providers/ec2/securitygroup.py (+90/-0)
ensemble/providers/ec2/tests/common.py (+42/-29)
ensemble/providers/ec2/tests/test_accessor.py (+24/-2)
ensemble/providers/ec2/tests/test_bootstrap.py (+12/-6)
ensemble/providers/ec2/tests/test_files.py (+1/-1)
ensemble/providers/ec2/tests/test_launch.py (+106/-25)
ensemble/providers/ec2/tests/test_securitygroup.py (+129/-0)
ensemble/providers/orchestra/launch.py (+1/-1)
ensemble/providers/orchestra/tests/common.py (+0/-1)
ensemble/providers/tests/test_dummy.py (+9/-8)
examples/wordpress/hooks/db-relation-changed (+3/-0)
To merge this branch: bzr merge lp:~jimbaker/pyjuju/expose-provider-ec2
Reviewer Review Type Date Requested Status
Gustavo Niemeyer Approve
William Reade (community) Approve
Review via email: mp+68478@code.launchpad.net

Description of the change

This branch implements the following:

 * When launching a machine, it ensures the creation of an overall
   Ensemble security group for all machines, with only port 22/tcp
   open, named "ensemble-ENVIRONMENT", along with a per-machine
   security group named "ensemble-ENVIRONMENT-MACHINE_ID", with no
   ports open.

 * The EC2 provider implementation of open_port, close_port, and
   get_opened_ports corresponds to EC2 operations of
   authorize_security_group, revoke_security_group, and
   describe_security_groups (using txaws). For the latter, the
   IPPermissions model is parsed to return a set of (port, proto)
   pairs for a given machine.

Lastly, this branch also adds this formula change to the wordpress
db-relation-changed hook. Originally this was going to be in a
separate branch, but this change makes it possible to really test!

  # Make it publicly visible, once the wordpress service is exposed
  open-port 80/tcp

Given the above, the branch can be readily tested by deploying the
wordpress and mysql example formulas as usual, then doing:

  ensemble expose wordpress

This will result in debug log output like so:

  2011-07-19 16:24:58,498 provision:ec2: ensemble.agents.provision DEBUG: Service 'wordpress' is exposed
  2011-07-19 16:24:58,840 provision:ec2: ensemble.ec2 DEBUG: Opened 80/tcp on provider machine 'i-4bc10a2a'

Further operations of unexpose/expose are also supported, given the
dependence on lp:~jimbaker/ensemble/expose-provision-machines-reexpose

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

* Tiny, irrelevant note: rather than directly creating an EC2ProviderMachine, there's now a machine_from_instance function in ec2.machine which is a touch more convenient.

* Significant issue: I don't like having both 'machine' and 'machine_id' parameters: if there isn't currently a way to determine machine_id given a machine, I think it would be worth adding one and using it, to avoid cluttering up the Provider interface.

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

[1]

On an overlook, this feels nice. We just debated online about one detail that
would be good to get handled before this is merged: there's no reason to have
that logic inside classes. Let's talk further online if you want to debate
more on this.

review: Needs Fixing
Revision history for this message
Jim Baker (jimbaker) wrote :

On Tue, Jul 26, 2011 at 9:19 AM, William Reade
<email address hidden>wrote:

> Review: Needs Fixing
> * Tiny, irrelevant note: rather than directly creating an
> EC2ProviderMachine, there's now a machine_from_instance function in
> ec2.machine which is a touch more convenient.
>

Now using

>
> * Significant issue: I don't like having both 'machine' and 'machine_id'
> parameters: if there isn't currently a way to determine machine_id given a
> machine, I think it would be worth adding one and using it, to avoid
> cluttering up the Provider interface.
>

Changed such that ProviderMachine now takes an optional machine_id so this
can be passed around. In general, this field needs to be assigned after the
fact (EC2 doesn't know these machine IDs from our topology), and this is
what is done in the provisioning agent. But it does provide a central place
for this information instead of having to pass around two related pieces.

Revision history for this message
Jim Baker (jimbaker) wrote :

On Tue, Jul 26, 2011 at 9:51 AM, Gustavo Niemeyer <email address hidden>wrote:

> Review: Needs Fixing
> [1]
>
> On an overlook, this feels nice. We just debated online about one detail
> that
> would be good to get handled before this is merged: there's no reason to
> have
> that logic inside classes. Let's talk further online if you want to debate
> more on this.
>

Removed class wrappers in favor of just functions.

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

> Changed such that ProviderMachine now takes an optional machine_id so this
> can be passed around. In general, this field needs to be assigned after the
> fact (EC2 doesn't know these machine IDs from our topology), and this is
> what is done in the provisioning agent. But it does provide a central place
> for this information instead of having to pass around two related pieces.

Cool. One day, it would be good to ensure we do always have machine_id set on ProviderMachine instances, but no clean way to do so springs to mind now, so I don't think it blocks anything.

Everything else looks sensible to me, +1. (And I definitely like the operations as functions :))

review: Approve
Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :
Download full text (4.2 KiB)

Here are a few additional points Jim. Nothing huge, I think:

[1]

         self._watched_services = {}
+ self._watched_machines = set()

Bogus line. It's duplicated.

[2]

             machine = yield self.provider.get_machine(instance_id)
      machine.machine_id = machine_id
      (...)
             current_ports = yield self.provider.get_opened_ports(machine)

We really need to fix this. You ask the provider for a machine, it
gives you the machine with a field it has no idea about how to
retrieve, you stuff an id into that object, and then give it _back_
to the provider because it will need it.

Result:

- The provider is the one responsible for its machines, but can't
  create one with the needed data.
- The provider expects a machine_id in arbitrary moments, and doesn't
  tell when.
- Arbitrary locations set the id, because they magically know the dance.

No one but you understand that. We need a more consistent API to
handle this.

If get_open_ports and friends need unique identifier, let's provide it
explicitly. This should be the external id, not the internal id used
within zookeeper.

Unless the providers can set the machine_id attribute, please take it
off from the machine.

[3]

+ def __init__(self, instance_id, dns_name=None,
+ private_dns_name=None, launch_time=None, machine_id=None):

Per above, please drop this.

[4]

- launch_deferred = self.get_machine_variables(machine_data)
- launch_deferred.addCallback(self.start_machine)
+ machine_variables = yield self.get_machine_variables(machine_data)
+ provider_machines = yield self.start_machine(
+ machine_variables, machine_data)

The idea of having a data bag like machine_data was a bad one. Now we
have logic all around which depends on arbitrary fields inside it, and
do not say so. Let's not increase the problem.

If start_machine needs the machine_id, let's just provide the machine_id
rather than a bag.

[5]

+ return open_provider_port(self, machine, port, protocol)

Much better, thanks!

[6]

+ except EC2Error:
+ # AWS may return an error when the instance is not found
+ # (but presumably eventually will be), eg,
+ # txaws.ec2.exception.EC2Error: Error Message:
+ # The instance ID 'i-afe113ce' does not exist
+ raise ProviderInteractionError(
+ "Machine (id: %r) was not found" % provider_id)

This looks too generic. What other errors can EC2Error represent?

Also, should we perhaps try again a couple of times before giving
up if the provider_id was indeed not found, to compensate for the
boredom of eventual consistency?

For the latter, free to just add a comment in case you don't want
to do now. The former looks like an issue we'll want to address
before merging.

[7]

- # TODO: For now authorize external traffic to the instance,
- # except for the ZooKeeper port. The expose functionality

Good to see that stuff going away.

[8]

+ if ensemble_machine_group in group_ids:
+ yield self._provider.ec2.delete_security_group(
+ ensemble_machine_group)

Has this logic ...

Read more...

review: Needs Fixing
Revision history for this message
Jim Baker (jimbaker) wrote :
Download full text (5.7 KiB)

On Fri, Jul 29, 2011 at 3:09 PM, Gustavo Niemeyer <email address hidden>wrote:

> Review: Needs Fixing
> Here are a few additional points Jim. Nothing huge, I think:
>
>
> [1]
>
> self._watched_services = {}
> + self._watched_machines = set()
>
>
> Bogus line. It's duplicated.
>

Fixed

>
>
> [2]
>
> machine = yield self.provider.get_machine(instance_id)
> machine.machine_id = machine_id
> (...)
> current_ports = yield self.provider.get_opened_ports(machine)
>
> We really need to fix this. You ask the provider for a machine, it
> gives you the machine with a field it has no idea about how to
> retrieve, you stuff an id into that object, and then give it _back_
> to the provider because it will need it.
>
> Result:
>
> - The provider is the one responsible for its machines, but can't
> create one with the needed data.
> - The provider expects a machine_id in arbitrary moments, and doesn't
> tell when.
> - Arbitrary locations set the id, because they magically know the dance.
>
> No one but you understand that. We need a more consistent API to
> handle this.
>
> If get_open_ports and friends need unique identifier, let's provide it
> explicitly. This should be the external id, not the internal id used
> within zookeeper.
>
> Unless the providers can set the machine_id attribute, please take it
> off from the machine.
>

Changed accordingly to a model where machine_id is passed explicitly.
Confirmed with William that this is OK.

>
>
> [3]
>
> + def __init__(self, instance_id, dns_name=None,
> + private_dns_name=None, launch_time=None,
> machine_id=None):
>
> Per above, please drop this.
>

Reverted

>
> [4]
>
> - launch_deferred = self.get_machine_variables(machine_data)
> - launch_deferred.addCallback(self.start_machine)
> + machine_variables = yield self.get_machine_variables(machine_data)
> + provider_machines = yield self.start_machine(
> + machine_variables, machine_data)
>
> The idea of having a data bag like machine_data was a bad one. Now we
> have logic all around which depends on arbitrary fields inside it, and
> do not say so. Let's not increase the problem.
>
> If start_machine needs the machine_id, let's just provide the machine_id
> rather than a bag.
>

Changed so that start_machine takes machine_id. Note there are two
start_machine functions, which is probably not ideal. I only refactored this
one.

>
> [5]
>
> + return open_provider_port(self, machine, port, protocol)
>
> Much better, thanks!
>

Cool!

>
> [6]
>
> + except EC2Error:
> + # AWS may return an error when the instance is not found
> + # (but presumably eventually will be), eg,
> + # txaws.ec2.exception.EC2Error: Error Message:
> + # The instance ID 'i-afe113ce' does not exist
> + raise ProviderInteractionError(
> + "Machine (id: %r) was not found" % provider_id)
>
> This looks too generic. What other errors can EC2Error represent?
>

Well, I have certainly seen them, but I don't know of a complete list. I
will look. I have made this and the similar u...

Read more...

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

Looks good, +1 with a few extra comments:

[13]

+ raise ProviderInteractionError(
+ "Got EC2 error (id %r: %s) when looking up machine" % (
+ provider_id, ex))

Please reword it like this:

    "EC2 error when looking up instance %s: %s" % (provider_id, e)

[14]

+ raise ProviderInteractionError(
+ "Machine (id: %r) was not found" % provider_id)

Please reword as:

"EC2 instance %s was not found"

[15]

+ except EC2Error:
+ log.debug("Ignoring machine group %s that just disappeared",
+ ensemble_machine_group)
+ pass # Security group for machine is no longer around, ignore

The pass + comment may be dropped as it's redundant with the previous line.

[16]

+# TODO These security group functions do not handle the eventual
+# consistency seen with EC2. This is likely not the right place to
+# address this issue. The provisioning agent is more likely the place
+# to do this.

This would mean we're introducing eventual consistency within our API,
which sounds really bad to handle. The eventual consistency aspect
should be handled as closely to the EC2 API as possible, rather than
across all of the library.

Please adapt the comment to reflect this.

[17]

+ "Got EC2 error (id %r: %s) when attempting to open %s/%s" % (
+ machine.instance_id, ex, port, protocol))

"EC2 error when attempting to open %s/%s on machine %s: %s"

[18]

Similar as the above for the other messages.

[19]

+ for ip_permission in security_groups[0].allowed_ips:
+ if ip_permission.cidr_ip != "0.0.0.0/0":

Is it possible for this list to be empty? In which circumstances?
Do we have to handle it to avoid breaking the agent entirely?

[20]

+ if from_port == to_port:
+ # Ignore ranges of ports, since they are set outside of
+ # Ensemble (at this time at least)

This comment seems reversed. This branch of logic is the one that
does _not_ ignore. It should probably be reworded in the positive
side to be more clear ("Only take ...").

[21]

It would be awesome to take the chance we have Mark and Clint here
and take them through a whole run of that branch before merging,
both to share the knowledge and to help ensuring the latest changes
work fine.

review: Approve
331. By Jim Baker

Added logging on creating machine security group

332. By Jim Baker

Revised error messages

333. By Jim Baker

Add testing around a security group is still active and cannot be deleted

334. By Jim Baker

PEP8 & PyFlakes

335. By Jim Baker

Merged upstream & resolve conflicts

336. By Jim Baker

PEP8/PyFlakes/leftover text conflict

Revision history for this message
Jim Baker (jimbaker) wrote :

On Tue, Aug 9, 2011 at 11:41 AM, Gustavo Niemeyer <email address hidden>wrote:

> Review: Approve
> Looks good, +1 with a few extra comments:
>
> [13]
>
> + raise ProviderInteractionError(
> + "Got EC2 error (id %r: %s) when looking up machine" % (
> + provider_id, ex))
>
> Please reword it like this:
>
> "EC2 error when looking up instance %s: %s" % (provider_id, e)
>

Done

>
>
> [14]
>
> + raise ProviderInteractionError(
> + "Machine (id: %r) was not found" % provider_id)
>
> Please reword as:
>
> "EC2 instance %s was not found"
>

Done

>
> [15]
>
> + except EC2Error:
> + log.debug("Ignoring machine group %s that just
> disappeared",
> + ensemble_machine_group)
> + pass # Security group for machine is no longer around,
> ignore
>
> The pass + comment may be dropped as it's redundant with the previous line.
>

Removed pass, but per conversation in sprint room, the logic was wrong, so
it nows raises this as ProviderInteractionError. This is because this is
almost certainly because of a security group that cannot be deleted because
it's used by another instance (generally being shut down). This will be
addressed in another branch that does security group cleanup on ensemble
shutdown.

>
> [16]
>
> +# TODO These security group functions do not handle the eventual
> +# consistency seen with EC2. This is likely not the right place to
> +# address this issue. The provisioning agent is more likely the place
> +# to do this.
>
> This would mean we're introducing eventual consistency within our API,
> which sounds really bad to handle. The eventual consistency aspect
> should be handled as closely to the EC2 API as possible, rather than
> across all of the library.
>
> Please adapt the comment to reflect this.
>

Fixed

>
> [17]
>
> + "Got EC2 error (id %r: %s) when attempting to open %s/%s" % (
> + machine.instance_id, ex, port, protocol))
>
> "EC2 error when attempting to open %s/%s on machine %s: %s"
>

Fixed

>
> [18]
>
> Similar as the above for the other messages.
>

Fixed

>
> [19]
>
> + for ip_permission in security_groups[0].allowed_ips:
> + if ip_permission.cidr_ip != "0.0.0.0/0":
>
> Is it possible for this list to be empty? In which circumstances?
> Do we have to handle it to avoid breaking the agent entirely?
>

security_groups will not be empty

>
> [20]
>
> + if from_port == to_port:
> + # Ignore ranges of ports, since they are set outside of
> + # Ensemble (at this time at least)
>
> This comment seems reversed. This branch of logic is the one that
> does _not_ ignore. It should probably be reworded in the positive
> side to be more clear ("Only take ...").
>

Changed

>
> [21]
>
> It would be awesome to take the chance we have Mark and Clint here
> and take them through a whole run of that branch before merging,
> both to share the knowledge and to help ensuring the latest changes
> work fine.
>

Mark and Jorge both ran this successfully (a reasonable sub for Clint).

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'ensemble/agents/provision.py'
2--- ensemble/agents/provision.py 2011-08-10 18:22:54 +0000
3+++ ensemble/agents/provision.py 2011-08-10 18:22:55 +0000
4@@ -317,8 +317,10 @@
5 yield self.open_close_ports(unit_state)
6
7 if not exposed:
8+ log.debug("Service %r is unexposed", service_name)
9 self._watched_services[service_name] = NotExposed
10 else:
11+ log.debug("Service %r is exposed", service_name)
12 self._watched_services[service_name] = set()
13 yield self._setup_service_unit_watch(service_state)
14
15@@ -332,7 +334,7 @@
16 @inlineCallbacks
17 def cb_check_service_units(old_service_units, new_service_units):
18 watched_units = self._watched_services.get(
19- service_state.service_name)
20+ service_state.service_name, NotExposed)
21 if not self._running or watched_units is NotExposed:
22 raise StopWatcher()
23
24@@ -447,14 +449,15 @@
25 for port in ports:
26 policy_ports.add(
27 (port["port"], port["proto"]))
28-
29- current_ports = yield self.provider.get_opened_ports(machine)
30+ current_ports = yield self.provider.get_opened_ports(
31+ machine, machine_id)
32 to_open = policy_ports - current_ports
33 to_close = current_ports - policy_ports
34 for port, proto in to_open:
35- yield self.provider.open_port(machine, port, proto)
36+ yield self.provider.open_port(machine, machine_id, port, proto)
37 for port, proto in to_close:
38- yield self.provider.close_port(machine, port, proto)
39+ yield self.provider.close_port(
40+ machine, machine_id, port, proto)
41 except ProviderInteractionError:
42 log.info("No provisioned machine for machine %r", machine_id)
43 finally:
44
45=== modified file 'ensemble/agents/tests/test_provision.py'
46--- ensemble/agents/tests/test_provision.py 2011-08-10 18:22:54 +0000
47+++ ensemble/agents/tests/test_provision.py 2011-08-10 18:22:55 +0000
48@@ -652,7 +652,7 @@
49 instance_id = yield machine.get_instance_id()
50 machine_provider = yield self.agent.provider.get_machine(instance_id)
51 provider_ports = yield self.agent.provider.get_opened_ports(
52- machine_provider)
53+ machine_provider, machine.id)
54 returnValue(provider_ports)
55
56 def test_open_close_ports_on_machine(self):
57
58=== modified file 'ensemble/agents/tests/test_unit.py'
59--- ensemble/agents/tests/test_unit.py 2011-07-13 09:04:36 +0000
60+++ ensemble/agents/tests/test_unit.py 2011-08-10 18:22:55 +0000
61@@ -265,8 +265,7 @@
62 "app-relation-changed", executor=self.agent.executor)
63
64 # trigger the hook that will read service options
65- wordpress_states = yield self.add_opposite_service_unit(
66- self.states)
67+ yield self.add_opposite_service_unit(self.states)
68
69 # Verify the hook has executed.
70 yield hook_complete
71
72=== modified file 'ensemble/agents/unit.py'
73--- ensemble/agents/unit.py 2011-07-21 01:15:02 +0000
74+++ ensemble/agents/unit.py 2011-08-10 18:22:55 +0000
75@@ -188,7 +188,8 @@
76 log.debug("Configuration Changed")
77
78 if current_state != "started":
79- log.debug("Configuration updated on service in a non-started state")
80+ log.debug(
81+ "Configuration updated on service in a non-started state")
82 returnValue(None)
83
84 yield self.workflow.fire_transition("reconfigure")
85
86=== modified file 'ensemble/machine/__init__.py'
87--- ensemble/machine/__init__.py 2011-07-13 22:08:49 +0000
88+++ ensemble/machine/__init__.py 2011-08-10 18:22:55 +0000
89@@ -2,14 +2,15 @@
90
91 class ProviderMachine(object):
92 """
93- Representative of a machine resource created by a C{MachineProvider}. The
94- object is typically annotated by the machine provider, such that the
95- provider can perform subsequent actions upon it, using the additional
96- metadata for identification, without leaking these details to consumers of
97- the C{MachineProvider} api.
98+ Representative of a machine resource created by a
99+ :class:`MachineProvider`. The object is typically annotated by the
100+ machine provider, such that the provider can perform subsequent
101+ actions upon it, using the additional metadata for identification,
102+ without leaking these details to consumers of the
103+ :class:`MachineProvider` api.
104 """
105
106- def __init__(self, instance_id, dns_name=None,
107+ def __init__(self, instance_id, dns_name=None,
108 private_dns_name=None, launch_time=None):
109 self.instance_id = instance_id
110 # ideally this would be ip_address, but txaws doesn't expose it.
111
112=== modified file 'ensemble/machine/tests/test_machine.py'
113--- ensemble/machine/tests/test_machine.py 2011-01-26 23:39:35 +0000
114+++ ensemble/machine/tests/test_machine.py 2011-08-10 18:22:55 +0000
115@@ -1,6 +1,4 @@
116-
117 from ensemble.lib.testing import TestCase
118-
119 from ensemble.machine import ProviderMachine
120
121
122
123=== modified file 'ensemble/providers/common/launch.py'
124--- ensemble/providers/common/launch.py 2011-08-03 14:48:50 +0000
125+++ ensemble/providers/common/launch.py 2011-08-10 18:22:55 +0000
126@@ -1,6 +1,6 @@
127 import copy
128
129-from twisted.internet.defer import inlineCallbacks, returnValue, fail
130+from twisted.internet.defer import inlineCallbacks, returnValue
131
132 from ensemble.errors import ProviderError
133 from ensemble.state.auth import make_identity
134@@ -69,31 +69,37 @@
135 self._provider = provider
136 self._bootstrap = bootstrap
137
138+ @inlineCallbacks
139 def run(self, machine_data):
140 """Launch an instance node within the machine provider environment.
141
142- @param machine_data a dictionary of data that is passed along to
143- provided to the machine as serialized yaml. 'machine-id' is a
144- required key, denoting the machine's zookeeper node for its
145- machine agent.
146+ `machine_data`: a dictionary of data that is passed along to
147+ provided to the machine as serialized yaml. 'machine-id' is a
148+ required key, denoting the machine's zookeeper node for its
149+ machine agent.
150 """
151 if "machine-id" not in machine_data:
152- return fail(ProviderError(
153- "Machine state `machine-id` not provided in machine_data."))
154+ raise ProviderError(
155+ "Machine state `machine-id` not provided in machine_data.")
156
157- launch_deferred = self.get_machine_variables(machine_data)
158- launch_deferred.addCallback(self.start_machine)
159+ machine_variables = yield self.get_machine_variables(machine_data)
160+ provider_machines = yield self.start_machine(
161+ machine_variables, machine_data["machine-id"])
162 if self._bootstrap:
163- launch_deferred.addCallback(self._on_bootstrap_launched)
164- return launch_deferred
165+ yield self._on_bootstrap_launched(provider_machines)
166+ returnValue(provider_machines)
167
168- def start_machine(self, variables):
169+ def start_machine(self, machine_variables, machine_id):
170 """Actually launch a machine for the appropriate provider.
171
172- @param variables: non-provider-specific data, sufficient to
173+ `machine_variables`: non-provider-specific data, sufficient to
174 define the machine's behaviour once it exists.
175
176- @return: a list of newly-launched ProviderMachines.
177+ `machine_id`: the external machine ID (also exists in the
178+ above bag of data)
179+
180+ Returns a singe-entry list containing a ProviderMachine for the
181+ new instance
182 """
183 raise NotImplementedError()
184
185@@ -169,7 +175,8 @@
186 return [
187 "sudo apt-get install -y python-txzookeeper",
188 "sudo mkdir -p /usr/lib/ensemble",
189- "cd /usr/lib/ensemble && sudo /usr/bin/bzr co %s ensemble" % branch,
190+ "cd /usr/lib/ensemble && sudo /usr/bin/bzr co %s ensemble" % \
191+ branch,
192 "cd /usr/lib/ensemble/ensemble && sudo python setup.py develop"]
193
194 def _get_initialize_script(self):
195
196=== modified file 'ensemble/providers/common/tests/test_launch.py'
197--- ensemble/providers/common/tests/test_launch.py 2011-08-04 09:50:57 +0000
198+++ ensemble/providers/common/tests/test_launch.py 2011-08-10 18:22:55 +0000
199@@ -1,10 +1,9 @@
200-from cStringIO import StringIO
201 import logging
202 import tempfile
203
204 from twisted.internet.defer import fail, succeed
205
206-from ensemble.errors import EnvironmentNotFound, FileNotFound, ProviderError
207+from ensemble.errors import EnvironmentNotFound, ProviderError
208 from ensemble.lib.testing import TestCase
209 from ensemble.providers.common.bootstrap import Bootstrap
210 from ensemble.providers.common.launch import (
211@@ -18,7 +17,7 @@
212
213 class DummyLaunchMachine(LaunchMachine):
214
215- def start_machine(self, variables):
216+ def start_machine(self, variables, data):
217 if self._bootstrap:
218 name = "bootstrapped-instance-id"
219 else:
220
221=== modified file 'ensemble/providers/dummy.py'
222--- ensemble/providers/dummy.py 2011-08-10 18:22:54 +0000
223+++ ensemble/providers/dummy.py 2011-08-10 18:22:55 +0000
224@@ -122,7 +122,7 @@
225 config["dynamicduck"] = "magic"
226 return config
227
228- def open_port(self, machine, port, protocol="tcp"):
229+ def open_port(self, machine, machine_id, port, protocol="tcp"):
230 """Dummy equivalent of ec2-authorize-group"""
231 if not isinstance(machine, DummyMachine):
232 return fail(ProviderError("Invalid machine for provider"))
233@@ -131,7 +131,7 @@
234 port, protocol, machine.instance_id)
235 return succeed(None)
236
237- def close_port(self, machine, port, protocol="tcp"):
238+ def close_port(self, machine, machin_id, port, protocol="tcp"):
239 """Dummy equivalent of ec2-revoke-group"""
240 if not isinstance(machine, DummyMachine):
241 return fail(ProviderError("Invalid machine for provider"))
242@@ -143,7 +143,7 @@
243 pass
244 return succeed(None)
245
246- def get_opened_ports(self, machine):
247+ def get_opened_ports(self, machine, machine_id):
248 """Dummy equivalent of ec2-describe-group
249
250 This returns the current exposed ports in the environment for
251
252=== modified file 'ensemble/providers/ec2/__init__.py'
253--- ensemble/providers/ec2/__init__.py 2011-08-05 12:43:39 +0000
254+++ ensemble/providers/ec2/__init__.py 2011-08-10 18:22:55 +0000
255@@ -16,6 +16,8 @@
256 from .iterate import EC2MachineIteration
257 from .launch import EC2LaunchMachine
258 from .machine import get_machine
259+from .securitygroup import (
260+ open_provider_port, close_provider_port, get_provider_opened_ports)
261 from .shutdown import EC2Shutdown, EC2ShutdownMachine
262 from .utils import get_region_uri
263
264@@ -147,3 +149,15 @@
265 @raise: EnvironmentNotFound or EnvironmentPending
266 """
267 return find_zookeepers(self, get_machine)
268+
269+ def open_port(self, machine, machine_id, port, protocol="tcp"):
270+ """Authorizes `port` using `protocol` on EC2 for `machine`."""
271+ return open_provider_port(self, machine, machine_id, port, protocol)
272+
273+ def close_port(self, machine, machine_id, port, protocol="tcp"):
274+ """Revokes `port` using `protocol` on EC2 for `machine`."""
275+ return close_provider_port(self, machine, machine_id, port, protocol)
276+
277+ def get_opened_ports(self, machine, machine_id):
278+ """Returns a set of open (port, proto) pairs for `machine`."""
279+ return get_provider_opened_ports(self, machine, machine_id)
280
281=== modified file 'ensemble/providers/ec2/accessor.py'
282--- ensemble/providers/ec2/accessor.py 2011-07-13 22:08:49 +0000
283+++ ensemble/providers/ec2/accessor.py 2011-08-10 18:22:55 +0000
284@@ -1,4 +1,5 @@
285-from twisted.internet.defer import fail
286+from twisted.internet.defer import inlineCallbacks, returnValue
287+from txaws.ec2.exception import EC2Error
288
289 from ensemble.errors import ProviderInteractionError
290
291@@ -9,17 +10,21 @@
292 class EC2MachineAccessor(EC2ProviderMachineFilter):
293 """Retrieve a specific machine by provider id."""
294
295+ @inlineCallbacks
296 def run(self, provider_id):
297- d = self._provider.ec2.describe_instances(provider_id)
298- d.addCallback(self._filter_provider_machines)
299- d.addCallback(self._create_provider_machine, provider_id)
300- return d
301-
302- def _create_provider_machine(self, instances, provider_id):
303- if not instances:
304- return fail(
305- ProviderInteractionError(
306- "Machine (id: %r) was not found" % provider_id))
307-
308- instance = instances.pop()
309- return machine_from_instance(instance)
310+ try:
311+ instances = yield self._provider.ec2.describe_instances(
312+ provider_id)
313+ except EC2Error, e:
314+ # AWS may return an error when the instance is not found
315+ # (but presumably eventually will be), eg,
316+ # txaws.ec2.exception.EC2Error: Error Message:
317+ # The instance ID 'i-afe113ce' does not exist
318+ raise ProviderInteractionError(
319+ "EC2 error when looking up instance %s: %s" % (provider_id, e))
320+ filtered = self._filter_provider_machines(instances)
321+ if not filtered:
322+ raise ProviderInteractionError(
323+ "EC2 instance %s was not found" % provider_id)
324+ instance = filtered.pop()
325+ returnValue(machine_from_instance(instance))
326
327=== modified file 'ensemble/providers/ec2/launch.py'
328--- ensemble/providers/ec2/launch.py 2011-07-18 07:07:03 +0000
329+++ ensemble/providers/ec2/launch.py 2011-08-10 18:22:55 +0000
330@@ -1,5 +1,7 @@
331 from twisted.internet.defer import inlineCallbacks, returnValue
332+from txaws.ec2.exception import EC2Error
333
334+from ensemble.errors import ProviderInteractionError
335 from ensemble.providers.common.launch import LaunchMachine
336
337 from .machine import machine_from_instance
338@@ -10,26 +12,30 @@
339 """Amazon EC2 operation for launching an instance"""
340
341 @inlineCallbacks
342- def start_machine(self, variables):
343+ def start_machine(self, machine_variables, machine_id):
344 """Actually launch an instance on EC2.
345
346- @param variables: should be a dictionary with entries sufficient
347- to specify everything necessary to launch the requested
348- instance.
349-
350- @return: a singe-entry list containing a ProviderMachine for the
351- new instance
352+ `machine_variables`: non-provider-specific data, sufficient to
353+ define the machine's behaviour once it exists.
354+
355+ `machine_id`: the external machine ID (also exists in the
356+ above bag of data)
357+
358+ Returns a singe-entry list containing a ProviderMachine for the
359+ new instance
360 """
361 # Retrieves standard ec2 run_instances arguments, of note
362 # image id, and instance type.
363 machine_options = yield get_launch_options(
364 self._provider.config,
365- **variables)
366+ **machine_variables)
367
368- # Ensure the ensemble security group exists and is included in the
369- # machine instance launch options.
370+ # Ensure the ensemble security groups exist (both the overall
371+ # group and the machine-specific group) and are included in
372+ # the machine instance launch options.
373 machine_options["security_groups"] = yield self._ensure_group(
374- machine_options["security_groups"])
375+ machine_options["security_groups"],
376+ machine_id)
377
378 # Launch the instance.
379 instances = yield self._provider.ec2.run_instances(**machine_options)
380@@ -54,24 +60,26 @@
381 vars_deferred.addCallback(set_provider_type)
382 return vars_deferred
383
384- def _ensure_group(self, groups):
385+ @inlineCallbacks
386+ def _ensure_group(self, groups, machine_id):
387 """Ensure the ensemble group is the machine launch groups.
388
389- Machines launched by ensemble are tagged with a group so they can
390- be distinguished from other machines that might be running on
391- an EC2 account. This group can be specified explicitly or implicitly
392- defined by the environment name.
393-
394- @param security_groups: The configured security groups for a machine
395- instance.
396+ Machines launched by ensemble are tagged with a group so they
397+ can be distinguished from other machines that might be running
398+ on an EC2 account. This group can be specified explicitly or
399+ implicitly defined by the environment name. In addition, a
400+ specific machine security group is created for each machine,
401+ so that its firewall rules can be configured per machine.
402+
403+ `groups`: The configured security groups for a machine instance.
404+
405+ `machine_id`: The external machine ID of this machine instance
406 """
407 # Label the instance with the ensemble name.
408- d = self._provider.ec2.describe_security_groups()
409- d.addCallback(self._on_received_groups, groups)
410- return d
411-
412- def _on_received_groups(self, security_groups, groups):
413+ security_groups = yield self._provider.ec2.describe_security_groups()
414 ensemble_group = "ensemble-%s" % self._provider.environment_name
415+ ensemble_machine_group = "ensemble-%s-%s" % (
416+ self._provider.environment_name, machine_id)
417 group_ids = [group.name for group in security_groups]
418
419 # Ensure the provider group is added to the instance groups.
420@@ -81,65 +89,49 @@
421 # Create the provider group if doesn't exist.
422 if not ensemble_group in group_ids:
423 log.debug("Creating ensemble provider group %s", ensemble_group)
424- group_created = self._provider.ec2.create_security_group(
425+ yield self._provider.ec2.create_security_group(
426 ensemble_group,
427 "Ensemble group for %s" % self._provider.environment_name)
428
429 # Authorize SSH.
430- group_created.addCallback(
431- lambda x: self._provider.ec2.authorize_security_group(
432- ensemble_group,
433- ip_protocol="tcp",
434- from_port="22", to_port="22",
435- cidr_ip="0.0.0.0/0"))
436+ yield self._provider.ec2.authorize_security_group(
437+ ensemble_group,
438+ ip_protocol="tcp",
439+ from_port="22", to_port="22",
440+ cidr_ip="0.0.0.0/0")
441
442 # We need to describe the group to pickup the owner_id for auth.
443- group_created.addCallback(
444- lambda x: self._provider.ec2.describe_security_groups(
445- ensemble_group))
446+ groups_info = yield self._provider.ec2.describe_security_groups(
447+ ensemble_group)
448
449 # Authorize Internal ZK Traffic
450- group_created.addCallback(
451- lambda groups: self._provider.ec2.authorize_security_group(
452- ensemble_group,
453- source_group_name=ensemble_group,
454- source_group_owner_id=groups.pop().owner_id))
455-
456- # TODO: For now authorize external traffic to the instance,
457- # except for the ZooKeeper port. The expose functionality
458- # that is in progress will address this in a better way.
459- group_created.addCallback(
460- lambda groups: self._provider.ec2.authorize_security_group(
461- ensemble_group,
462- ip_protocol="tcp",
463- from_port="1",
464- to_port="2180",
465- cidr_ip="0.0.0.0/0"))
466- group_created.addCallback(
467- lambda groups: self._provider.ec2.authorize_security_group(
468- ensemble_group,
469- ip_protocol="tcp",
470- from_port="2182",
471- to_port="65535",
472- cidr_ip="0.0.0.0/0"))
473-
474- group_created.addCallback(
475- lambda groups: self._provider.ec2.authorize_security_group(
476- ensemble_group,
477- ip_protocol="udp",
478- from_port="1",
479- to_port="2180",
480- cidr_ip="0.0.0.0/0"))
481- group_created.addCallback(
482- lambda groups: self._provider.ec2.authorize_security_group(
483- ensemble_group,
484- ip_protocol="udp",
485- from_port="2182",
486- to_port="65535",
487- cidr_ip="0.0.0.0/0"))
488-
489- # We use a separate callback here, because we need to wait
490- # on the authorize security group deferred to succeed before
491- # we can return the groups result.
492- return group_created.addCallback(lambda x: groups)
493- return groups
494+ yield self._provider.ec2.authorize_security_group(
495+ ensemble_group,
496+ source_group_name=ensemble_group,
497+ source_group_owner_id=groups_info.pop().owner_id)
498+
499+ # Create the machine-specific group, but first see if there's
500+ # one already existing from a previous machine launch;
501+ # if so, delete it, since it can have the wrong firewall setup
502+ if ensemble_machine_group in group_ids:
503+ try:
504+ yield self._provider.ec2.delete_security_group(
505+ ensemble_machine_group)
506+ log.debug("Deleted existing machine group %s, will replace",
507+ ensemble_machine_group)
508+ except EC2Error, e:
509+ log.debug("Cannot delete security group %s: %s",
510+ ensemble_machine_group, e)
511+ raise ProviderInteractionError(
512+ "EC2 error when attempting to delete "
513+ "security group %s: %s" % (
514+ ensemble_machine_group, e))
515+ log.debug("Creating ensemble machine security group %s",
516+ ensemble_machine_group)
517+ yield self._provider.ec2.create_security_group(
518+ ensemble_machine_group,
519+ "Ensemble group for %s machine %s" % (
520+ self._provider.environment_name, machine_id))
521+ groups.append(ensemble_machine_group)
522+
523+ returnValue(groups)
524
525=== added file 'ensemble/providers/ec2/securitygroup.py'
526--- ensemble/providers/ec2/securitygroup.py 1970-01-01 00:00:00 +0000
527+++ ensemble/providers/ec2/securitygroup.py 2011-08-10 18:22:55 +0000
528@@ -0,0 +1,90 @@
529+from twisted.internet.defer import inlineCallbacks, returnValue
530+from txaws.ec2.exception import EC2Error
531+
532+from ensemble.errors import ProviderInteractionError
533+
534+from .utils import log
535+
536+
537+def _get_machine_group_name(provider, machine_id):
538+ """Get EC2 security group name associated just with `machine_id`."""
539+ return "ensemble-%s-%s" % (provider.environment_name, machine_id)
540+
541+
542+# TODO These security group functions do not handle the eventual
543+# consistency seen with EC2. A future branch will add support for
544+# retry so that using code doesn't have to be aware of this issue.
545+#
546+# In addition, the functions work with respect to the machine id,
547+# since they manipulate a security group permanently associated with
548+# the EC2 provided machine, and the machine must be launched into this
549+# security group. This security group, per the above
550+# `_get_machine_group_name`, embeds the machine id, eg
551+# ensemble-moon-42. Ideally, this would not be the case. See the
552+# comments associated with the merge proposal of
553+# https://code.launchpad.net/~jimbaker/ensemble/expose-provider-ec2/
554+
555+
556+@inlineCallbacks
557+def open_provider_port(provider, machine, machine_id, port, protocol):
558+ """Authorize `port`/`proto` for the machine security group."""
559+ try:
560+ yield provider.ec2.authorize_security_group(
561+ _get_machine_group_name(provider, machine_id),
562+ ip_protocol=protocol,
563+ from_port=str(port), to_port=str(port),
564+ cidr_ip="0.0.0.0/0")
565+ log.debug("Opened %s/%s on provider machine %r",
566+ port, protocol, machine.instance_id)
567+ except EC2Error, e:
568+ raise ProviderInteractionError(
569+ "EC2 error when attempting to open %s/%s on machine %s: %s" % (
570+ port, protocol, machine.instance_id, e))
571+
572+
573+@inlineCallbacks
574+def close_provider_port(provider, machine, machine_id, port, protocol):
575+ """Revoke `port`/`proto` for the machine security group."""
576+ try:
577+ yield provider.ec2.revoke_security_group(
578+ _get_machine_group_name(provider, machine_id),
579+ ip_protocol=protocol,
580+ from_port=str(port), to_port=str(port),
581+ cidr_ip="0.0.0.0/0")
582+ log.debug("Closed %s/%s on provider machine %r",
583+ port, protocol, machine.instance_id)
584+ except EC2Error, e:
585+ raise ProviderInteractionError(
586+ "EC2 error when attempting to close %s/%s on machine %s: %s" % (
587+ port, protocol, machine.instance_id, e))
588+
589+
590+@inlineCallbacks
591+def get_provider_opened_ports(provider, machine, machine_id):
592+ """Gets the opened ports for `machine`.
593+
594+ Retrieves the IP permissions associated with the machine
595+ security group, then parses them to return a set of (port,
596+ proto) pairs.
597+ """
598+ try:
599+ security_groups = yield provider.ec2.describe_security_groups(
600+ _get_machine_group_name(provider, machine_id))
601+ except EC2Error, e:
602+ raise ProviderInteractionError(
603+ "EC2 error when attempting to get opened ports "
604+ "on machine %s: %s" % (
605+ machine.instance_id, e))
606+
607+ opened_ports = set() # made up of (port, protocol) pairs
608+ for ip_permission in security_groups[0].allowed_ips:
609+ if ip_permission.cidr_ip != "0.0.0.0/0":
610+ continue
611+ from_port = int(ip_permission.from_port)
612+ to_port = int(ip_permission.to_port)
613+ if from_port == to_port:
614+ # Only return ports that are individually opened. We
615+ # ignore multi-port ranges, since they are set outside of
616+ # Ensemble (at this time at least)
617+ opened_ports.add((from_port, ip_permission.ip_protocol))
618+ returnValue(opened_ports)
619
620=== modified file 'ensemble/providers/ec2/tests/common.py'
621--- ensemble/providers/ec2/tests/common.py 2011-07-27 10:35:59 +0000
622+++ ensemble/providers/ec2/tests/common.py 2011-08-10 18:22:55 +0000
623@@ -5,6 +5,7 @@
624 from txaws.s3.client import S3Client
625 from txaws.s3.exception import S3Error
626 from txaws.ec2.client import EC2Client
627+from txaws.ec2.exception import EC2Error
628 from txaws.ec2.model import Instance, SecurityGroup
629
630 from ensemble.lib.mocker import KWARGS, MATCH
631@@ -32,6 +33,29 @@
632 """
633 return MachineProvider(self.env_name, self.get_config())
634
635+ def get_ec2_error(self, entity_id,
636+ format="The instance ID %r does not exist",
637+ code=503):
638+ """Make a representative EC2Error for `entity_id`, eg AWS instance_id.
639+
640+ This error is paired with `get_wrapped_ec2_text` below. The
641+ default format represents a fairly common error seen in
642+ working with EC2. There are others."""
643+ message = format % entity_id
644+ return EC2Error(
645+ "<error><Code>1</Code><Message>%s</Message></error>" % message,
646+ code)
647+
648+ def get_wrapped_ec2_error_text(self, entity_id, reason,
649+ format="The instance ID %r does not exist"):
650+ """By convention, `EC2Error` is wrapped as a `ProviderError`"""
651+ message = format % entity_id
652+ return (
653+ "ProviderError: Interaction with machine provider failed: "
654+ "\"EC2 error when attempting to %s %s: "
655+ "Error Message: %s\"" % (
656+ reason, entity_id, message))
657+
658 def setUp(self):
659 # mock out the aws services
660 service_factory = self.mocker.replace(
661@@ -95,33 +119,24 @@
662 source_group_owner_id="123")
663 self.mocker.result(succeed(True))
664
665- self.ec2.authorize_security_group(
666- group_name,
667- ip_protocol="tcp",
668- from_port="1",
669- to_port="2180",
670- cidr_ip="0.0.0.0/0")
671- self.ec2.authorize_security_group(
672- group_name,
673- ip_protocol="tcp",
674- from_port="2182",
675- to_port="65535",
676- cidr_ip="0.0.0.0/0")
677-
678- self.ec2.authorize_security_group(
679- group_name,
680- ip_protocol="udp",
681- from_port="1",
682- to_port="2180",
683- cidr_ip="0.0.0.0/0")
684- self.ec2.authorize_security_group(
685- group_name,
686- ip_protocol="udp",
687- from_port="2182",
688- to_port="65535",
689- cidr_ip="0.0.0.0/0")
690-
691- self.mocker.result(succeed(True))
692+ def _mock_create_machine_group(self, machine_id):
693+ machine_group_name = "ensemble-%s-%s" % (self.env_name, machine_id)
694+ self.ec2.create_security_group(
695+ machine_group_name, "Ensemble group for %s machine %s" % (
696+ self.env_name, machine_id))
697+ self.mocker.result(succeed(True))
698+
699+ def _mock_delete_machine_group(self, machine_id):
700+ machine_group_name = "ensemble-%s-%s" % (self.env_name, machine_id)
701+ self.ec2.delete_security_group(machine_group_name)
702+ self.mocker.result(succeed(True))
703+
704+ def _mock_delete_machine_group_was_deleted(self, machine_id):
705+ machine_group_name = "ensemble-%s-%s" % (self.env_name, machine_id)
706+ self.ec2.delete_security_group(machine_group_name)
707+ self.mocker.result(fail(self.get_ec2_error(
708+ machine_group_name,
709+ "There are active instances using security group %r")))
710
711 def _mock_get_zookeeper_hosts(self, hosts=None):
712 """
713@@ -152,5 +167,3 @@
714 # connect grabs the first host of a set.
715 self.ec2.describe_instances(hosts[0].instance_id)
716 self.mocker.result(succeed([hosts[0]]))
717-
718-
719
720=== modified file 'ensemble/providers/ec2/tests/test_accessor.py'
721--- ensemble/providers/ec2/tests/test_accessor.py 2011-01-26 23:39:35 +0000
722+++ ensemble/providers/ec2/tests/test_accessor.py 2011-08-10 18:22:55 +0000
723@@ -1,4 +1,4 @@
724-from twisted.internet.defer import succeed
725+from twisted.internet.defer import fail, succeed
726 from txaws.ec2.model import Instance, Reservation
727
728 from ensemble.errors import ProviderInteractionError
729@@ -54,7 +54,29 @@
730
731 def validate_error(error):
732 self.assertIn(
733- "Machine (id: 'i-foobar') was not found",
734+ "EC2 instance i-foobar was not found",
735+ str(error))
736+
737+ d.addCallback(validate_error)
738+ return d
739+
740+ def test_instance_not_found(self):
741+ """Verify that if the instance is not known to EC2,
742+ raises ProviderInteractionError.
743+ """
744+ self.ec2.describe_instances("i-foobar")
745+ self.mocker.result(fail(self.get_ec2_error("i-foobar")))
746+ self.mocker.replay()
747+
748+ provider = self.get_provider()
749+
750+ d = self.assertFailure(
751+ provider.get_machine("i-foobar"),
752+ ProviderInteractionError)
753+
754+ def validate_error(error):
755+ self.assertIn(
756+ "The instance ID \'i-foobar\' does not exist",
757 str(error))
758
759 d.addCallback(validate_error)
760
761=== modified file 'ensemble/providers/ec2/tests/test_bootstrap.py'
762--- ensemble/providers/ec2/tests/test_bootstrap.py 2011-08-04 09:50:57 +0000
763+++ ensemble/providers/ec2/tests/test_bootstrap.py 2011-08-10 18:22:55 +0000
764@@ -35,7 +35,7 @@
765 MATCH(match_string))
766 self.mocker.result(succeed(True))
767
768- def _mock_launch(self):
769+ def _mock_launch(self, machine_id):
770 """Mock launching a bootstrap machine on ec2."""
771 credentials = "admin:%s" % self.get_config()["admin-secret"]
772 admin_identity = make_identity(credentials)
773@@ -81,7 +81,9 @@
774 instance_type="m1.small",
775 max_count=1,
776 min_count=1,
777- security_groups=["ensemble-%s" % self.env_name],
778+ security_groups=[
779+ "%s-%s" % ("ensemble", self.env_name),
780+ "%s-%s-%s" % ("ensemble", self.env_name, machine_id)],
781 user_data=MATCH(verify_user_data))
782
783 def test_launch_bootstrap(self):
784@@ -95,8 +97,9 @@
785 self.ec2.describe_security_groups()
786 self.mocker.result(succeed([]))
787 self._mock_create_group()
788+ self._mock_create_machine_group(0)
789 self._mock_launch_utils(region="us-east-1")
790- self._mock_launch()
791+ self._mock_launch(0)
792 self.mocker.result(succeed([]))
793 self._mock_save()
794 self.mocker.replay()
795@@ -124,8 +127,9 @@
796 self.ec2.describe_security_groups()
797 self.mocker.result(succeed([
798 SecurityGroup("ensemble-%s" % self.env_name, "")]))
799+ self._mock_create_machine_group(0)
800 self._mock_launch_utils(region="us-east-1")
801- self._mock_launch()
802+ self._mock_launch(0)
803 self.mocker.result(succeed([]))
804 self._mock_save()
805 self.mocker.replay()
806@@ -201,8 +205,9 @@
807 self.ec2.describe_security_groups()
808 self.mocker.result(succeed([
809 SecurityGroup("ensemble-%s" % self.env_name, "")]))
810+ self._mock_create_machine_group(0)
811 self._mock_launch_utils(region="us-east-1")
812- self._mock_launch()
813+ self._mock_launch(0)
814 self.mocker.result(succeed(instances))
815 self._mock_save()
816 self.mocker.replay()
817@@ -229,8 +234,9 @@
818 self.ec2.describe_security_groups()
819 self.mocker.result(succeed([
820 SecurityGroup("ensemble-%s" % self.env_name, "")]))
821+ self._mock_create_machine_group(0)
822 self._mock_launch_utils(region="us-east-1")
823- self._mock_launch()
824+ self._mock_launch(0)
825 self.mocker.result(succeed(instances))
826 self._mock_save()
827 self.mocker.replay()
828
829=== modified file 'ensemble/providers/ec2/tests/test_files.py'
830--- ensemble/providers/ec2/tests/test_files.py 2011-08-01 20:25:56 +0000
831+++ ensemble/providers/ec2/tests/test_files.py 2011-08-10 18:22:55 +0000
832@@ -100,7 +100,7 @@
833
834 def test_put_file_slightly_wrong_error(self):
835 error = S3Error("<ignored/>", 404)
836- error.errors = [{"Code": "NoSuchKey"}] # note total nonsense
837+ error.errors = [{"Code": "NoSuchKey"}] # note total nonsense
838 return self.verify_strange_put_error(error)
839
840 def test_put_file_unknown_error(self):
841
842=== modified file 'ensemble/providers/ec2/tests/test_launch.py'
843--- ensemble/providers/ec2/tests/test_launch.py 2011-08-02 09:15:47 +0000
844+++ ensemble/providers/ec2/tests/test_launch.py 2011-08-10 18:22:55 +0000
845@@ -1,10 +1,10 @@
846 from yaml import load
847
848-from twisted.internet.defer import succeed
849-
850-from txaws.ec2.model import Instance
851-
852-from ensemble.errors import EnvironmentNotFound
853+from twisted.internet.defer import inlineCallbacks, succeed
854+
855+from txaws.ec2.model import Instance, SecurityGroup
856+
857+from ensemble.errors import EnvironmentNotFound, ProviderInteractionError
858 from ensemble.providers.common.launch import (
859 DEFAULT_REPOSITORIES, DEFAULT_PACKAGES)
860 from ensemble.providers.ec2.machine import EC2ProviderMachine
861@@ -17,7 +17,7 @@
862
863 class EC2MachineLaunchTest(EC2TestMixin, EC2MachineLaunchMixin, TestCase):
864
865- def _mock_launch(self, instance=None, custom_verify=None):
866+ def _mock_launch(self, instance=None, custom_verify=None, machine_id=None):
867
868 def verify_user_data(data):
869 lines = data.split("\n")
870@@ -37,7 +37,9 @@
871 instance_type="m1.small",
872 max_count=1,
873 min_count=1,
874- security_groups=["%s-%s" % ("ensemble", self.env_name)],
875+ security_groups=[
876+ "%s-%s" % ("ensemble", self.env_name),
877+ "%s-%s-%s" % ("ensemble", self.env_name, machine_id)],
878 user_data=MATCH(verify_user_data))
879
880 if instance:
881@@ -45,6 +47,7 @@
882 else:
883 self.mocker.result(succeed([]))
884
885+ @inlineCallbacks
886 def test_provider_launch(self):
887 """
888 The provider can be used to launch a machine with a minimal set of
889@@ -55,27 +58,101 @@
890 self.ec2.describe_security_groups()
891 self.mocker.result(succeed([]))
892 self._mock_create_group()
893- self._mock_launch_utils(region="us-east-1")
894- self._mock_get_zookeeper_hosts()
895- self._mock_launch(instance)
896- self.mocker.replay()
897-
898- def verify_result(result):
899- self.assertEqual(len(result), 1)
900- result = result.pop()
901- self.assertTrue(isinstance(result, EC2ProviderMachine))
902- self.assertEqual(result.instance_id, instance.instance_id)
903-
904- provider = self.get_provider()
905- d = provider.start_machine({"machine-id": "machine-1"})
906- d.addCallback(verify_result)
907- return d
908+ self._mock_create_machine_group("machine-1")
909+ self._mock_launch_utils(region="us-east-1")
910+ self._mock_get_zookeeper_hosts()
911+ self._mock_launch(instance, machine_id="machine-1")
912+ self.mocker.replay()
913+
914+ provider = self.get_provider()
915+ provided_machines = yield provider.start_machine(
916+ {"machine-id": "machine-1"})
917+ self.assertEqual(len(provided_machines), 1)
918+ self.assertTrue(isinstance(provided_machines[0], EC2ProviderMachine))
919+ self.assertEqual(
920+ provided_machines[0].instance_id, instance.instance_id)
921+
922+ @inlineCallbacks
923+ def test_provider_launch_existing_security_group(self):
924+ """Verify that the launch works if the env security group exists"""
925+ instance = Instance("i-foobar", "running", dns_name="x1.example.com")
926+ security_group = SecurityGroup("ensemble-moon", "some description")
927+
928+ self.ec2.describe_security_groups()
929+ self.mocker.result(succeed([security_group]))
930+ self._mock_create_machine_group("machine-1")
931+ self._mock_launch_utils(region="us-east-1")
932+ self._mock_get_zookeeper_hosts()
933+ self._mock_launch(instance, machine_id="machine-1")
934+ self.mocker.replay()
935+
936+ provider = self.get_provider()
937+ provided_machines = yield provider.start_machine(
938+ {"machine-id": "machine-1"})
939+ self.assertEqual(len(provided_machines), 1)
940+ self.assertTrue(isinstance(provided_machines[0], EC2ProviderMachine))
941+ self.assertEqual(
942+ provided_machines[0].instance_id, instance.instance_id)
943+
944+ @inlineCallbacks
945+ def test_provider_launch_existing_machine_security_group(self):
946+ """Verify that the launch works if the machine security group exists"""
947+ instance = Instance("i-foobar", "running", dns_name="x1.example.com")
948+ machine_group = SecurityGroup(
949+ "ensemble-moon-machine-1", "some description")
950+
951+ self.ec2.describe_security_groups()
952+ self.mocker.result(succeed([machine_group]))
953+ self._mock_create_group()
954+ self._mock_delete_machine_group("machine-1") # delete existing sg
955+ self._mock_create_machine_group("machine-1") # then recreate
956+ self._mock_launch_utils(region="us-east-1")
957+ self._mock_get_zookeeper_hosts()
958+ self._mock_launch(instance, machine_id="machine-1")
959+ self.mocker.replay()
960+
961+ provider = self.get_provider()
962+ provided_machines = yield provider.start_machine(
963+ {"machine-id": "machine-1"})
964+ self.assertEqual(len(provided_machines), 1)
965+ self.assertTrue(isinstance(provided_machines[0], EC2ProviderMachine))
966+ self.assertEqual(
967+ provided_machines[0].instance_id, instance.instance_id)
968+
969+ @inlineCallbacks
970+ def test_provider_launch_existing_machine_security_group_is_active(self):
971+ """Verify launch fails properly if the machine group is stil active.
972+
973+ This condition occurs when there is a corresponding machine in
974+ that security group, generally because it is still shutting
975+ down."""
976+ machine_group = SecurityGroup(
977+ "ensemble-moon-machine-1", "some description")
978+
979+ self.ec2.describe_security_groups()
980+ self.mocker.result(succeed([machine_group]))
981+ self._mock_create_group()
982+ self._mock_delete_machine_group_was_deleted("machine-1") # sg is gone!
983+ self._mock_get_zookeeper_hosts()
984+ self.mocker.replay()
985+
986+ provider = self.get_provider()
987+ ex = yield self.assertFailure(
988+ provider.start_machine({"machine-id": "machine-1"}),
989+ ProviderInteractionError)
990+ self.assertEqual(
991+ str(ex),
992+ self.get_wrapped_ec2_error_text(
993+ "ensemble-moon-machine-1", "delete security group",
994+ "There are active instances using security group %r"
995+ ))
996
997 def test_provider_type_machine_variable(self):
998 """The provider type is available via cloud-init."""
999 self.ec2.describe_security_groups()
1000 self.mocker.result(succeed([]))
1001 self._mock_create_group()
1002+ self._mock_create_machine_group("machine-1")
1003 self._mock_launch_utils(region="us-east-1")
1004 self._mock_get_zookeeper_hosts()
1005
1006@@ -83,7 +160,8 @@
1007 self.assertEqual(
1008 data["machine-data"]["ensemble-provider-type"], "ec2")
1009
1010- self._mock_launch(custom_verify=verify_provider_type)
1011+ self._mock_launch(custom_verify=verify_provider_type,
1012+ machine_id="machine-1")
1013 self.mocker.replay()
1014
1015 provider = self.get_provider()
1016@@ -128,6 +206,7 @@
1017 self.ec2.describe_security_groups()
1018 self.mocker.result(succeed([]))
1019 self._mock_create_group()
1020+ self._mock_create_machine_group("machine-1")
1021 self._mock_launch_utils(region="us-east-1")
1022 self._mock_get_zookeeper_hosts()
1023
1024@@ -139,7 +218,8 @@
1025 "--pidfile=/var/run/ensemble/machine-agent.pid")
1026 self.assertEqual(script, data["runcmd"][-1])
1027
1028- self._mock_launch(custom_verify=verify_machine_agent)
1029+ self._mock_launch(custom_verify=verify_machine_agent,
1030+ machine_id="machine-1")
1031 self.mocker.replay()
1032
1033 provider = self.get_provider()
1034@@ -151,9 +231,10 @@
1035 self.ec2.describe_security_groups()
1036 self.mocker.result(succeed([]))
1037 self._mock_create_group()
1038+ self._mock_create_machine_group("machine-1")
1039 self._mock_launch_utils(**get_ami_kwargs)
1040 self._mock_get_zookeeper_hosts()
1041- self._mock_launch(instance)
1042+ self._mock_launch(instance, machine_id="machine-1")
1043
1044 def test_launch_options_known_ami(self):
1045 self._mock_launch_with_ami_params({})
1046
1047=== added file 'ensemble/providers/ec2/tests/test_securitygroup.py'
1048--- ensemble/providers/ec2/tests/test_securitygroup.py 1970-01-01 00:00:00 +0000
1049+++ ensemble/providers/ec2/tests/test_securitygroup.py 2011-08-10 18:22:55 +0000
1050@@ -0,0 +1,129 @@
1051+import logging
1052+
1053+from twisted.internet.defer import fail, succeed, inlineCallbacks
1054+from txaws.ec2.model import IPPermission, SecurityGroup
1055+
1056+from ensemble.errors import ProviderInteractionError
1057+from ensemble.lib.testing import TestCase
1058+from ensemble.machine import ProviderMachine
1059+from ensemble.providers.ec2.securitygroup import (
1060+ open_provider_port, close_provider_port, get_provider_opened_ports)
1061+
1062+from .common import EC2TestMixin
1063+
1064+
1065+class EC2SecurityGroupTest(EC2TestMixin, TestCase):
1066+
1067+ @inlineCallbacks
1068+ def test_open_provider_port(self):
1069+ """Verify open port op will use the correct EC2 API."""
1070+ log = self.capture_logging("ensemble.ec2", level=logging.DEBUG)
1071+ machine = ProviderMachine("i-foobar", "x1.example.com")
1072+ self.ec2.authorize_security_group(
1073+ "ensemble-moon-machine-1", ip_protocol="tcp", from_port="80",
1074+ to_port="80", cidr_ip="0.0.0.0/0")
1075+ self.mocker.result(succeed(True))
1076+ self.mocker.replay()
1077+
1078+ provider = self.get_provider()
1079+ yield open_provider_port(provider, machine, "machine-1", 80, "tcp")
1080+ self.assertIn(
1081+ "Opened 80/tcp on provider machine 'i-foobar'",
1082+ log.getvalue())
1083+
1084+ @inlineCallbacks
1085+ def test_close_provider_port(self):
1086+ """Verify close port op will use the correct EC2 API."""
1087+ log = self.capture_logging("ensemble.ec2", level=logging.DEBUG)
1088+ machine = ProviderMachine("i-foobar", "x1.example.com")
1089+ self.ec2.revoke_security_group(
1090+ "ensemble-moon-machine-1", ip_protocol="tcp", from_port="80",
1091+ to_port="80", cidr_ip="0.0.0.0/0")
1092+ self.mocker.result(succeed(True))
1093+ self.mocker.replay()
1094+
1095+ provider = self.get_provider()
1096+ yield close_provider_port(provider, machine, "machine-1", 80, "tcp")
1097+ self.assertIn(
1098+ "Closed 80/tcp on provider machine 'i-foobar'",
1099+ log.getvalue())
1100+
1101+ @inlineCallbacks
1102+ def test_get_provider_opened_ports(self):
1103+ """Verify correct parse of IP perms from describe_security_group."""
1104+ self.ec2.describe_security_groups("ensemble-moon-machine-1")
1105+ self.mocker.result(succeed([
1106+ SecurityGroup(
1107+ "ensemble-%s-machine-1" % self.env_name,
1108+ "a security group name",
1109+ ips=[
1110+ IPPermission("udp", "53", "53", "0.0.0.0/0"),
1111+ IPPermission("tcp", "80", "80", "0.0.0.0/0"),
1112+ # The range 8080-8082 will be ignored
1113+ IPPermission("tcp", "8080", "8082", "0.0.0.0/0"),
1114+ # Ignore permissions that are not 0.0.0.0/0
1115+ IPPermission("tcp", "443", "443", "10.1.2.3")
1116+ ])]))
1117+ self.mocker.replay()
1118+
1119+ provider = self.get_provider()
1120+ machine = ProviderMachine(
1121+ "i-foobar", "x1.example.com")
1122+ opened_ports = yield get_provider_opened_ports(
1123+ provider, machine, "machine-1")
1124+ self.assertEqual(opened_ports, set([(53, "udp"), (80, "tcp")]))
1125+
1126+ @inlineCallbacks
1127+ def test_open_provider_port_unknown_instance(self):
1128+ """Verify open port op will use the correct EC2 API."""
1129+ machine = ProviderMachine("i-foobar", "x1.example.com")
1130+ self.ec2.authorize_security_group(
1131+ "ensemble-moon-machine-1", ip_protocol="tcp", from_port="80",
1132+ to_port="80", cidr_ip="0.0.0.0/0")
1133+ self.mocker.result(fail(self.get_ec2_error("i-foobar")))
1134+ self.mocker.replay()
1135+
1136+ provider = self.get_provider()
1137+ ex = yield self.assertFailure(
1138+ open_provider_port(provider, machine, "machine-1", 80, "tcp"),
1139+ ProviderInteractionError)
1140+ self.assertEqual(
1141+ str(ex),
1142+ self.get_wrapped_ec2_error_text(
1143+ "i-foobar", "open 80/tcp on machine"))
1144+
1145+ @inlineCallbacks
1146+ def test_close_provider_port_unknown_instance(self):
1147+ """Verify open port op will use the correct EC2 API."""
1148+ machine = ProviderMachine("i-foobar", "x1.example.com")
1149+ self.ec2.revoke_security_group(
1150+ "ensemble-moon-machine-1", ip_protocol="tcp", from_port="80",
1151+ to_port="80", cidr_ip="0.0.0.0/0")
1152+ self.mocker.result(fail(self.get_ec2_error("i-foobar")))
1153+ self.mocker.replay()
1154+
1155+ provider = self.get_provider()
1156+ ex = yield self.assertFailure(
1157+ close_provider_port(provider, machine, "machine-1", 80, "tcp"),
1158+ ProviderInteractionError)
1159+ self.assertEqual(
1160+ str(ex),
1161+ self.get_wrapped_ec2_error_text(
1162+ "i-foobar", "close 80/tcp on machine"))
1163+
1164+ @inlineCallbacks
1165+ def test_get_provider_opened_ports_unknown_instance(self):
1166+ """Verify open port op will use the correct EC2 API."""
1167+ self.ec2.describe_security_groups("ensemble-moon-machine-1")
1168+ self.mocker.result(fail(self.get_ec2_error("i-foobar")))
1169+ self.mocker.replay()
1170+
1171+ provider = self.get_provider()
1172+ machine = ProviderMachine("i-foobar", "x1.example.com")
1173+ ex = yield self.assertFailure(
1174+ get_provider_opened_ports(provider, machine, "machine-1"),
1175+ ProviderInteractionError)
1176+ self.assertEqual(
1177+ str(ex),
1178+ self.get_wrapped_ec2_error_text(
1179+ "i-foobar", "get opened ports on machine"))
1180
1181=== modified file 'ensemble/providers/orchestra/launch.py'
1182--- ensemble/providers/orchestra/launch.py 2011-08-09 21:19:13 +0000
1183+++ ensemble/providers/orchestra/launch.py 2011-08-10 18:22:55 +0000
1184@@ -62,7 +62,7 @@
1185 class OrchestraLaunchMachine(LaunchMachine):
1186
1187 @inlineCallbacks
1188- def start_machine(self, variables):
1189+ def start_machine(self, variables, machine_id):
1190 cobbler = self._provider.cobbler
1191 instance_id = yield cobbler.acquire_system()
1192 ks_meta = self._build_ks_meta(instance_id, variables)
1193
1194=== modified file 'ensemble/providers/orchestra/tests/common.py'
1195--- ensemble/providers/orchestra/tests/common.py 2011-08-09 21:19:13 +0000
1196+++ ensemble/providers/orchestra/tests/common.py 2011-08-10 18:22:55 +0000
1197@@ -153,4 +153,3 @@
1198 self.mocker.result(succeed([name]))
1199 self.proxy_m.callRemote("get_system", name)
1200 self.mocker.result(succeed({"name": name, "uid": uid}))
1201-
1202
1203=== modified file 'ensemble/providers/tests/test_dummy.py'
1204--- ensemble/providers/tests/test_dummy.py 2011-07-31 00:20:08 +0000
1205+++ ensemble/providers/tests/test_dummy.py 2011-08-10 18:22:55 +0000
1206@@ -57,7 +57,8 @@
1207
1208 @inlineCallbacks
1209 def test_start_machine_accepts_machine_data(self):
1210- machines = yield self.provider.start_machine({"machine-id": 0, "a": 1})
1211+ machines = yield self.provider.start_machine(
1212+ {"machine-id": 0, "a": 1})
1213 self.assertEqual(len(machines), 1)
1214 self.assertTrue(isinstance(machines[0], ProviderMachine))
1215
1216@@ -141,13 +142,13 @@
1217 """Verifies dummy provider properly works with ports."""
1218 machines = yield self.provider.start_machine({"machine-id": 0})
1219 machine = machines[0]
1220- yield self.provider.open_port(machine, 25, "tcp")
1221- yield self.provider.open_port(machine, 80)
1222- yield self.provider.open_port(machine, 53, "udp")
1223- yield self.provider.open_port(machine, 443, "tcp")
1224- yield self.provider.close_port(machine, 25)
1225- yield self.provider.close_port(machine, 25) # ignored
1226- exposed_ports = yield self.provider.get_opened_ports(machine)
1227+ yield self.provider.open_port(machine, 0, 25, "tcp")
1228+ yield self.provider.open_port(machine, 0, 80)
1229+ yield self.provider.open_port(machine, 0, 53, "udp")
1230+ yield self.provider.open_port(machine, 0, 443, "tcp")
1231+ yield self.provider.close_port(machine, 0, 25)
1232+ yield self.provider.close_port(machine, 0, 25) # ignored
1233+ exposed_ports = yield self.provider.get_opened_ports(machine, 0)
1234 self.assertEqual(exposed_ports,
1235 set([(53, 'udp'), (80, 'tcp'), (443, 'tcp')]))
1236
1237
1238=== modified file 'examples/wordpress/hooks/db-relation-changed'
1239--- examples/wordpress/hooks/db-relation-changed 2011-05-03 08:54:13 +0000
1240+++ examples/wordpress/hooks/db-relation-changed 2011-08-10 18:22:55 +0000
1241@@ -98,3 +98,6 @@
1242 # Restart apache
1243 ensemble-log "Restarting apache2 service"
1244 /etc/init.d/apache2 restart
1245+
1246+# Make it publicly visible, once the wordpress service is exposed
1247+open-port 80/tcp

Subscribers

People subscribed via source and target branches

to status/vote changes: