Merge lp:~fwereade/pyjuju/provider-base into lp:pyjuju

Proposed by William Reade
Status: Merged
Approved by: Kapil Thangavelu
Approved revision: 325
Merged at revision: 319
Proposed branch: lp:~fwereade/pyjuju/provider-base
Merge into: lp:pyjuju
Prerequisite: lp:~fwereade/pyjuju/spike-catchup
Diff against target: 1364 lines (+566/-318)
16 files modified
ensemble/control/shutdown.py (+1/-1)
ensemble/control/tests/test_shutdown.py (+1/-1)
ensemble/providers/common/base.py (+173/-0)
ensemble/providers/common/bootstrap.py (+4/-5)
ensemble/providers/common/launch.py (+13/-10)
ensemble/providers/common/tests/test_base.py (+135/-0)
ensemble/providers/common/tests/test_findzookeepers.py (+41/-31)
ensemble/providers/common/tests/test_launch.py (+31/-34)
ensemble/providers/common/tests/test_state.py (+2/-1)
ensemble/providers/common/tests/test_utils.py (+1/-1)
ensemble/providers/dummy.py (+15/-10)
ensemble/providers/ec2/__init__.py (+24/-84)
ensemble/providers/ec2/tests/test_provider.py (+4/-2)
ensemble/providers/ec2/tests/test_shutdown.py (+103/-14)
ensemble/providers/orchestra/__init__.py (+14/-120)
ensemble/providers/tests/test_dummy.py (+4/-4)
To merge this branch: bzr merge lp:~fwereade/pyjuju/provider-base
Reviewer Review Type Date Requested Status
Kapil Thangavelu (community) Approve
Gustavo Niemeyer Approve
Review via email: mp+71272@code.launchpad.net

Description of the change

The ec2 and orchestra MachineProviders have been getting much more similar, and it's time to create a common base class; as part of this change, I've altered the interface slightly such that the base class provides .shutdown_machine and .destroy_environment (renamed from .shutdown); subclasses are expected to implement the new .shutdown_machines method.

I've left the DummyProvider separate (besides fixing the interface), because *many* tests use it in such a way as to assume it's already bootstrapped (or doesn't need to be bootstrapped, from an alternative perspective), and it seems more sensible to keep it optimized for the common case than to (1) rearrange its code and (2) demand that all the clients change to accommodate things they don't really care about.

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

(er, didn't directly test the base class itself... marked WIP again before someone does it for me)

lp:~fwereade/pyjuju/provider-base updated
319. By William Reade

unsaved file

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

> (er, didn't directly test the base class itself... marked WIP again before
> someone does it for me)

OK, I think the base class is now thoroughly tested; the parts that aren't hit in test_base.py are hit via MachineProviderBase subclasses in the other tests, which only mock out/override the parts of the interface which would otherwise raise NotImplemented. One exception: the LaunchMachine test stubs out get_zookeeper_machines separately, to avoid adding to the already-excessive complexity, but I think we have that covered from all angles already.

This branch now makes me think that find_zookeepers, SaveState, and LoadState (at least) should lose their independent existence and just become plain MachineProviderBase methods, but that feels like a job for another branch.

lp:~fwereade/pyjuju/provider-base updated
320. By William Reade

merge parent

321. By William Reade

merge parent

322. By William Reade

merge parent

323. By William Reade

merge parent

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

Very good branch, thanks!

+1, with [1] sorted.

[1]

+ # XXX something's a bit inelegant here; also see Bootstrap operation
+ launch = self.launch_machine_class(self)
+ return launch.run(machine_data=machine_data)

Indeed.. it doesn't look necessary. launch_machine_class can go away
entirely and the subclass can simply define its own start_machie method,
with the base raising NotImplemented. I bet it's going to be less code
in the end.

+ super(MachineProvider, self).__init__(
+ environment_name, config, EC2LaunchMachine)

Yeah, this really sounds wrong. Why EC2LaunchMachine and not
everything else? Let's drop it.

[2]

+ data = super(MachineProvider, self).get_serialization_data()
+ data.setdefault("access-key", os.environ.get("AWS_ACCESS_KEY_ID"))
+ data.setdefault("secret-key", os.environ.get("AWS_SECRET_ACCESS_KEY"))

Nice clean up!

[3]

+ """Retrieve an S3-backed C{FileStorage}."""
+ return FileStorage(self.s3, self.config["control-bucket"])

Good stuff too.

review: Approve
lp:~fwereade/pyjuju/provider-base updated
324. By William Reade

merge trunk

325. By William Reade

merge lp:~fwereade/ensemble/provider-base-launch-machine

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

[1] addressed in https://code.launchpad.net/~fwereade/ensemble/provider-base-launch-machine/+merge/71850 (now merged); includes a general rename of "bootstrap" -- in the narrow context of launching a machine which will run a zookeeper and a provisioning agent, as opposed to the broader start-ensemble-itself context -- to "master".

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

looks very nice +1

review: Approve
lp:~fwereade/pyjuju/provider-base updated
326. By William Reade

merge trunk

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'ensemble/control/shutdown.py'
--- ensemble/control/shutdown.py 2011-06-09 15:13:03 +0000
+++ ensemble/control/shutdown.py 2011-08-17 16:02:43 +0000
@@ -29,4 +29,4 @@
29 returnValue(None)29 returnValue(None)
30 options.log.info("Shutting down environment %r (type: %s)..." % (30 options.log.info("Shutting down environment %r (type: %s)..." % (
31 environment.name, environment.type))31 environment.name, environment.type))
32 yield provider.shutdown()32 yield provider.destroy_environment()
3333
=== modified file 'ensemble/control/tests/test_shutdown.py'
--- ensemble/control/tests/test_shutdown.py 2011-06-09 15:13:03 +0000
+++ ensemble/control/tests/test_shutdown.py 2011-08-17 16:02:43 +0000
@@ -89,7 +89,7 @@
89 return succeed(True)89 return succeed(True)
9090
91 provider = self.mocker.patch(MachineProvider)91 provider = self.mocker.patch(MachineProvider)
92 provider.shutdown()92 provider.destroy_environment()
93 self.mocker.call(track_shutdown_call, with_object=True)93 self.mocker.call(track_shutdown_call, with_object=True)
9494
95 self.setup_exit(0)95 self.setup_exit(0)
9696
=== added file 'ensemble/providers/common/base.py'
--- ensemble/providers/common/base.py 1970-01-01 00:00:00 +0000
+++ ensemble/providers/common/base.py 2011-08-17 16:02:43 +0000
@@ -0,0 +1,173 @@
1import copy
2from operator import itemgetter
3
4from ensemble.environment.errors import EnvironmentsConfigError
5from ensemble.providers.common.bootstrap import Bootstrap
6from ensemble.providers.common.findzookeepers import find_zookeepers
7from ensemble.providers.common.state import SaveState, LoadState
8from ensemble.providers.common.utils import get_user_authorized_keys
9
10
11class MachineProviderBase(object):
12 """Base class supplying common functionality for MachineProviders.
13
14 To write a working subclass, you will need to override the following
15 methods:
16
17 * connect
18 * get_file_storage
19 * start_machine
20 * get_machines
21 * shutdown_machines
22 * open_port
23 * close_port
24 * get_opened_ports
25
26 You may want to override the following methods, but you should be careful
27 to call MachineProviderBase's implementation (or be very sure you don't
28 need to:
29
30 * __init__
31 * get_serialization_data
32
33 You probably shouldn't override anything else.
34 """
35
36 def __init__(self, environment_name, config):
37 if ("authorized-keys-path" in config and
38 "authorized-keys" in config):
39 raise EnvironmentsConfigError(
40 "Environment config cannot define both authorized-keys "
41 "and authorized-keys-path. Pick one!")
42
43 self.environment_name = environment_name
44 self.config = config
45
46 def get_serialization_data(self):
47 """Get provider configuration suitable for serialization."""
48 data = copy.deepcopy(self.config)
49 data["authorized-keys"] = get_user_authorized_keys(data)
50 # Not relevant, on a remote system.
51 data.pop("authorized-keys-path", None)
52 return data
53
54 #================================================================
55 # Subclasses need to implement their own versions of everything
56 # in the following block
57
58 def connect(self, share=False):
59 """Connect to the zookeeper ensemble running in the machine provider.
60
61 @param share: Requests sharing of the connection with other clients
62 attempting to connect to the same provider, if that's feasible.
63
64 returns an open C{txzookeeper.client.ZookeeperClient} and a
65 C{ensemble.storage.connection.TunnelProtocol}
66 """
67 raise NotImplementedError()
68
69 def get_file_storage(self):
70 """Retrieve the provider C{FileStorage} abstraction."""
71 raise NotImplementedError()
72
73 def start_machine(self, machine_data, master=False):
74 """Start a machine in the provider.
75
76 @param machine_data: a dictionary of data to pass along to the newly
77 launched machine.
78
79 @param master: if True, machine will initialize the ensemble admin
80 and run a provisioning agent.
81 """
82 raise NotImplementedError()
83
84 def get_machines(self, instance_ids=()):
85 """List machines running in the provider.
86
87 @param instance_ids: ids of instances you want to get. Leave empty
88 to list all machines owned by this provider.
89
90 @return: a list of provider-specific ProviderMachines.
91
92 @raise: MachinesNotFound
93 """
94 raise NotImplementedError()
95
96 def shutdown_machines(self, requested_machines=()):
97 """Terminate machines associated with this provider.
98
99 @param requested_machines: list of machines to shut down; leave
100 empty to shut down all associated machines.
101 """
102 raise NotImplementedError()
103
104 def open_port(self, machine, machine_id, port, protocol="tcp"):
105 """Authorizes `port` using `protocol` for `machine`."""
106 raise NotImplementedError()
107
108 def close_port(self, machine, machine_id, port, protocol="tcp"):
109 """Revokes `port` using `protocol` for `machine`."""
110 raise NotImplementedError()
111
112 def get_opened_ports(self, machine, machine_id):
113 """Returns a set of open (port, protocol) pairs for `machine`."""
114 raise NotImplementedError()
115
116 #================================================================
117 # Subclasses will not generally need to override the methods in
118 # this block
119
120 def get_zookeeper_machines(self):
121 """Find running zookeeper instances.
122
123 @return: the first valid instance found as a single element list.
124
125 @raise: EnvironmentNotFound
126 """
127 return find_zookeepers(self)
128
129 def bootstrap(self):
130 """Bootstrap an ensemble server in the provider."""
131 return Bootstrap(self).run()
132
133 def get_machine(self, instance_id):
134 """Retrieve a provider machine by instance id.
135
136 @param instance_id: instance_id of the Machine you want to get.
137
138 @raise: MachinesNotFound
139 """
140 d = self.get_machines([instance_id])
141 d.addCallback(itemgetter(0))
142 return d
143
144 def shutdown_machine(self, machine):
145 """Terminate one machine associated with this provider.
146
147 @param machine: machine to shut down.
148
149 @raise: MachinesNotFound
150 """
151 d = self.shutdown_machines([machine])
152 d.addCallback(itemgetter(0))
153 return d
154
155 def destroy_environment(self):
156 """Clear ensemble state and terminate all associated machines"""
157 return self.shutdown_machines()
158
159 def save_state(self, state):
160 """Save state to the provider.
161
162 @param state
163 @type dict
164 """
165 return SaveState(self).run(state)
166
167 def load_state(self):
168 """Load state from the provider.
169
170 @return: a dictionary.
171 """
172 return LoadState(self).run()
173
0174
=== modified file 'ensemble/providers/common/bootstrap.py'
--- ensemble/providers/common/bootstrap.py 2011-08-04 09:50:57 +0000
+++ ensemble/providers/common/bootstrap.py 2011-08-17 16:02:43 +0000
@@ -41,11 +41,10 @@
41 return storage.put(_VERIFY_PATH, StringIO("storage is writable"))41 return storage.put(_VERIFY_PATH, StringIO("storage is writable"))
4242
43 def _cannot_write(self, failure):43 def _cannot_write(self, failure):
44 raise ProviderError("Bootstrap aborted: file storage not writable (%s)"44 raise ProviderError(
45 % str(failure.value))45 "Bootstrap aborted because file storage is not writable: %s"
46 % str(failure.value))
4647
47 def _launch_machine(self, unused):48 def _launch_machine(self, unused):
48 log.debug("Launching Ensemble bootstrap instance.")49 log.debug("Launching Ensemble bootstrap instance.")
49 launch_class = self._provider.launch_machine_class50 return self._provider.start_machine({"machine-id": "0"}, master=True)
50 launch = launch_class(self._provider, bootstrap=True)
51 return launch.run({"machine-id": "0"})
5251
=== modified file 'ensemble/providers/common/launch.py'
--- ensemble/providers/common/launch.py 2011-08-11 18:43:06 +0000
+++ ensemble/providers/common/launch.py 2011-08-17 16:02:43 +0000
@@ -56,7 +56,7 @@
56class LaunchMachine(object):56class LaunchMachine(object):
57 """Abstract class with generic instance-launching logic.57 """Abstract class with generic instance-launching logic.
5858
59 Constructing with bootstrap=True will cause the run method to59 Constructing with master=True will cause the run method to
60 construct a machine which is also running a zookeeper for the60 construct a machine which is also running a zookeeper for the
61 cluster, and a provisioning agent, as well as the usual machine61 cluster, and a provisioning agent, as well as the usual machine
62 agent.62 agent.
@@ -66,9 +66,9 @@
66 convenient to override C{get_machine_variables} as well.66 convenient to override C{get_machine_variables} as well.
67 """67 """
6868
69 def __init__(self, provider, bootstrap=False):69 def __init__(self, provider, master=False):
70 self._provider = provider70 self._provider = provider
71 self._bootstrap = bootstrap71 self._master = master
7272
73 @inlineCallbacks73 @inlineCallbacks
74 def run(self, machine_data):74 def run(self, machine_data):
@@ -86,8 +86,8 @@
86 machine_variables = yield self.get_machine_variables(machine_data)86 machine_variables = yield self.get_machine_variables(machine_data)
87 provider_machines = yield self.start_machine(87 provider_machines = yield self.start_machine(
88 machine_variables, machine_data["machine-id"])88 machine_variables, machine_data["machine-id"])
89 if self._bootstrap:89 if self._master:
90 yield self._on_bootstrap_launched(provider_machines)90 yield self._on_master_launched(provider_machines)
91 returnValue(provider_machines)91 returnValue(provider_machines)
9292
93 def start_machine(self, machine_variables, machine_id):93 def start_machine(self, machine_variables, machine_id):
@@ -128,7 +128,7 @@
128128
129 @inlineCallbacks129 @inlineCallbacks
130 def _get_zookeeper_hosts(self):130 def _get_zookeeper_hosts(self):
131 if self._bootstrap:131 if self._master:
132 returnValue("localhost:2181")132 returnValue("localhost:2181")
133 machines = yield self._provider.get_zookeeper_machines()133 machines = yield self._provider.get_zookeeper_machines()
134 hosts = [m.private_dns_name for m in machines]134 hosts = [m.private_dns_name for m in machines]
@@ -137,7 +137,7 @@
137137
138 def _get_packages(self):138 def _get_packages(self):
139 result = list(DEFAULT_PACKAGES)139 result = list(DEFAULT_PACKAGES)
140 if self._bootstrap:140 if self._master:
141 result.extend(BOOTSTRAP_PACKAGES)141 result.extend(BOOTSTRAP_PACKAGES)
142 return result142 return result
143143
@@ -150,7 +150,7 @@
150 "sudo mkdir -p /var/lib/ensemble",150 "sudo mkdir -p /var/lib/ensemble",
151 "sudo mkdir -p /var/log/ensemble"])151 "sudo mkdir -p /var/log/ensemble"])
152152
153 if self._bootstrap:153 if self._master:
154 launch_scripts.append(self._get_initialize_script())154 launch_scripts.append(self._get_initialize_script())
155155
156 # Every machine has its own agent.156 # Every machine has its own agent.
@@ -162,7 +162,7 @@
162 "--pidfile=/var/run/ensemble/machine-agent.pid")162 "--pidfile=/var/run/ensemble/machine-agent.pid")
163 launch_scripts.append(machine_agent_script_template % machine_data)163 launch_scripts.append(machine_agent_script_template % machine_data)
164164
165 if self._bootstrap:165 if self._master:
166 launch_scripts.append(_get_provision_agent_script(machine_data))166 launch_scripts.append(_get_provision_agent_script(machine_data))
167167
168 return launch_scripts168 return launch_scripts
@@ -185,7 +185,10 @@
185 return _get_initialize_script(185 return _get_initialize_script(
186 self.get_instance_id_command(), admin_secret)186 self.get_instance_id_command(), admin_secret)
187187
188 def _on_bootstrap_launched(self, machines):188 def _on_master_launched(self, machines):
189 # TODO this should be part of Bootstrap (and should really extend,
190 # rather than effectively replace, the result of
191 # self._provider.get_zookeeper_machines)
189 instance_ids = [m.instance_id for m in machines]192 instance_ids = [m.instance_id for m in machines]
190 d = self._provider.save_state({"zookeeper-instances": instance_ids})193 d = self._provider.save_state({"zookeeper-instances": instance_ids})
191 d.addCallback(lambda _: machines)194 d.addCallback(lambda _: machines)
192195
=== added file 'ensemble/providers/common/tests/test_base.py'
--- ensemble/providers/common/tests/test_base.py 1970-01-01 00:00:00 +0000
+++ ensemble/providers/common/tests/test_base.py 2011-08-17 16:02:43 +0000
@@ -0,0 +1,135 @@
1from twisted.internet.defer import fail, succeed
2
3from ensemble.environment.errors import EnvironmentsConfigError
4from ensemble.lib.testing import TestCase
5from ensemble.machine import ProviderMachine
6from ensemble.providers.common.base import MachineProviderBase
7
8
9class SomeError(Exception):
10 pass
11
12
13class DummyLaunchMachine(object):
14
15 def __init__(self, master=False):
16 self._master = master
17
18 def run(self, machine_data):
19 return succeed([ProviderMachine(machine_data["machine-id"])])
20
21
22class DummyProvider(MachineProviderBase):
23
24 def __init__(self, config=None):
25 super(DummyProvider, self).__init__("venus", config or {})
26
27
28class MachineProviderBaseTest(TestCase):
29
30 def test_init(self):
31 provider = DummyProvider({"some": "config"})
32 self.assertEquals(provider.environment_name, "venus")
33 self.assertEquals(provider.config, {"some": "config"})
34
35 def test_bad_config(self):
36 try:
37 DummyProvider({"authorized-keys": "foo",
38 "authorized-keys-path": "bar"})
39 except EnvironmentsConfigError as error:
40 expect = ("Environment config cannot define both authorized-keys "
41 "and authorized-keys-path. Pick one!")
42 self.assertEquals(str(error), expect)
43 else:
44 self.fail("Failed to detect bad config")
45
46 def test_get_serialization_data(self):
47 keys_path = self.makeFile("some-key")
48 provider = DummyProvider({"foo": {"bar": "baz"},
49 "authorized-keys-path": keys_path})
50 data = provider.get_serialization_data()
51 self.assertEquals(data, {"foo": {"bar": "baz"},
52 "authorized-keys": "some-key"})
53 data["foo"]["bar"] = "qux"
54 self.assertEquals(provider.config, {"foo": {"bar": "baz"},
55 "authorized-keys-path": keys_path})
56
57 def test_get_machine_error(self):
58 provider = DummyProvider()
59 provider.get_machines = self.mocker.mock()
60 provider.get_machines(["piffle"])
61 self.mocker.result(fail(SomeError()))
62 self.mocker.replay()
63
64 d = provider.get_machine("piffle")
65 self.assertFailure(d, SomeError)
66 return d
67
68 def test_get_machine_success(self):
69 provider = DummyProvider()
70 provider.get_machines = self.mocker.mock()
71 provider.get_machines(["piffle"])
72 machine = object()
73 self.mocker.result(succeed([machine]))
74 self.mocker.replay()
75
76 d = provider.get_machine("piffle")
77
78 def verify(result):
79 self.assertEquals(result, machine)
80 d.addCallback(verify)
81 return d
82
83 def test_shutdown_machine_error(self):
84 provider = DummyProvider()
85 provider.shutdown_machines = self.mocker.mock()
86 machine = object()
87 provider.shutdown_machines([machine])
88 self.mocker.result(fail(SomeError()))
89 self.mocker.replay()
90
91 d = provider.shutdown_machine(machine)
92 self.assertFailure(d, SomeError)
93 return d
94
95 def test_shutdown_machine_success(self):
96 provider = DummyProvider()
97 provider.shutdown_machines = self.mocker.mock()
98 machine = object()
99 provider.shutdown_machines([machine])
100 probably_the_same_machine = object()
101 self.mocker.result(succeed([probably_the_same_machine]))
102 self.mocker.replay()
103
104 d = provider.shutdown_machine(machine)
105
106 def verify(result):
107 self.assertEquals(result, probably_the_same_machine)
108 d.addCallback(verify)
109 return d
110
111 def test_destroy_environment_error(self):
112 provider = DummyProvider()
113 provider.shutdown_machines = self.mocker.mock()
114 provider.shutdown_machines()
115 self.mocker.result(fail(SomeError()))
116 self.mocker.replay()
117
118 d = provider.destroy_environment()
119 self.assertFailure(d, SomeError)
120 return d
121
122 def test_destroy_environment_success(self):
123 provider = DummyProvider()
124 provider.shutdown_machines = self.mocker.mock()
125 provider.shutdown_machines()
126 machines = object()
127 self.mocker.result(succeed(machines))
128 self.mocker.replay()
129
130 d = provider.destroy_environment()
131
132 def verify(result):
133 self.assertEquals(result, machines)
134 d.addCallback(verify)
135 return d
0136
=== modified file 'ensemble/providers/common/tests/test_findzookeepers.py'
--- ensemble/providers/common/tests/test_findzookeepers.py 2011-08-16 13:24:41 +0000
+++ ensemble/providers/common/tests/test_findzookeepers.py 2011-08-17 16:02:43 +0000
@@ -1,69 +1,79 @@
1from cStringIO import StringIO
2from yaml import dump
3
1from twisted.internet.defer import fail, succeed4from twisted.internet.defer import fail, succeed
25
3from ensemble.errors import EnvironmentNotFound, MachinesNotFound6from ensemble.errors import EnvironmentNotFound, MachinesNotFound
4from ensemble.lib.testing import TestCase7from ensemble.lib.testing import TestCase
5from ensemble.providers.common.findzookeepers import find_zookeepers8from ensemble.providers.common.base import MachineProviderBase
69
710
8class DummyProvider(object):11class SomeError(Exception):
912 pass
10 def __init__(self, state, get_machine):
11 self.state = state
12 self.get_machine = get_machine
13
14 def load_state(self):
15 return succeed(self.state)
1613
1714
18class FindZookeepersTest(TestCase):15class FindZookeepersTest(TestCase):
1916
20 def get_provider(self, state):17 def get_provider(self, state):
21 return DummyProvider(state, self.mocker.mock())18 test = self
19
20 class DummyStorage(object):
21
22 def get(self, path):
23 test.assertEquals(path, "provider-state")
24 return succeed(StringIO(dump(state)))
25
26 class DummyProvider(MachineProviderBase):
27
28 def __init__(self):
29 self.get_machines = test.mocker.mock()
30
31 def get_file_storage(self):
32 return DummyStorage()
33
34 return DummyProvider()
2235
23 def test_no_state(self):36 def test_no_state(self):
24 provider = self.get_provider(False)37 provider = self.get_provider(False)
25 d = find_zookeepers(provider)38 d = provider.get_zookeeper_machines()
26 self.assertFailure(d, EnvironmentNotFound)39 self.assertFailure(d, EnvironmentNotFound)
27 return d40 return d
2841
29 def test_empty_state(self):42 def test_empty_state(self):
30 provider = self.get_provider({})43 provider = self.get_provider({})
31 d = find_zookeepers(provider)44 d = provider.get_zookeeper_machines()
32 self.assertFailure(d, EnvironmentNotFound)45 self.assertFailure(d, EnvironmentNotFound)
33 return d46 return d
3447
35 def test_get_machine_error_aborts(self):48 def test_get_machine_error_aborts(self):
36 provider = self.get_provider(49 provider = self.get_provider(
37 {"zookeeper-instances": ["porter", "carter"]})50 {"zookeeper-instances": ["porter", "carter"]})
38 provider.get_machine("porter")51 provider.get_machines(["porter"])
39
40 class SomeError(Exception):
41 pass
42 self.mocker.result(fail(SomeError()))52 self.mocker.result(fail(SomeError()))
43 self.mocker.replay()53 self.mocker.replay()
4454
45 d = find_zookeepers(provider)55 d = provider.get_zookeeper_machines()
46 self.assertFailure(d, SomeError)56 self.assertFailure(d, SomeError)
47 return d57 return d
4858
49 def test_bad_machine(self):59 def test_bad_machine(self):
50 provider = self.get_provider({"zookeeper-instances": ["porter"]})60 provider = self.get_provider({"zookeeper-instances": ["porter"]})
51 provider.get_machine("porter")61 provider.get_machines(["porter"])
52 self.mocker.result(fail(MachinesNotFound(["porter"])))62 self.mocker.result(fail(MachinesNotFound(["porter"])))
53 self.mocker.replay()63 self.mocker.replay()
5464
55 d = find_zookeepers(provider)65 d = provider.get_zookeeper_machines()
56 self.assertFailure(d, EnvironmentNotFound)66 self.assertFailure(d, EnvironmentNotFound)
57 return d67 return d
5868
59 def test_good_machine(self):69 def test_good_machine(self):
60 provider = self.get_provider({"zookeeper-instances": ["porter"]})70 provider = self.get_provider({"zookeeper-instances": ["porter"]})
61 provider.get_machine("porter")71 provider.get_machines(["porter"])
62 machine = object()72 machine = object()
63 self.mocker.result(succeed(machine))73 self.mocker.result(succeed([machine]))
64 self.mocker.replay()74 self.mocker.replay()
6575
66 d = find_zookeepers(provider)76 d = provider.get_zookeeper_machines()
6777
68 def verify_machine(result):78 def verify_machine(result):
69 self.assertEquals(result, [machine])79 self.assertEquals(result, [machine])
@@ -73,19 +83,19 @@
73 def test_gets_all_good_machines(self):83 def test_gets_all_good_machines(self):
74 provider = self.get_provider(84 provider = self.get_provider(
75 {"zookeeper-instances": ["porter", "carter", "miller", "baker"]})85 {"zookeeper-instances": ["porter", "carter", "miller", "baker"]})
76 provider.get_machine("porter")86 provider.get_machines(["porter"])
77 self.mocker.result(fail(MachinesNotFound(["porter"])))87 self.mocker.result(fail(MachinesNotFound(["porter"])))
78 provider.get_machine("carter")88 provider.get_machines(["carter"])
79 carter = object()89 carter = object()
80 self.mocker.result(succeed(carter))90 self.mocker.result(succeed([carter]))
81 provider.get_machine("miller")91 provider.get_machines(["miller"])
82 self.mocker.result(fail(MachinesNotFound(["miller"])))92 self.mocker.result(fail(MachinesNotFound(["miller"])))
83 provider.get_machine("baker")93 provider.get_machines(["baker"])
84 baker = object()94 baker = object()
85 self.mocker.result(succeed(baker))95 self.mocker.result(succeed([baker]))
86 self.mocker.replay()96 self.mocker.replay()
8797
88 d = find_zookeepers(provider)98 d = provider.get_zookeeper_machines()
8999
90 def verify_machine(result):100 def verify_machine(result):
91 self.assertEquals(result, [carter, baker])101 self.assertEquals(result, [carter, baker])
92102
=== modified file 'ensemble/providers/common/tests/test_launch.py'
--- ensemble/providers/common/tests/test_launch.py 2011-08-10 18:16:51 +0000
+++ ensemble/providers/common/tests/test_launch.py 2011-08-17 16:02:43 +0000
@@ -1,11 +1,11 @@
1import logging1import logging
2import tempfile2import tempfile
33
4from twisted.internet.defer import fail, succeed4from twisted.internet.defer import fail, inlineCallbacks, succeed
55
6from ensemble.errors import EnvironmentNotFound, ProviderError6from ensemble.errors import EnvironmentNotFound, ProviderError
7from ensemble.lib.testing import TestCase7from ensemble.lib.testing import TestCase
8from ensemble.providers.common.bootstrap import Bootstrap8from ensemble.providers.common.base import MachineProviderBase
9from ensemble.providers.common.launch import (9from ensemble.providers.common.launch import (
10 BOOTSTRAP_PACKAGES, DEFAULT_PACKAGES, DEFAULT_REPOSITORIES, LaunchMachine)10 BOOTSTRAP_PACKAGES, DEFAULT_PACKAGES, DEFAULT_REPOSITORIES, LaunchMachine)
11from ensemble.providers.dummy import DummyMachine, FileStorage11from ensemble.providers.dummy import DummyMachine, FileStorage
@@ -18,7 +18,7 @@
18class DummyLaunchMachine(LaunchMachine):18class DummyLaunchMachine(LaunchMachine):
1919
20 def start_machine(self, variables, data):20 def start_machine(self, variables, data):
21 if self._bootstrap:21 if self._master:
22 name = "bootstrapped-instance-id"22 name = "bootstrapped-instance-id"
23 else:23 else:
24 name = "some-instance-id"24 name = "some-instance-id"
@@ -42,11 +42,15 @@
4242
43def get_provider(launch_class=DummyLaunchMachine, config=None, zookeeper=True,43def get_provider(launch_class=DummyLaunchMachine, config=None, zookeeper=True,
44 file_storage_class=WorkingFileStorage):44 file_storage_class=WorkingFileStorage):
45 class DummyProvider(object):45
46 config = {"authorized-keys": "abc"}46 class DummyProvider(MachineProviderBase):
47 launch_machine_class = launch_class47
48 def __init__(self, config):
49 super(DummyProvider, self).__init__("venus", config)
50 self._file_storage = file_storage_class()
4851
49 def get_zookeeper_machines(self):52 def get_zookeeper_machines(self):
53 # this is mocked out to avoid insane complexity
50 if isinstance(zookeeper, Exception):54 if isinstance(zookeeper, Exception):
51 return fail(zookeeper)55 return fail(zookeeper)
52 return succeed(56 return succeed(
@@ -54,21 +58,21 @@
54 private_dns_name="zookeeper.internal")])58 private_dns_name="zookeeper.internal")])
5559
56 def get_file_storage(self):60 def get_file_storage(self):
57 return file_storage_class()61 return self._file_storage
5862
59 def save_state(self, state):63 def start_machine(self, machine_data, master=False):
60 self.saved_state = state64 return launch_class(self, master).run(machine_data)
61 return succeed(None)65
6266 create_config = {"authorized-keys": "abc"}
63 if config is not None:67 if config is not None:
64 DummyProvider.config.update(config)68 create_config.update(config)
65 return DummyProvider()69 return DummyProvider(create_config)
6670
6771
68def get_launch(launch_class=DummyLaunchMachine, config=None,72def get_launch(launch_class=DummyLaunchMachine, config=None,
69 zookeeper=True, bootstrap=False):73 zookeeper=True, master=False):
70 provider = get_provider(launch_class, config, zookeeper)74 provider = get_provider(launch_class, config, zookeeper)
71 return launch_class(provider, bootstrap=bootstrap)75 return launch_class(provider, master=master)
7276
7377
74class LaunchMachineTest(TestCase):78class LaunchMachineTest(TestCase):
@@ -90,17 +94,14 @@
90 d.addCallback(self._verify_machines)94 d.addCallback(self._verify_machines)
91 return d95 return d
9296
97 @inlineCallbacks
93 def test_bootstrap_run(self):98 def test_bootstrap_run(self):
94 provider = get_provider(config={"admin-secret": "whatever"})99 provider = get_provider(config={"admin-secret": "whatever"})
95 launch = DummyLaunchMachine(provider, bootstrap=True)100 launch = DummyLaunchMachine(provider, master=True)
96 d = launch.run({"machine-id": "machine-32767"})101 yield launch.run({"machine-id": "machine-32767"})
97102 saved_state = yield provider.load_state()
98 def verify_state_saved(_):103 self.assertEquals(
99 self.assertEquals(104 saved_state, {"zookeeper-instances": ["bootstrapped-instance-id"]})
100 provider.saved_state,
101 {"zookeeper-instances": ["bootstrapped-instance-id"]})
102 d.addCallback(verify_state_saved)
103 return d
104105
105 def test_get_machine_variables_normal(self):106 def test_get_machine_variables_normal(self):
106 launch = get_launch()107 launch = get_launch()
@@ -152,8 +153,7 @@
152 return d153 return d
153154
154 def test_get_machine_variables_bootstrap(self):155 def test_get_machine_variables_bootstrap(self):
155 launch = get_launch(config={"admin-secret": "SEEKRIT"},156 launch = get_launch(config={"admin-secret": "SEEKRIT"}, master=True)
156 bootstrap=True)
157 d = launch.get_machine_variables({"machine-id": "machine-757"})157 d = launch.get_machine_variables({"machine-id": "machine-757"})
158158
159 def verify_vars(vars):159 def verify_vars(vars):
@@ -199,23 +199,21 @@
199 class BBQError(Exception):199 class BBQError(Exception):
200 pass200 pass
201 provider = get_provider(zookeeper=BBQError())201 provider = get_provider(zookeeper=BBQError())
202 bootstrap = Bootstrap(provider)202 d = provider.bootstrap()
203 d = bootstrap.run()
204 self.assertFailure(d, BBQError)203 self.assertFailure(d, BBQError)
205 return d204 return d
206205
207 def test_bootstrap_unwritable_storage(self):206 def test_bootstrap_unwritable_storage(self):
208 provider = get_provider(zookeeper=EnvironmentNotFound(),207 provider = get_provider(zookeeper=EnvironmentNotFound(),
209 file_storage_class=UnwritableFileStorage)208 file_storage_class=UnwritableFileStorage)
210 d = Bootstrap(provider).run()209 d = provider.bootstrap()
211 self.assertFailure(d, ProviderError)210 self.assertFailure(d, ProviderError)
212 return d211 return d
213212
214 def test_bootstrap_no_launch(self):213 def test_bootstrap_no_launch(self):
215 log = self.capture_logging("ensemble.common", level=logging.DEBUG)214 log = self.capture_logging("ensemble.common", level=logging.DEBUG)
216 provider = get_provider()215 provider = get_provider()
217 bootstrap = Bootstrap(provider)216 d = provider.bootstrap()
218 d = bootstrap.run()
219217
220 def verify_machines(machines):218 def verify_machines(machines):
221 (machine,) = machines219 (machine,) = machines
@@ -232,8 +230,7 @@
232 log = self.capture_logging("ensemble.common", level=logging.DEBUG)230 log = self.capture_logging("ensemble.common", level=logging.DEBUG)
233 provider = get_provider(config={"admin-secret": "SEEKRIT"},231 provider = get_provider(config={"admin-secret": "SEEKRIT"},
234 zookeeper=EnvironmentNotFound())232 zookeeper=EnvironmentNotFound())
235 bootstrap = Bootstrap(provider)233 d = provider.bootstrap()
236 d = bootstrap.run()
237234
238 def verify_machines(machines):235 def verify_machines(machines):
239 (machine,) = machines236 (machine,) = machines
240237
=== modified file 'ensemble/providers/common/tests/test_state.py'
--- ensemble/providers/common/tests/test_state.py 2011-07-26 10:45:08 +0000
+++ ensemble/providers/common/tests/test_state.py 2011-08-17 16:02:43 +0000
@@ -6,10 +6,11 @@
6from ensemble.errors import FileNotFound6from ensemble.errors import FileNotFound
7from ensemble.lib.mocker import MATCH7from ensemble.lib.mocker import MATCH
8from ensemble.lib.testing import TestCase8from ensemble.lib.testing import TestCase
9from ensemble.providers.common.base import MachineProviderBase
9from ensemble.providers.common.state import LoadState, SaveState10from ensemble.providers.common.state import LoadState, SaveState
1011
1112
12class DummyProvider(object):13class DummyProvider(MachineProviderBase):
1314
14 def __init__(self, get=None, put=None):15 def __init__(self, get=None, put=None):
15 self._get = get16 self._get = get
1617
=== modified file 'ensemble/providers/common/tests/test_utils.py'
--- ensemble/providers/common/tests/test_utils.py 2011-08-03 15:53:38 +0000
+++ ensemble/providers/common/tests/test_utils.py 2011-08-17 16:02:43 +0000
@@ -91,7 +91,7 @@
91 "OSError('Bad',)")91 "OSError('Bad',)")
9292
9393
94class FormatCloudIniTest(TestCase):94class FormatCloudInitTest(TestCase):
9595
96 def test_format_cloud_init_with_data(self):96 def test_format_cloud_init_with_data(self):
97 """The format cloud init creates a user-data cloud-init config file.97 """The format cloud init creates a user-data cloud-init config file.
9898
=== modified file 'ensemble/providers/dummy.py'
--- ensemble/providers/dummy.py 2011-08-12 19:28:37 +0000
+++ ensemble/providers/dummy.py 2011-08-17 16:02:43 +0000
@@ -2,17 +2,16 @@
2import os2import os
3import tempfile3import tempfile
44
5from twisted.internet.defer import inlineCallbacks, returnValue, succeed, fail
6
5from txzookeeper import ZookeeperClient7from txzookeeper import ZookeeperClient
6from twisted.internet.defer import succeed, fail
7
8
9log = logging.getLogger("ensemble.providers")
10
118
12from ensemble.errors import (9from ensemble.errors import (
13 EnvironmentNotFound, FileNotFound, MachinesNotFound, ProviderError)10 EnvironmentNotFound, FileNotFound, MachinesNotFound, ProviderError)
14from ensemble.machine import ProviderMachine11from ensemble.machine import ProviderMachine
1512
13log = logging.getLogger("ensemble.providers")
14
1615
17class DummyMachine(ProviderMachine):16class DummyMachine(ProviderMachine):
18 """Provider machine implementation specific to the dummy provider."""17 """Provider machine implementation specific to the dummy provider."""
@@ -59,7 +58,7 @@
59 return fail(MachinesNotFound(missing_instance_ids))58 return fail(MachinesNotFound(missing_instance_ids))
60 return succeed(machines)59 return succeed(machines)
6160
62 def start_machine(self, machine_data):61 def start_machine(self, machine_data, master=False):
63 """Start a machine in the provider."""62 """Start a machine in the provider."""
64 if not "machine-id" in machine_data:63 if not "machine-id" in machine_data:
65 return fail(ProviderError(64 return fail(ProviderError(
@@ -85,13 +84,16 @@
85 return succeed(self._machines[:1])84 return succeed(self._machines[:1])
86 return self.start_machine({"machine-id": 0})85 return self.start_machine({"machine-id": 0})
8786
88 def shutdown(self):87 @inlineCallbacks
88 def shutdown_machines(self, requested_machines=()):
89 """89 """
90 Terminate any machine resources associated to the provider.90 Terminate any machine resources associated to the provider.
91 """91 """
92 machines = self._machines92 instance_ids = [m.instance_id for m in requested_machines]
93 self._machines = []93 machines = yield self.get_machines(instance_ids)
94 return succeed(machines)94 for machine in machines:
95 self._machines.remove(machine)
96 returnValue(machines)
9597
96 def shutdown_machine(self, machine):98 def shutdown_machine(self, machine):
97 """Terminate the given machine"""99 """Terminate the given machine"""
@@ -103,6 +105,9 @@
103 return105 return
104 return fail(ProviderError("Machine not found %r" % machine))106 return fail(ProviderError("Machine not found %r" % machine))
105107
108 def destroy_environment(self):
109 return self.shutdown_machines()
110
106 def save_state(self, state):111 def save_state(self, state):
107 """Save the state to the provider."""112 """Save the state to the provider."""
108 self._state = state113 self._state = state
109114
=== modified file 'ensemble/providers/ec2/__init__.py'
--- ensemble/providers/ec2/__init__.py 2011-08-17 08:58:51 +0000
+++ ensemble/providers/ec2/__init__.py 2011-08-17 16:02:43 +0000
@@ -1,5 +1,3 @@
1import copy
2from operator import itemgetter
3import os1import os
4import re2import re
53
@@ -8,14 +6,10 @@
8from txaws.ec2.exception import EC2Error6from txaws.ec2.exception import EC2Error
9from txaws.service import AWSServiceRegion7from txaws.service import AWSServiceRegion
108
11from ensemble.environment.errors import EnvironmentsConfigError
12from ensemble.errors import (9from ensemble.errors import (
13 MachinesNotFound, ProviderError, ProviderInteractionError)10 MachinesNotFound, ProviderError, ProviderInteractionError)
14from ensemble.providers.common.bootstrap import Bootstrap11from ensemble.providers.common.base import MachineProviderBase
15from ensemble.providers.common.findzookeepers import find_zookeepers12from ensemble.providers.common.utils import convert_unknown_error
16from ensemble.providers.common.state import SaveState, LoadState
17from ensemble.providers.common.utils import (
18 convert_unknown_error, get_user_authorized_keys)
1913
20from .connect import EC2Connect14from .connect import EC2Connect
21from .files import FileStorage15from .files import FileStorage
@@ -26,13 +20,10 @@
26from .utils import get_region_uri20from .utils import get_region_uri
2721
2822
29class MachineProvider(object):23class MachineProvider(MachineProviderBase):
30
31 launch_machine_class = EC2LaunchMachine
3224
33 def __init__(self, environment_name, config):25 def __init__(self, environment_name, config):
34 self.environment_name = environment_name26 super(MachineProvider, self).__init__(environment_name, config)
35 self.config = config
3627
37 if not config.get("ec2-uri"):28 if not config.get("ec2-uri"):
38 ec2_uri = get_region_uri(config.get("region", "us-east-1"))29 ec2_uri = get_region_uri(config.get("region", "us-east-1"))
@@ -47,27 +38,14 @@
47 self.s3 = self._service.get_s3_client()38 self.s3 = self._service.get_s3_client()
48 self.ec2 = self._service.get_ec2_client()39 self.ec2 = self._service.get_ec2_client()
4940
50 if ("authorized-keys-path" in config and
51 "authorized-keys" in config):
52 raise EnvironmentsConfigError(
53 "Environment config cannot define both authorized-keys "
54 "and authorized-keys-path. Pick one!")
55
56 def get_serialization_data(self):41 def get_serialization_data(self):
57 """Return a dictionary serialization of the provider configuration.42 """Get provider configuration suitable for serialization.
5843
59 Additionally this extracts crednetial information from the environment.44 Also extracts credential information from the environment.
60 """45 """
61 data = copy.deepcopy(self.config)46 data = super(MachineProvider, self).get_serialization_data()
62 data["secret-key"] = self.config.get(47 data.setdefault("access-key", os.environ.get("AWS_ACCESS_KEY_ID"))
63 "secret-key", os.environ.get("AWS_SECRET_ACCESS_KEY"))48 data.setdefault("secret-key", os.environ.get("AWS_SECRET_ACCESS_KEY"))
64 data["access-key"] = self.config.get(
65 "access-key", os.environ.get("AWS_ACCESS_KEY_ID"))
66 data["authorized-keys"] = get_user_authorized_keys(data)
67
68 # Not relevant, on a remote system.
69 data.pop("authorized-keys-path", None)
70
71 return data49 return data
7250
73 def _run_operation(self, operation, *args, **kw):51 def _run_operation(self, operation, *args, **kw):
@@ -88,9 +66,19 @@
88 return self._run_operation(connect, share=share)66 return self._run_operation(connect, share=share)
8967
90 def get_file_storage(self):68 def get_file_storage(self):
91 """Retrieve the provider C{FileStorage} abstraction."""69 """Retrieve an S3-backed C{FileStorage}."""
92 file_storage = FileStorage(self.s3, self.config["control-bucket"])70 return FileStorage(self.s3, self.config["control-bucket"])
93 return file_storage71
72 def start_machine(self, machine_data, master=False):
73 """Start a machine in the provider.
74
75 @param machine_data: a dictionary of data to pass along to the newly
76 launched machine.
77
78 @param master: if True, machine will initialize the ensemble admin
79 and run a provisioning agent.
80 """
81 return EC2LaunchMachine(self, master).run(machine_data)
9482
95 @inlineCallbacks83 @inlineCallbacks
96 def get_machines(self, instance_ids=()):84 def get_machines(self, instance_ids=()):
@@ -132,33 +120,8 @@
132 raise MachinesNotFound(missing)120 raise MachinesNotFound(missing)
133 returnValue(machines)121 returnValue(machines)
134122
135 def get_machine(self, instance_id):
136 """Retrieve a provider machine by instance id."""
137 d = self.get_machines([instance_id])
138 d.addCallback(itemgetter(0))
139 return d
140
141 def start_machine(self, machine_data):
142 """Start a machine in the provider.
143
144 @param machine_data a dictionary of data to pass along to the newly
145 launched machine.
146 @type dict
147 """
148 launch = EC2LaunchMachine(self)
149 return launch.run(machine_data=machine_data)
150
151 def bootstrap(self):
152 """Bootstrap an ensemble server in the provider."""
153 bootstrap = Bootstrap(self)
154 return bootstrap.run()
155
156 def shutdown_machine(self, machine):
157 """Stop a machine in the provider."""
158 return self.shutdown([machine])
159
160 @inlineCallbacks123 @inlineCallbacks
161 def shutdown(self, requested_machines=()):124 def shutdown_machines(self, requested_machines=()):
162 """Terminate machine resources associated with this provider."""125 """Terminate machine resources associated with this provider."""
163 for machine in requested_machines:126 for machine in requested_machines:
164 if not isinstance(machine, EC2ProviderMachine):127 if not isinstance(machine, EC2ProviderMachine):
@@ -170,30 +133,7 @@
170 if killable_machines:133 if killable_machines:
171 killable_ids = [m.instance_id for m in killable_machines]134 killable_ids = [m.instance_id for m in killable_machines]
172 yield self.ec2.terminate_instances(*killable_ids)135 yield self.ec2.terminate_instances(*killable_ids)
173136 returnValue(killable_machines)
174 def save_state(self, state):
175 """Save state to the provider.
176
177 @param state
178 @type dict
179 """
180 return SaveState(self).run(state)
181
182 def load_state(self):
183 """Load state from the provider.
184
185 @return: a dictionary.
186 """
187 return LoadState(self).run()
188
189 def get_zookeeper_machines(self):
190 """Find running zookeeper instances.
191
192 @return: the first valid instance found as a single element list.
193
194 @raise: EnvironmentNotFound
195 """
196 return find_zookeepers(self)
197137
198 def open_port(self, machine, machine_id, port, protocol="tcp"):138 def open_port(self, machine, machine_id, port, protocol="tcp"):
199 """Authorizes `port` using `protocol` on EC2 for `machine`."""139 """Authorizes `port` using `protocol` on EC2 for `machine`."""
200140
=== modified file 'ensemble/providers/ec2/tests/test_provider.py'
--- ensemble/providers/ec2/tests/test_provider.py 2011-05-03 08:54:13 +0000
+++ ensemble/providers/ec2/tests/test_provider.py 2011-08-17 16:02:43 +0000
@@ -115,14 +115,16 @@
115 serialized.pop("authorized-keys", None)115 serialized.pop("authorized-keys", None)
116 self.assertEqual(config, serialized)116 self.assertEqual(config, serialized)
117117
118class FailCreateTest(TestCase):
119
118 def test_conflicting_authorized_keys_options(self):120 def test_conflicting_authorized_keys_options(self):
119 """121 """
120 We can't handle two different authorized keys options, so deny122 We can't handle two different authorized keys options, so deny
121 constructing an environment that way.123 constructing an environment that way.
122 """124 """
123 config = self.get_config()125 config = {}
124 config["authorized-keys"] = "File content"126 config["authorized-keys"] = "File content"
125 config["authorized-keys-path"] = "File path"127 config["authorized-keys-path"] = "File path"
126 error = self.assertRaises(EnvironmentsConfigError,128 error = self.assertRaises(EnvironmentsConfigError,
127 MachineProvider, self.env_name, config)129 MachineProvider, "some-env-name", config)
128 self.assertIn("authorized-keys", str(error))130 self.assertIn("authorized-keys", str(error))
129131
=== modified file 'ensemble/providers/ec2/tests/test_shutdown.py'
--- ensemble/providers/ec2/tests/test_shutdown.py 2011-08-12 21:01:27 +0000
+++ ensemble/providers/ec2/tests/test_shutdown.py 2011-08-17 16:02:43 +0000
@@ -11,7 +11,7 @@
1111
12class EC2ShutdownMachineTest(EC2TestMixin, TestCase):12class EC2ShutdownMachineTest(EC2TestMixin, TestCase):
1313
14 def test_shutdown(self):14 def test_shutdown_machine(self):
15 instance = self.get_instance("i-foobar")15 instance = self.get_instance("i-foobar")
16 self.ec2.describe_instances("i-foobar")16 self.ec2.describe_instances("i-foobar")
17 self.mocker.result(succeed([instance]))17 self.mocker.result(succeed([instance]))
@@ -21,9 +21,15 @@
2121
22 machine = EC2ProviderMachine("i-foobar")22 machine = EC2ProviderMachine("i-foobar")
23 provider = self.get_provider()23 provider = self.get_provider()
24 return provider.shutdown_machine(machine)24 d = provider.shutdown_machine(machine)
2525
26 def test_shutdown_invalid_group(self):26 def verify(machine):
27 self.assertTrue(isinstance(machine, EC2ProviderMachine))
28 self.assertEquals(machine.instance_id, "i-foobar")
29 d.addCallback(verify)
30 return d
31
32 def test_shutdown_machine_invalid_group(self):
27 """33 """
28 Attempting to shutdown a machine that does not belong to this34 Attempting to shutdown a machine that does not belong to this
29 provider instance raises an exception.35 provider instance raises an exception.
@@ -43,7 +49,7 @@
43 d.addCallback(verify)49 d.addCallback(verify)
44 return d50 return d
4551
46 def test_shutdown_invalid_machine(self):52 def test_shutdown_machine_invalid_machine(self):
47 """53 """
48 Attempting to shutdown a machine that from a different provider54 Attempting to shutdown a machine that from a different provider
49 type will raise a syntaxerror.55 type will raise a syntaxerror.
@@ -61,12 +67,81 @@
61 d.addCallback(check_error)67 d.addCallback(check_error)
62 return d68 return d
6369
6470 def test_shutdown_machines_all(self):
65class EC2ShutdownTest(EC2TestMixin, TestCase):71 self.ec2.describe_instances()
6672 self.mocker.result(succeed([
67 def test_shutdown(self):73 self.get_instance("i-amkillable"),
74 self.get_instance("i-amdead", "shutting-down"),
75 self.get_instance("i-amalien", groups=["other"]),
76 self.get_instance("i-amkillabletoo")]))
77 self.ec2.terminate_instances("i-amkillable", "i-amkillabletoo")
78 self.mocker.result(succeed([
79 ("i-amkillable", "running", "shutting-down"),
80 ("i-amkillabletoo", "running", "shutting-down")]))
81 self.mocker.replay()
82
83 provider = self.get_provider()
84 d = provider.shutdown_machines()
85
86 def verify(result):
87 (machine_1, machine_2) = result
88 self.assertTrue(isinstance(machine_1, EC2ProviderMachine))
89 self.assertEquals(machine_1.instance_id, "i-amkillable")
90 self.assertTrue(isinstance(machine_2, EC2ProviderMachine))
91 self.assertEquals(machine_2.instance_id, "i-amkillabletoo")
92 d.addCallback(verify)
93 return d
94
95 def test_shutdown_machines_some_invalid(self):
96 self.ec2.describe_instances("i-amkillable", "i-amdead")
97 self.mocker.result(succeed([
98 self.get_instance("i-amkillable"),
99 self.get_instance("i-amdead", "shutting-down")]))
100 self.mocker.replay()
101
102 provider = self.get_provider()
103 d = provider.shutdown_machines([
104 EC2ProviderMachine("i-amkillable"),
105 EC2ProviderMachine("i-amdead")])
106 self.failUnlessFailure(d, MachinesNotFound)
107
108 def verify(error):
109 self.assertEquals(str(error),
110 "Cannot find machine: i-amdead")
111 d.addCallback(verify)
112 return d
113
114 def test_shutdown_machines_some_success(self):
115 self.ec2.describe_instances("i-amkillable", "i-amkillabletoo")
116 self.mocker.result(succeed([
117 self.get_instance("i-amkillable"),
118 self.get_instance("i-amkillabletoo")]))
119 self.ec2.terminate_instances("i-amkillable", "i-amkillabletoo")
120 self.mocker.result(succeed([
121 ("i-amkillable", "running", "shutting-down"),
122 ("i-amkillabletoo", "running", "shutting-down")]))
123 self.mocker.replay()
124
125 provider = self.get_provider()
126 d = provider.shutdown_machines([
127 EC2ProviderMachine("i-amkillable"),
128 EC2ProviderMachine("i-amkillabletoo")])
129
130 def verify(result):
131 (machine_1, machine_2) = result
132 self.assertTrue(isinstance(machine_1, EC2ProviderMachine))
133 self.assertEquals(machine_1.instance_id, "i-amkillable")
134 self.assertTrue(isinstance(machine_2, EC2ProviderMachine))
135 self.assertEquals(machine_2.instance_id, "i-amkillabletoo")
136 d.addCallback(verify)
137 return d
138
139
140class EC2DestroyTest(EC2TestMixin, TestCase):
141
142 def test_destroy_environment(self):
68 """143 """
69 The shutdown operation terminates all running and pending144 The destroy_environment operation terminates all running and pending
70 instances associated to the C{MachineProvider} instance.145 instances associated to the C{MachineProvider} instance.
71 """146 """
72 instances = [self.get_instance("i-canbekilled"),147 instances = [self.get_instance("i-canbekilled"),
@@ -83,16 +158,30 @@
83 self.mocker.replay()158 self.mocker.replay()
84159
85 provider = self.get_provider()160 provider = self.get_provider()
86 return provider.shutdown()161 d = provider.destroy_environment()
162
163 def verify(result):
164 (machine_1, machine_2) = result
165 self.assertTrue(isinstance(machine_1, EC2ProviderMachine))
166 self.assertEquals(machine_1.instance_id, "i-canbekilled")
167 self.assertTrue(isinstance(machine_2, EC2ProviderMachine))
168 self.assertEquals(machine_2.instance_id, "i-canbekilledtoo")
169 d.addCallback(verify)
170 return d
87171
88 def test_shutdown_no_instances(self):172 def test_shutdown_no_instances(self):
89 """173 """
90 If there are no instances to shutdown, running the shutdown174 If there are no instances to shutdown, running the destroy_environment
91 operation returns None.175 operation does nothing.
92 """176 """
93 self.ec2.describe_instances()177 self.ec2.describe_instances()
94 self.mocker.result(succeed([]))178 self.mocker.result(succeed([]))
95 self.mocker.replay()179 self.mocker.replay()
96180
97 provider = self.get_provider()181 provider = self.get_provider()
98 return provider.shutdown()182 d = provider.destroy_environment()
183
184 def verify(result):
185 self.assertEquals(result, None)
186 d.addCallback(verify)
187 return d
99188
=== modified file 'ensemble/providers/orchestra/__init__.py'
--- ensemble/providers/orchestra/__init__.py 2011-08-12 20:31:16 +0000
+++ ensemble/providers/orchestra/__init__.py 2011-08-17 16:02:43 +0000
@@ -1,13 +1,6 @@
1import copy
2from operator import itemgetter
3
4from twisted.internet.defer import inlineCallbacks, returnValue1from twisted.internet.defer import inlineCallbacks, returnValue
52
6from ensemble.environment.errors import EnvironmentsConfigError3from ensemble.providers.common.base import MachineProviderBase
7from ensemble.providers.common.bootstrap import Bootstrap
8from ensemble.providers.common.findzookeepers import find_zookeepers
9from ensemble.providers.common.state import SaveState, LoadState
10from ensemble.providers.common.utils import get_user_authorized_keys
114
12from .cobbler import CobblerClient5from .cobbler import CobblerClient
13from .files import FileStorage6from .files import FileStorage
@@ -15,43 +8,12 @@
15from .machine import machine_from_dict8from .machine import machine_from_dict
169
1710
18class MachineProvider(object):11class MachineProvider(MachineProviderBase):
19
20 launch_machine_class = OrchestraLaunchMachine
2112
22 def __init__(self, environment_name, config):13 def __init__(self, environment_name, config):
23 self.environment_name = environment_name14 super(MachineProvider, self).__init__(environment_name, config)
24 self.config = config
25 self.cobbler = CobblerClient(config)15 self.cobbler = CobblerClient(config)
2616
27 if ("authorized-keys-path" in config and
28 "authorized-keys" in config):
29 raise EnvironmentsConfigError(
30 "Environment config cannot define both authorized-keys "
31 "and authorized-keys-path. Pick one!")
32
33 def get_serialization_data(self):
34 """Return a dictionary serialization of the provider configuration.
35
36 Additionally this extracts credential information from the environment.
37 """
38 data = copy.deepcopy(self.config)
39 data["authorized-keys"] = get_user_authorized_keys(data)
40 data.pop("authorized-keys-path", None)
41 return data
42
43 def connect(self, share=False):
44 """
45 Connect to the zookeeper ensemble running in the machine provider.
46
47 @param share: Requests sharing of the connection with other clients
48 attempting to connect to the same provider, if that's feasible.
49
50 returns an open C{txzookeeper.client.ZookeeperClient} and a
51 C{ensemble.storage.connection.TunnelProtocol}
52 """
53 raise NotImplementedError()
54
55 def get_file_storage(self):17 def get_file_storage(self):
56 """Retrieve the provider C{FileStorage} abstraction."""18 """Retrieve the provider C{FileStorage} abstraction."""
57 if "storage-url" not in self.config:19 if "storage-url" not in self.config:
@@ -59,6 +21,17 @@
59 "http://%(orchestra-server)s/webdav" % self.config)21 "http://%(orchestra-server)s/webdav" % self.config)
60 return FileStorage(self.config["storage-url"])22 return FileStorage(self.config["storage-url"])
6123
24 def start_machine(self, machine_data, master=False):
25 """Start a machine in the provider.
26
27 @param machine_data: a dictionary of data to pass along to the newly
28 launched machine.
29
30 @param master: if True, machine will initialize the ensemble admin
31 and run a provisioning agent.
32 """
33 return OrchestraLaunchMachine(self, master).run(machine_data)
34
62 @inlineCallbacks35 @inlineCallbacks
63 def get_machines(self, instance_ids=()):36 def get_machines(self, instance_ids=()):
64 """List machines running in the provider.37 """List machines running in the provider.
@@ -72,82 +45,3 @@
72 """45 """
73 instances = yield self.cobbler.describe_systems(*instance_ids)46 instances = yield self.cobbler.describe_systems(*instance_ids)
74 returnValue([machine_from_dict(i) for i in instances])47 returnValue([machine_from_dict(i) for i in instances])
75
76 def get_machine(self, instance_id):
77 """Retrieve a provider machine by instance id. """
78 d = self.get_machines([instance_id])
79 d.addCallback(itemgetter(0))
80 return d
81
82 def start_machine(self, machine_data):
83 """
84 Start a machine in the provider.
85
86 @param machine_data a dictionary of data to pass along to the newly
87 launched machine.
88 @type dict
89 """
90 launch = OrchestraLaunchMachine(self)
91 return launch.run(machine_data)
92
93 def bootstrap(self):
94 """
95 Bootstrap an ensemble server in the provider.
96 """
97 bootstrap = Bootstrap(self)
98 return bootstrap.run()
99
100 def shutdown_machine(self):
101 """
102 Stop a machine in the provider.
103 """
104 raise NotImplementedError()
105
106 def shutdown(self, requested_machines=()):
107 """Terminate machine resources associated with this provider."""
108 raise NotImplementedError()
109
110 def save_state(self, state):
111 """Save state to the provider.
112
113 @param state
114 @type dict
115 """
116 return SaveState(self).run(state)
117
118 def load_state(self):
119 """Load state from the provider.
120
121 @return: a dictionary.
122 """
123 return LoadState(self).run()
124
125 def get_zookeeper_machines(self):
126 """Find running zookeeper instances.
127
128 @return: the first valid instance found as a single element list.
129
130 @raise: EnvironmentNotFound
131 """
132 return find_zookeepers(self)
133
134 def open_port(self, machine, port, protocol="tcp"):
135 """Expose port to the environment.
136
137 Approximate equivalent of ec2-authorize-group
138 """
139 raise NotImplementedError()
140
141 def close_port(self, machine, port, protocol="tcp"):
142 """Close opened port
143
144 Approximate equivalent of ec2-revoke-group
145 """
146 raise NotImplementedError()
147
148 def get_opened_ports(self, machine):
149 """List ports currently exposed to the environment.
150
151 Approximate equivalent of ec2-describe-group
152 """
153 raise NotImplementedError()
15448
=== modified file 'ensemble/providers/tests/test_dummy.py'
--- ensemble/providers/tests/test_dummy.py 2011-08-11 05:59:30 +0000
+++ ensemble/providers/tests/test_dummy.py 2011-08-17 16:02:43 +0000
@@ -67,14 +67,14 @@
67 return self.assertFailure(d, ProviderError)67 return self.assertFailure(d, ProviderError)
6868
69 @inlineCallbacks69 @inlineCallbacks
70 def test_shutdown(self):70 def test_destroy_environment(self):
71 result = yield self.provider.shutdown()71 result = yield self.provider.destroy_environment()
72 self.assertEqual(result, [])72 self.assertEqual(result, [])
7373
74 @inlineCallbacks74 @inlineCallbacks
75 def test_shutdown_returns_machines(self):75 def test_destroy_environment_returns_machines(self):
76 yield self.provider.bootstrap()76 yield self.provider.bootstrap()
77 result = yield self.provider.shutdown()77 result = yield self.provider.destroy_environment()
78 self.assertEqual(len(result), 1)78 self.assertEqual(len(result), 1)
79 self.assertTrue(isinstance(result[0], ProviderMachine))79 self.assertTrue(isinstance(result[0], ProviderMachine))
8080

Subscribers

People subscribed via source and target branches

to status/vote changes: