Merge lp:~fwereade/pyjuju/cloud-init-class-used into lp:pyjuju

Proposed by William Reade
Status: Merged
Approved by: Gustavo Niemeyer
Approved revision: 341
Merged at revision: 342
Proposed branch: lp:~fwereade/pyjuju/cloud-init-class-used
Merge into: lp:pyjuju
Prerequisite: lp:~fwereade/pyjuju/cloud-init-class-prepare
Diff against target: 2107 lines (+514/-880)
28 files modified
ensemble/providers/common/base.py (+3/-4)
ensemble/providers/common/cloudinit.py (+12/-13)
ensemble/providers/common/launch.py (+46/-170)
ensemble/providers/common/tests/data/cloud_init_bootstrap (+1/-2)
ensemble/providers/common/tests/data/cloud_init_bootstrap_zookeepers (+1/-2)
ensemble/providers/common/tests/data/cloud_init_branch (+1/-2)
ensemble/providers/common/tests/data/cloud_init_distro (+1/-2)
ensemble/providers/common/tests/data/cloud_init_normal (+1/-2)
ensemble/providers/common/tests/test_bootstrap.py (+102/-0)
ensemble/providers/common/tests/test_cloudinit.py (+2/-2)
ensemble/providers/common/tests/test_launch.py (+61/-224)
ensemble/providers/common/tests/test_utils.py (+1/-2)
ensemble/providers/common/utils.py (+3/-3)
ensemble/providers/ec2/__init__.py (+10/-5)
ensemble/providers/ec2/launch.py (+32/-53)
ensemble/providers/ec2/tests/common.py (+2/-4)
ensemble/providers/ec2/tests/data/bootstrap_cloud_init (+18/-0)
ensemble/providers/ec2/tests/data/launch_cloud_init (+15/-0)
ensemble/providers/ec2/tests/test_bootstrap.py (+16/-51)
ensemble/providers/ec2/tests/test_launch.py (+79/-114)
ensemble/providers/ec2/tests/test_utils.py (+40/-106)
ensemble/providers/ec2/utils.py (+17/-69)
ensemble/providers/orchestra/__init__.py (+9/-6)
ensemble/providers/orchestra/launch.py (+23/-34)
ensemble/providers/orchestra/tests/common.py (+2/-1)
ensemble/providers/orchestra/tests/data/bootstrap_late_command (+5/-5)
ensemble/providers/orchestra/tests/data/launch_late_command (+5/-4)
ensemble/providers/orchestra/tests/test_launch.py (+6/-0)
To merge this branch: bzr merge lp:~fwereade/pyjuju/cloud-init-class-used
Reviewer Review Type Date Requested Status
Kapil Thangavelu (community) Approve
Gustavo Niemeyer Approve
Review via email: mp+73634@code.launchpad.net

Description of the change

Another attempt at a cleaner cloud-init/machine-data mechanism. Comments on https://code.launchpad.net/~fwereade/ensemble/cloud-init-class-replace/+merge/72399 still apply, except that the unbugged prereq (whose intent has not changed) is now lp:~fwereade/ensemble/cloud-init-class-prepare; the only thing to add is that I *know* the docstrings are a mess, and I'm just about to start a new stacked branch to bring them all in line with the agreed standard.

To post a comment you must log in.
Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

This looks _very_ nice. Thanks William!

+1, with the following sorted:

[1]

+ self._zookeeper_names = [m.private_dns_name for m in machines]
(...)
+ def _zookeeper_hosts(self):
+ all_names = self._zookeeper_names[:]

The branch has two different names for the same data in different
formats. The better approach is the opposite: the same name with
different actions that give a hint about the undelying operation.

I suggest using "zookeeper_hosts" for both, and naming the last
function as join_zookeeper_hosts or something similar.

[2]

+ def _validate(self):
+ missing = []

Very nice logic there.

[3]

+ def _create_cloud_init(self, machine_id, zookeepers):
+ config = self._provider.config
+ cloud_init = CloudInit()
+ cloud_init.add_ssh_key(get_user_authorized_keys(config))
+ cloud_init.set_machine_id(machine_id)
+ cloud_init.set_zookeeper_machines(zookeepers)
+ if config.get("ensemble-branch"):
+ cloud_init.set_ensemble_source(config["ensemble-branch"])
+ if self._master:
+ cloud_init.enable_bootstrap()
+ cloud_init.set_zookeeper_secret(config["admin-secret"])
+ return cloud_init

Beautiful! :-)

[4]

+ instance_type = self._provider.config.get(
+ "default-instance-type", "m1.small")
+ image_id = yield get_image_id(self._provider.config, self._constraints)
+ security_groups = yield self._ensure_groups(machine_id)
+
+ instances = yield self._provider.ec2.run_instances(
+ image_id=image_id,
+ instance_type=instance_type,
+ min_count=1,
+ max_count=1,
+ security_groups=security_groups,
+ user_data=user_data)

Very clean as well, thanks!

[5]

+def get_image_id(config, constraints):
(...)
+_KWARG_NAMES = {
+ "ubuntu_release": "ubuntu_release_name",
+ "architecture": "arch",
+ "daily": "daily",
+ "persistent_storage": "ebs"}
(...)
+def _convert_constraints(constraints):
+ kwargs = {}
+ for (constraint_name, kwarg_name) in _KWARG_NAMES.items():
+ if constraint_name in constraints:
+ kwargs[kwarg_name] = constraints[constraint_name]
+ return kwargs
(...)
+ kwargs = _convert_constraints(constraints)

Why are we doing this? These names in the right-hand-side of the
_KWARG_NAMES are keyword arguments of get_current_ami, which is a function
on the same file (!). The left-hand-side ones are not even used in
any sensible way in our current implementation.

Let's just unify the naming and avoid any conversions entirely. Feel
free to pick sensible ones.

[6]

Again, this looks very nice. Please just make sure you merge trunk into
this, and do one final real-world test on EC2 before committing back
into trunk.

review: Approve
339. By William Reade

merge parent

340. By William Reade

renaming in CloudInit

341. By William Reade

get_current_ami now uses same keys as machine_data constraints dict

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

Thanks for the changes William.

[7]

+# XXX ideally should come from latest available or client release name.

FWIW, it should come from the formula Ubuntu series instead.

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

Please go ahead and merge this William. It's been in review for 2 weeks already.

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

looks great, bye-bye anonymous blobs. some comments.

[0]

- @param machine_data: a dictionary of data to pass along to the newly
- launched machine.
+ `machine_data`: a dictionary describing the newly launched machine.

'describing the machine to be launched.'

[1]

+ def set_provider_type(self, type_):
+ self._provider_type = type_

type isn't a keyword

[2]
+ def set_machine_id(self, id):
+ self._machine_id = id
+
+ def set_instance_id(self, expr):
+ self._instance_id = expr

+ if self._zookeeper:
+ require("_instance_id", "set_instance_id")
+ require("_zookeeper_secret", "set_zookeeper_secret")

what's the distinction between the two variables. instance id comes from the provider
implementation as a consequence of actually running the machine, its an output value, not
an input value to cloud init afaics.

its apparent a little bit latter into the diff, this is actually a shell command for retrieval
of instance id.. the naming should be reflective of that. perhaps set_instance_id_accessor

what's the case for the instance id not being set? is that just for testing ease, if so it should probably be commented as such.

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

> looks great, bye-bye anonymous blobs. some comments.
>
> [0]
>
> - @param machine_data: a dictionary of data to pass along to the newly
> - launched machine.
> + `machine_data`: a dictionary describing the newly launched machine.
>
> 'describing the machine to be launched.'

Thanks

> [1]
>
> + def set_provider_type(self, type_):
> + self._provider_type = type_
>
> type isn't a keyword

But it is a builtin, and I prefer not to shadow them unless I absopositively have to.

> [2]
> + def set_machine_id(self, id):
> + self._machine_id = id
> +
> + def set_instance_id(self, expr):
> + self._instance_id = expr
>
> + if self._zookeeper:
> + require("_instance_id", "set_instance_id")
> + require("_zookeeper_secret", "set_zookeeper_secret")
>
> what's the distinction between the two variables. instance id comes from the
> provider
> implementation as a consequence of actually running the machine, its an output
> value, not
> an input value to cloud init afaics.
>
> its apparent a little bit latter into the diff, this is actually a shell
> command for retrieval
> of instance id.. the naming should be reflective of that. perhaps
> set_instance_id_accessor

Good idea

> what's the case for the instance id not being set? is that just for testing
> ease, if so it should probably be commented as such.

It's just that it's not actually used, except when we're running a bootstrap node. This could well change in the future.

342. By William Reade

merge parent

343. By William Reade

fixes per hazmat's review

344. By William Reade

cloud-inits now contain lists of ssh keys, not list-of-lists of ssh keys

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'ensemble/providers/common/base.py'
--- ensemble/providers/common/base.py 2011-08-29 02:23:11 +0000
+++ ensemble/providers/common/base.py 2011-09-13 10:56:30 +0000
@@ -65,11 +65,10 @@
65 def start_machine(self, machine_data, master=False):65 def start_machine(self, machine_data, master=False):
66 """Start a machine in the provider.66 """Start a machine in the provider.
6767
68 @param machine_data: a dictionary of data to pass along to the newly68 `machine_data`: a dictionary describing the machine to be launched.
69 launched machine.
7069
71 @param master: if True, machine will initialize the ensemble admin70 `master`: if True, machine will initialize the ensemble admin and run
72 and run a provisioning agent.71 a provisioning agent, in addition to running a machine agent
73 """72 """
74 raise NotImplementedError()73 raise NotImplementedError()
7574
7675
=== modified file 'ensemble/providers/common/cloudinit.py'
--- ensemble/providers/common/cloudinit.py 2011-09-13 10:56:30 +0000
+++ ensemble/providers/common/cloudinit.py 2011-09-13 10:56:30 +0000
@@ -60,7 +60,7 @@
60 self._ssh_keys = []60 self._ssh_keys = []
61 self._provision = False61 self._provision = False
62 self._zookeeper = False62 self._zookeeper = False
63 self._zookeeper_names = []63 self._zookeeper_hosts = []
64 self._zookeeper_secret = None64 self._zookeeper_secret = None
6565
66 def add_ssh_key(self, key):66 def add_ssh_key(self, key):
@@ -83,14 +83,14 @@
83 def set_machine_id(self, id):83 def set_machine_id(self, id):
84 self._machine_id = id84 self._machine_id = id
8585
86 def set_instance_id(self, expr):86 def set_instance_id_accessor(self, expr):
87 self._instance_id = expr87 self._instance_id = expr
8888
89 def set_provider_type(self, type_):89 def set_provider_type(self, type_):
90 self._provider_type = type_90 self._provider_type = type_
9191
92 def set_zookeeper_machines(self, machines):92 def set_zookeeper_machines(self, machines):
93 self._zookeeper_names = [m.private_dns_name for m in machines]93 self._zookeeper_hosts = [m.private_dns_name for m in machines]
9494
95 def set_zookeeper_secret(self, secret):95 def set_zookeeper_secret(self, secret):
96 self._zookeeper_secret = secret96 self._zookeeper_secret = secret
@@ -115,20 +115,19 @@
115 require("_machine_id", "set_machine_id")115 require("_machine_id", "set_machine_id")
116 require("_provider_type", "set_provider_type")116 require("_provider_type", "set_provider_type")
117 if self._zookeeper:117 if self._zookeeper:
118 require("_instance_id", "set_instance_id")118 require("_instance_id", "set_instance_id_accessor")
119 require("_zookeeper_secret", "set_zookeeper_secret")119 require("_zookeeper_secret", "set_zookeeper_secret")
120 else:120 else:
121 require("_zookeeper_names", "set_zookeeper_machines")121 require("_zookeeper_hosts", "set_zookeeper_machines")
122 if missing:122 if missing:
123 raise CloudInitError("Incomplete cloud-init: you need to call %s"123 raise CloudInitError("Incomplete cloud-init: you need to call %s"
124 % ", ".join(missing))124 % ", ".join(missing))
125125
126 @property126 def _join_zookeeper_hosts(self):
127 def _zookeeper_hosts(self):127 all_hosts = self._zookeeper_hosts[:]
128 all_names = self._zookeeper_names[:]
129 if self._zookeeper:128 if self._zookeeper:
130 all_names.append("localhost")129 all_hosts.append("localhost")
131 return ",".join(["%s:2181" % name for name in all_names])130 return ",".join(["%s:2181" % host for host in all_hosts])
132131
133 def _collect_packages(self):132 def _collect_packages(self):
134 packages = [133 packages = [
@@ -150,13 +149,13 @@
150 scripts.extend(_zookeeper_scripts(149 scripts.extend(_zookeeper_scripts(
151 self._instance_id, self._zookeeper_secret))150 self._instance_id, self._zookeeper_secret))
152 scripts.extend(_machine_scripts(151 scripts.extend(_machine_scripts(
153 self._machine_id, self._zookeeper_hosts))152 self._machine_id, self._join_zookeeper_hosts()))
154 if self._provision:153 if self._provision:
155 scripts.extend(_provision_scripts(self._zookeeper_hosts))154 scripts.extend(_provision_scripts(self._join_zookeeper_hosts()))
156 return scripts155 return scripts
157156
158 def _collect_machine_data(self):157 def _collect_machine_data(self):
159 return {158 return {
160 "machine-id": self._machine_id,159 "machine-id": self._machine_id,
161 "ensemble-provider-type": self._provider_type,160 "ensemble-provider-type": self._provider_type,
162 "ensemble-zookeeper-hosts": self._zookeeper_hosts}161 "ensemble-zookeeper-hosts": self._join_zookeeper_hosts()}
163162
=== modified file 'ensemble/providers/common/launch.py'
--- ensemble/providers/common/launch.py 2011-08-17 16:01:43 +0000
+++ ensemble/providers/common/launch.py 2011-09-13 10:56:30 +0000
@@ -1,195 +1,71 @@
1import copy
2
3from twisted.internet.defer import inlineCallbacks, returnValue1from twisted.internet.defer import inlineCallbacks, returnValue
42
5from ensemble.errors import ProviderError3from .cloudinit import CloudInit
6from ensemble.state.auth import make_identity
7
8from .utils import get_user_authorized_keys4from .utils import get_user_authorized_keys
95
10DEFAULT_REPOSITORIES = [
11 "ppa:ensemble/ppa"]
12
13DEFAULT_PACKAGES = [
14 "bzr",
15 "byobu",
16 "tmux",
17 "python-setuptools",
18 "python-twisted",
19 "python-argparse",
20 "python-txaws",
21 "python-zookeeper"]
22
23BOOTSTRAP_PACKAGES = [
24 "bzr",
25 "default-jre-headless",
26 "zookeeper",
27 "zookeeperd"]
28
29
30def _get_initialize_script(instance_id_command, admin_secret):
31 admin_identity = make_identity("admin:%s" % admin_secret)
32 template = ('ensemble-admin initialize '
33 '--instance-id=%s --admin-identity="%s"')
34 return template % (instance_id_command, admin_identity)
35
36
37def _get_machine_agent_script(vars):
38 template = (
39 "ENSEMBLE_MACHINE_ID=%(machine-id)s "
40 "ENSEMBLE_ZOOKEEPER=%(ensemble-zookeeper-hosts)s "
41 "python -m ensemble.agents.machine -n "
42 "--logfile=/var/log/ensemble/machine-agent.log "
43 "--pidfile=/var/run/ensemble/machine-agent.pid")
44 return template % vars
45
46
47def _get_provision_agent_script(vars):
48 template = (
49 "ENSEMBLE_ZOOKEEPER=%(ensemble-zookeeper-hosts)s "
50 "python -m ensemble.agents.provision -n "
51 "--logfile=/var/log/ensemble/provision-agent.log "
52 "--pidfile=/var/run/ensemble/provision-agent.pid")
53 return template % vars
54
556
56class LaunchMachine(object):7class LaunchMachine(object):
57 """Abstract class with generic instance-launching logic.8 """Abstract class with generic instance-launching logic.
589
59 Constructing with master=True will cause the run method to
60 construct a machine which is also running a zookeeper for the
61 cluster, and a provisioning agent, as well as the usual machine
62 agent.
63
64 To create your own subclass, you will certainly need to override10 To create your own subclass, you will certainly need to override
65 C{start_machine} and C{get_instance_id_command}, and may find it11 `start_machine`, and will very probably want to implement it with the aid
66 convenient to override C{get_machine_variables} as well.12 of a provider-specific subclass of `ProviderCloudInit`.
13
14 `provider`: the necessary `MachineProvider`
15
16 `master`: if True, the machine will run a zookeeper and a provisioning
17 agent, in addition to the machine agent that every machine runs
67 """18 """
6819
69 def __init__(self, provider, master=False):20 def __init__(self, provider, master=False, constraints=None):
70 self._provider = provider21 self._provider = provider
71 self._master = master22 self._master = master
23 self._constraints = constraints or {}
7224
73 @inlineCallbacks25 @inlineCallbacks
74 def run(self, machine_data):26 def run(self, machine_id):
75 """Launch an instance node within the machine provider environment.27 """Launch an instance node within the machine provider environment.
7628
77 `machine_data`: a dictionary of data that is passed along to29 `machine_id`: the ensemble machine ID
78 provided to the machine as serialized yaml. 'machine-id' is a
79 required key, denoting the machine's zookeeper node for its
80 machine agent.
81 """30 """
82 if "machine-id" not in machine_data:31 # XXX at some point, we'll want to start multiple zookeepers
83 raise ProviderError(32 # that know about each other; for now, this'll do
84 "Machine state `machine-id` not provided in machine_data.")33 if self._master:
8534 zookeepers = []
86 machine_variables = yield self.get_machine_variables(machine_data)35 else:
87 provider_machines = yield self.start_machine(36 zookeepers = yield self._provider.get_zookeeper_machines()
88 machine_variables, machine_data["machine-id"])37
89 if self._master:38 machines = yield self.start_machine(machine_id, zookeepers)
90 yield self._on_master_launched(provider_machines)39 if self._master:
91 returnValue(provider_machines)40 yield self._on_new_zookeepers(machines)
9241 returnValue(machines)
93 def start_machine(self, machine_variables, machine_id):42
43 def start_machine(self, machine_id, zookeepers):
94 """Actually launch a machine for the appropriate provider.44 """Actually launch a machine for the appropriate provider.
9545
96 `machine_variables`: non-provider-specific data, sufficient to46 `machine_id`: the ensemble machine ID
97 define the machine's behaviour once it exists.47
9848 `zookeepers`: a list of `ProviderMachine`s already running zookeeper,
99 `machine_id`: the external machine ID (also exists in the49 which the machine will need to connect to. May be empty.
100 above bag of data)50
10151 Returns a singe-entry list containing a `ProviderMachine` for the
102 Returns a singe-entry list containing a ProviderMachine for the
103 new instance52 new instance
104 """53 """
105 raise NotImplementedError()54 raise NotImplementedError()
10655
107 def get_instance_id_command(self):56 def _create_cloud_init(self, machine_id, zookeepers):
108 """Snippet to discover local instance-id57 config = self._provider.config
10958 cloud_init = CloudInit()
110 @return: a snippet of shell script that evaluates to the59 cloud_init.add_ssh_key(get_user_authorized_keys(config))
111 local machine's instance_id.60 cloud_init.set_machine_id(machine_id)
112 """61 cloud_init.set_zookeeper_machines(zookeepers)
113 raise NotImplementedError()62 if config.get("ensemble-branch"):
11463 cloud_init.set_ensemble_source(config["ensemble-branch"])
115 @inlineCallbacks64 if self._master:
116 def get_machine_variables(self, vars_in):65 cloud_init.enable_bootstrap()
117 """Get the variables used for launching a machine and its cloud-init"""66 cloud_init.set_zookeeper_secret(config["admin-secret"])
118 vars_out = copy.deepcopy(vars_in)67 return cloud_init
119 vars_out[68
120 "ensemble-zookeeper-hosts"] = yield self._get_zookeeper_hosts()69 def _on_new_zookeepers(self, machines):
121
122 authorized_keys = get_user_authorized_keys(self._provider.config)
123 vars_out["authorized_keys"] = authorized_keys
124 vars_out["packages"] = self._get_packages()
125 vars_out["repositories"] = list(DEFAULT_REPOSITORIES)
126 vars_out["scripts"] = self._get_launch_scripts(vars_out)
127 returnValue(vars_out)
128
129 @inlineCallbacks
130 def _get_zookeeper_hosts(self):
131 if self._master:
132 returnValue("localhost:2181")
133 machines = yield self._provider.get_zookeeper_machines()
134 hosts = [m.private_dns_name for m in machines]
135 # format multiple hosts in string suitable for client
136 returnValue(",".join(["%s:2181" % h for h in hosts]))
137
138 def _get_packages(self):
139 result = list(DEFAULT_PACKAGES)
140 if self._master:
141 result.extend(BOOTSTRAP_PACKAGES)
142 return result
143
144 def _get_launch_scripts(self, machine_data):
145 """Get the scripts that will execute after the machine has launched."""
146 launch_scripts = self._get_install_scripts()
147
148 # These should be in the deb
149 launch_scripts.extend([
150 "sudo mkdir -p /var/lib/ensemble",
151 "sudo mkdir -p /var/log/ensemble"])
152
153 if self._master:
154 launch_scripts.append(self._get_initialize_script())
155
156 # Every machine has its own agent.
157 machine_agent_script_template = (
158 "ENSEMBLE_MACHINE_ID=%(machine-id)s "
159 "ENSEMBLE_ZOOKEEPER=%(ensemble-zookeeper-hosts)s "
160 "python -m ensemble.agents.machine -n "
161 "--logfile=/var/log/ensemble/machine-agent.log "
162 "--pidfile=/var/run/ensemble/machine-agent.pid")
163 launch_scripts.append(machine_agent_script_template % machine_data)
164
165 if self._master:
166 launch_scripts.append(_get_provision_agent_script(machine_data))
167
168 return launch_scripts
169
170 def _get_install_scripts(self):
171 branch = self._provider.config.get("ensemble-branch")
172 if branch is None:
173 return ["sudo apt-get -y install ensemble"]
174 # If we're using a custom branch, pass it via cloud-init to the agents,
175 # so the entire cluster uses it.
176 return [
177 "sudo apt-get install -y python-txzookeeper",
178 "sudo mkdir -p /usr/lib/ensemble",
179 "cd /usr/lib/ensemble && sudo /usr/bin/bzr co %s ensemble" % \
180 branch,
181 "cd /usr/lib/ensemble/ensemble && sudo python setup.py develop"]
182
183 def _get_initialize_script(self):
184 admin_secret = self._provider.config["admin-secret"]
185 return _get_initialize_script(
186 self.get_instance_id_command(), admin_secret)
187
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)
192 instance_ids = [m.instance_id for m in machines]70 instance_ids = [m.instance_id for m in machines]
193 d = self._provider.save_state({"zookeeper-instances": instance_ids})71 return self._provider.save_state({"zookeeper-instances": instance_ids})
194 d.addCallback(lambda _: machines)
195 return d
19672
=== modified file 'ensemble/providers/common/tests/data/cloud_init_bootstrap'
--- ensemble/providers/common/tests/data/cloud_init_bootstrap 2011-09-13 10:56:30 +0000
+++ ensemble/providers/common/tests/data/cloud_init_bootstrap 2011-09-13 10:56:30 +0000
@@ -14,5 +14,4 @@
14 -n --logfile=/var/log/ensemble/machine-agent.log --pidfile=/var/run/ensemble/machine-agent.pid',14 -n --logfile=/var/log/ensemble/machine-agent.log --pidfile=/var/run/ensemble/machine-agent.pid',
15 'ENSEMBLE_ZOOKEEPER=localhost:2181 python -m ensemble.agents.provision -n --logfile=/var/log/ensemble/provision-agent.log15 'ENSEMBLE_ZOOKEEPER=localhost:2181 python -m ensemble.agents.provision -n --logfile=/var/log/ensemble/provision-agent.log
16 --pidfile=/var/run/ensemble/provision-agent.pid']16 --pidfile=/var/run/ensemble/provision-agent.pid']
17ssh_authorized_keys:17ssh_authorized_keys: [chubb]
18- [chubb]
1918
=== modified file 'ensemble/providers/common/tests/data/cloud_init_bootstrap_zookeepers'
--- ensemble/providers/common/tests/data/cloud_init_bootstrap_zookeepers 2011-09-13 10:56:30 +0000
+++ ensemble/providers/common/tests/data/cloud_init_bootstrap_zookeepers 2011-09-13 10:56:30 +0000
@@ -15,5 +15,4 @@
15 --pidfile=/var/run/ensemble/machine-agent.pid', 'ENSEMBLE_ZOOKEEPER=cotswold:2181,longleat:2181,localhost:218115 --pidfile=/var/run/ensemble/machine-agent.pid', 'ENSEMBLE_ZOOKEEPER=cotswold:2181,longleat:2181,localhost:2181
16 python -m ensemble.agents.provision -n --logfile=/var/log/ensemble/provision-agent.log16 python -m ensemble.agents.provision -n --logfile=/var/log/ensemble/provision-agent.log
17 --pidfile=/var/run/ensemble/provision-agent.pid']17 --pidfile=/var/run/ensemble/provision-agent.pid']
18ssh_authorized_keys:18ssh_authorized_keys: [chubb]
19- [chubb]
2019
=== modified file 'ensemble/providers/common/tests/data/cloud_init_branch'
--- ensemble/providers/common/tests/data/cloud_init_branch 2011-09-13 10:56:30 +0000
+++ ensemble/providers/common/tests/data/cloud_init_branch 2011-09-13 10:56:30 +0000
@@ -14,5 +14,4 @@
14 sudo mkdir -p /var/log/ensemble, 'ENSEMBLE_MACHINE_ID=passport ENSEMBLE_ZOOKEEPER=cotswold:2181,longleat:218114 sudo mkdir -p /var/log/ensemble, 'ENSEMBLE_MACHINE_ID=passport ENSEMBLE_ZOOKEEPER=cotswold:2181,longleat:2181
15 python -m ensemble.agents.machine -n --logfile=/var/log/ensemble/machine-agent.log15 python -m ensemble.agents.machine -n --logfile=/var/log/ensemble/machine-agent.log
16 --pidfile=/var/run/ensemble/machine-agent.pid']16 --pidfile=/var/run/ensemble/machine-agent.pid']
17ssh_authorized_keys:17ssh_authorized_keys: [chubb]
18- [chubb]
1918
=== modified file 'ensemble/providers/common/tests/data/cloud_init_distro'
--- ensemble/providers/common/tests/data/cloud_init_distro 2011-09-13 10:56:30 +0000
+++ ensemble/providers/common/tests/data/cloud_init_distro 2011-09-13 10:56:30 +0000
@@ -10,5 +10,4 @@
10 -p /var/log/ensemble, 'ENSEMBLE_MACHINE_ID=passport ENSEMBLE_ZOOKEEPER=cotswold:2181,longleat:218110 -p /var/log/ensemble, 'ENSEMBLE_MACHINE_ID=passport ENSEMBLE_ZOOKEEPER=cotswold:2181,longleat:2181
11 python -m ensemble.agents.machine -n --logfile=/var/log/ensemble/machine-agent.log11 python -m ensemble.agents.machine -n --logfile=/var/log/ensemble/machine-agent.log
12 --pidfile=/var/run/ensemble/machine-agent.pid']12 --pidfile=/var/run/ensemble/machine-agent.pid']
13ssh_authorized_keys:13ssh_authorized_keys: [chubb]
14- [chubb]
1514
=== modified file 'ensemble/providers/common/tests/data/cloud_init_normal'
--- ensemble/providers/common/tests/data/cloud_init_normal 2011-09-13 10:56:30 +0000
+++ ensemble/providers/common/tests/data/cloud_init_normal 2011-09-13 10:56:30 +0000
@@ -12,5 +12,4 @@
12 -p /var/log/ensemble, 'ENSEMBLE_MACHINE_ID=passport ENSEMBLE_ZOOKEEPER=cotswold:2181,longleat:218112 -p /var/log/ensemble, 'ENSEMBLE_MACHINE_ID=passport ENSEMBLE_ZOOKEEPER=cotswold:2181,longleat:2181
13 python -m ensemble.agents.machine -n --logfile=/var/log/ensemble/machine-agent.log13 python -m ensemble.agents.machine -n --logfile=/var/log/ensemble/machine-agent.log
14 --pidfile=/var/run/ensemble/machine-agent.pid']14 --pidfile=/var/run/ensemble/machine-agent.pid']
15ssh_authorized_keys:15ssh_authorized_keys: [chubb]
16- [chubb]
1716
=== added file 'ensemble/providers/common/tests/test_bootstrap.py'
--- ensemble/providers/common/tests/test_bootstrap.py 1970-01-01 00:00:00 +0000
+++ ensemble/providers/common/tests/test_bootstrap.py 2011-09-13 10:56:30 +0000
@@ -0,0 +1,102 @@
1import logging
2import tempfile
3
4from twisted.internet.defer import fail, succeed
5
6from ensemble.errors import EnvironmentNotFound, ProviderError
7from ensemble.lib.testing import TestCase
8from ensemble.providers.common.base import MachineProviderBase
9from ensemble.providers.dummy import DummyMachine, FileStorage
10
11
12class SomeError(Exception):
13 pass
14
15
16class WorkingFileStorage(FileStorage):
17
18 def __init__(self):
19 super(WorkingFileStorage, self).__init__(tempfile.mkdtemp())
20
21
22class UnwritableFileStorage(object):
23
24 def put(self, name, f):
25 return fail(Exception("oh noes"))
26
27
28class DummyProvider(MachineProviderBase):
29
30 def __init__(self, file_storage, zookeeper):
31 self._file_storage = file_storage
32 self._zookeeper = zookeeper
33
34 def get_file_storage(self):
35 return self._file_storage
36
37 def get_zookeeper_machines(self):
38 if isinstance(self._zookeeper, Exception):
39 return fail(self._zookeeper)
40 if self._zookeeper:
41 return succeed([self._zookeeper])
42 return fail(EnvironmentNotFound())
43
44 def start_machine(self, machine_id, master=False):
45 assert master is True
46 return [DummyMachine("i-keepzoos")]
47
48
49class BootstrapTest(TestCase):
50
51 def test_unknown_error(self):
52 provider = DummyProvider(None, SomeError())
53 d = provider.bootstrap()
54 self.assertFailure(d, SomeError)
55 return d
56
57 def test_zookeeper_exists(self):
58 log = self.capture_logging("ensemble.common", level=logging.DEBUG)
59 provider = DummyProvider(
60 WorkingFileStorage(), DummyMachine("i-alreadykeepzoos"))
61 d = provider.bootstrap()
62
63 def verify(machines):
64 (machine,) = machines
65 self.assertTrue(isinstance(machine, DummyMachine))
66 self.assertEquals(machine.instance_id, "i-alreadykeepzoos")
67
68 log_text = log.getvalue()
69 self.assertIn(
70 "Ensemble environment previously bootstrapped", log_text)
71 self.assertNotIn("Launching", log_text)
72 d.addCallback(verify)
73 return d
74
75 def test_bad_storage(self):
76 provider = DummyProvider(UnwritableFileStorage(), None)
77 d = provider.bootstrap()
78 self.assertFailure(d, ProviderError)
79
80 def verify(error):
81 self.assertEquals(
82 str(error),
83 "Bootstrap aborted because file storage is not writable: "
84 "oh noes")
85 d.addCallback(verify)
86 return d
87
88 def test_create_zookeeper(self):
89 log = self.capture_logging("ensemble.common", level=logging.DEBUG)
90 provider = DummyProvider(WorkingFileStorage(), None)
91 d = provider.bootstrap()
92
93 def verify(machines):
94 (machine,) = machines
95 self.assertTrue(isinstance(machine, DummyMachine))
96 self.assertEquals(machine.instance_id, "i-keepzoos")
97
98 log_text = log.getvalue()
99 self.assertIn("Launching Ensemble bootstrap instance", log_text)
100 self.assertNotIn("previously bootstrapped", log_text)
101 d.addCallback(verify)
102 return d
0103
=== modified file 'ensemble/providers/common/tests/test_cloudinit.py'
--- ensemble/providers/common/tests/test_cloudinit.py 2011-09-13 10:56:30 +0000
+++ ensemble/providers/common/tests/test_cloudinit.py 2011-09-13 10:56:30 +0000
@@ -26,7 +26,7 @@
26 cloud_init.add_ssh_key("chubb")26 cloud_init.add_ssh_key("chubb")
27 cloud_init.set_machine_id("passport")27 cloud_init.set_machine_id("passport")
28 cloud_init.set_provider_type("dummy")28 cloud_init.set_provider_type("dummy")
29 cloud_init.set_instance_id("token")29 cloud_init.set_instance_id_accessor("token")
30 cloud_init.set_zookeeper_secret("seekrit")30 cloud_init.set_zookeeper_secret("seekrit")
31 if with_zookeepers:31 if with_zookeepers:
32 cloud_init.set_zookeeper_machines([32 cloud_init.set_zookeeper_machines([
@@ -54,7 +54,7 @@
54 self.assertEquals(54 self.assertEquals(
55 str(error),55 str(error),
56 "Incomplete cloud-init: you need to call add_ssh_key, "56 "Incomplete cloud-init: you need to call add_ssh_key, "
57 "set_machine_id, set_provider_type, set_instance_id, "57 "set_machine_id, set_provider_type, set_instance_id_accessor, "
58 "set_zookeeper_secret")58 "set_zookeeper_secret")
5959
60 def test_source_validate(self):60 def test_source_validate(self):
6161
=== modified file 'ensemble/providers/common/tests/test_launch.py'
--- ensemble/providers/common/tests/test_launch.py 2011-08-17 12:04:56 +0000
+++ ensemble/providers/common/tests/test_launch.py 2011-09-13 10:56:30 +0000
@@ -1,243 +1,80 @@
1import logging
2import tempfile1import tempfile
32
4from twisted.internet.defer import fail, inlineCallbacks, succeed3from twisted.internet.defer import fail, succeed
54
6from ensemble.errors import EnvironmentNotFound, ProviderError5from ensemble.errors import EnvironmentNotFound
7from ensemble.lib.testing import TestCase6from ensemble.lib.testing import TestCase
8from ensemble.providers.common.base import MachineProviderBase7from ensemble.providers.common.base import MachineProviderBase
9from ensemble.providers.common.launch import (8from ensemble.providers.common.launch import LaunchMachine
10 BOOTSTRAP_PACKAGES, DEFAULT_PACKAGES, DEFAULT_REPOSITORIES, LaunchMachine)
11from ensemble.providers.dummy import DummyMachine, FileStorage9from ensemble.providers.dummy import DummyMachine, FileStorage
1210
13DEFAULT_INSTALL = ["sudo apt-get -y install ensemble"]11
14CREATE_DIRS = ["sudo mkdir -p /var/lib/ensemble",12def launch_machine(test, master, zookeeper):
15 "sudo mkdir -p /var/log/ensemble"]
16
17
18class DummyLaunchMachine(LaunchMachine):
19
20 def start_machine(self, variables, data):
21 if self._master:
22 name = "bootstrapped-instance-id"
23 else:
24 name = "some-instance-id"
25 return [DummyMachine(name)]
26
27 def get_instance_id_command(self):
28 return "$(magic)"
29
30
31class WorkingFileStorage(FileStorage):
32
33 def __init__(self):
34 super(WorkingFileStorage, self).__init__(tempfile.mkdtemp())
35
36
37class UnwritableFileStorage(object):
38
39 def put(self, name, f):
40 return fail(Exception("oh noes"))
41
42
43def get_provider(launch_class=DummyLaunchMachine, config=None, zookeeper=True,
44 file_storage_class=WorkingFileStorage):
4513
46 class DummyProvider(MachineProviderBase):14 class DummyProvider(MachineProviderBase):
4715
48 def __init__(self, config):16 def __init__(self):
49 super(DummyProvider, self).__init__("venus", config)17 self.config = {"admin-secret": "BLAH"}
50 self._file_storage = file_storage_class()18 self._file_storage = FileStorage(tempfile.mkdtemp())
51
52 def get_zookeeper_machines(self):
53 # this is mocked out to avoid insane complexity
54 if isinstance(zookeeper, Exception):
55 return fail(zookeeper)
56 return succeed(
57 [DummyMachine("zookeeper-instance-id",
58 private_dns_name="zookeeper.internal")])
5919
60 def get_file_storage(self):20 def get_file_storage(self):
61 return self._file_storage21 return self._file_storage
6222
63 def start_machine(self, machine_data, master=False):23 def get_zookeeper_machines(self):
64 return launch_class(self, master).run(machine_data)24 if zookeeper:
6525 return succeed([zookeeper])
66 create_config = {"authorized-keys": "abc"}26 return fail(EnvironmentNotFound())
67 if config is not None:27
68 create_config.update(config)28 class DummyLaunchMachine(LaunchMachine):
69 return DummyProvider(create_config)29
7030 def start_machine(self, machine_id, zookeepers):
7131 test.assertEquals(machine_id, "1234")
72def get_launch(launch_class=DummyLaunchMachine, config=None,32 test.assertEquals(zookeepers, filter(None, [zookeeper]))
73 zookeeper=True, master=False):33 test.assertEquals(self._master, master)
74 provider = get_provider(launch_class, config, zookeeper)34 test.assertEquals(self._constraints, {})
75 return launch_class(provider, master=master)35 return succeed([DummyMachine("i-malive")])
36
37 provider = DummyProvider()
38 d = DummyLaunchMachine(provider, master).run("1234")
39 return provider, d
7640
7741
78class LaunchMachineTest(TestCase):42class LaunchMachineTest(TestCase):
7943
80 def _verify_machines(self, machines):44 def assert_success(self, master, zookeeper):
81 (machine,) = machines45 provider, d = launch_machine(self, master, zookeeper)
82 self.assertTrue(isinstance(machine, DummyMachine))46
83 self.assertEquals(machine.instance_id, "some-instance-id")47 def verify(machines):
8448 (machine,) = machines
85 def test_no_machine_id(self):49 self.assertTrue(isinstance(machine, DummyMachine))
86 launch = get_launch()50 self.assertEquals(machine.instance_id, "i-malive")
87 d = launch.run({})51 return provider
88 self.assertFailure(d, ProviderError)52 d.addCallback(verify)
89 return d53 return d
9054
91 def test_basic_run(self):55 def test_start_nonzookeeper_no_zookeepers(self):
92 launch = get_launch()56 """Starting a non-zookeeper without a zookeeper is an error"""
93 d = launch.run({"machine-id": "machine-77"})57 unused, d = launch_machine(self, False, None)
94 d.addCallback(self._verify_machines)58 self.assertFailure(d, EnvironmentNotFound)
95 return d59 return d
9660
97 @inlineCallbacks61 def test_start_zookeeper_no_zookeepers(self):
98 def test_bootstrap_run(self):62 """A new zookeeper should be recorded in provider state"""
99 provider = get_provider(config={"admin-secret": "whatever"})63 d = self.assert_success(True, None)
100 launch = DummyLaunchMachine(provider, master=True)64
101 yield launch.run({"machine-id": "machine-32767"})65 def verify(provider):
102 saved_state = yield provider.load_state()66 provider_state = yield provider.load_state()
103 self.assertEquals(67 self.assertEquals(
104 saved_state, {"zookeeper-instances": ["bootstrapped-instance-id"]})68 provider_state, {"zookeeper-instances": ["i-malive"]})
10569 d.addCallback(verify)
106 def test_get_machine_variables_normal(self):70 return d
107 launch = get_launch()71
108 d = launch.get_machine_variables({"machine-id": "machine-999"})72 def test_works_with_zookeeper(self):
10973 """Creating a non-zookeeper machine should not alter provider state"""
110 def verify_vars(vars):74 d = self.assert_success(False, DummyMachine("i-keepzoos"))
111 expect_vars = {75
112 "authorized_keys": "abc",76 def verify(provider):
113 "ensemble-zookeeper-hosts": "zookeeper.internal:2181",77 provider_state = yield provider.load_state()
114 "machine-id": "machine-999",78 self.assertEquals(provider_state, False)
115 "packages": DEFAULT_PACKAGES,79 d.addCallback(verify)
116 "repositories": DEFAULT_REPOSITORIES,
117 "scripts": DEFAULT_INSTALL + CREATE_DIRS +
118 ["ENSEMBLE_MACHINE_ID=machine-999 "
119 "ENSEMBLE_ZOOKEEPER=zookeeper.internal:2181 "
120 "python -m ensemble.agents.machine -n "
121 "--logfile=/var/log/ensemble/machine-agent.log "
122 "--pidfile=/var/run/ensemble/machine-agent.pid"]}
123 self.assertEquals(vars, expect_vars)
124 d.addCallback(verify_vars)
125 return d
126
127 def test_get_machine_variables_with_ensemble_branch(self):
128 branch_name = "lp:~my/ensemble/branch"
129 launch = get_launch(config={"ensemble-branch": branch_name})
130 d = launch.get_machine_variables({"machine-id": "machine-303"})
131
132 def verify_vars(vars):
133 scripts = vars["scripts"]
134 install_scripts = ["sudo apt-get install -y python-txzookeeper",
135 "sudo mkdir -p /usr/lib/ensemble",
136 "cd /usr/lib/ensemble && "
137 "sudo /usr/bin/bzr co "
138 + branch_name + " ensemble",
139 "cd /usr/lib/ensemble/ensemble && "
140 "sudo python setup.py develop"]
141 install_count = len(install_scripts)
142 self.assertEquals(scripts[:install_count], install_scripts)
143
144 other_scripts = scripts[install_count:]
145 self.assertEquals(other_scripts,
146 CREATE_DIRS + [
147 "ENSEMBLE_MACHINE_ID=machine-303 "
148 "ENSEMBLE_ZOOKEEPER=zookeeper.internal:2181 "
149 "python -m ensemble.agents.machine -n "
150 "--logfile=/var/log/ensemble/machine-agent.log "
151 "--pidfile=/var/run/ensemble/machine-agent.pid"])
152 d.addCallback(verify_vars)
153 return d
154
155 def test_get_machine_variables_bootstrap(self):
156 launch = get_launch(config={"admin-secret": "SEEKRIT"}, master=True)
157 d = launch.get_machine_variables({"machine-id": "machine-757"})
158
159 def verify_vars(vars):
160 expect_vars = {
161 "authorized_keys": "abc",
162 "ensemble-zookeeper-hosts": "localhost:2181",
163 "machine-id": "machine-757",
164 "packages": DEFAULT_PACKAGES + BOOTSTRAP_PACKAGES,
165 "repositories": DEFAULT_REPOSITORIES}
166 for key, value in expect_vars.items():
167 self.assertEquals(vars[key], value)
168
169 scripts = vars["scripts"]
170 self.assertEquals(scripts[:3], DEFAULT_INSTALL + CREATE_DIRS)
171 admin_init, machine_agent, provision_agent = scripts[3:]
172
173 self.assertTrue(admin_init.startswith(
174 "ensemble-admin initialize "
175 '--instance-id=$(magic) --admin-identity="admin:'))
176 self.assertFalse("SEEKRIT" in admin_init)
177
178 expect_machine = (
179 "ENSEMBLE_MACHINE_ID=machine-757 "
180 "ENSEMBLE_ZOOKEEPER=localhost:2181 "
181 "python -m ensemble.agents.machine -n "
182 "--logfile=/var/log/ensemble/machine-agent.log "
183 "--pidfile=/var/run/ensemble/machine-agent.pid")
184 self.assertEquals(machine_agent, expect_machine)
185
186 expect_provision = (
187 "ENSEMBLE_ZOOKEEPER=localhost:2181 "
188 "python -m ensemble.agents.provision -n "
189 "--logfile=/var/log/ensemble/provision-agent.log "
190 "--pidfile=/var/run/ensemble/provision-agent.pid")
191 self.assertEquals(provision_agent, expect_provision)
192 d.addCallback(verify_vars)
193 return d
194
195
196class BootstrapTest(TestCase):
197
198 def test_bootstrap_unknown_error(self):
199 class BBQError(Exception):
200 pass
201 provider = get_provider(zookeeper=BBQError())
202 d = provider.bootstrap()
203 self.assertFailure(d, BBQError)
204 return d
205
206 def test_bootstrap_unwritable_storage(self):
207 provider = get_provider(zookeeper=EnvironmentNotFound(),
208 file_storage_class=UnwritableFileStorage)
209 d = provider.bootstrap()
210 self.assertFailure(d, ProviderError)
211 return d
212
213 def test_bootstrap_no_launch(self):
214 log = self.capture_logging("ensemble.common", level=logging.DEBUG)
215 provider = get_provider()
216 d = provider.bootstrap()
217
218 def verify_machines(machines):
219 (machine,) = machines
220 self.assertTrue(isinstance(machine, DummyMachine))
221 self.assertEquals(machine.instance_id, "zookeeper-instance-id")
222 log_text = log.getvalue()
223 self.assertIn(
224 "Ensemble environment previously bootstrapped", log_text)
225 self.assertNotIn("Launching", log_text)
226 d.addCallback(verify_machines)
227 return d
228
229 def test_bootstrap_launch(self):
230 log = self.capture_logging("ensemble.common", level=logging.DEBUG)
231 provider = get_provider(config={"admin-secret": "SEEKRIT"},
232 zookeeper=EnvironmentNotFound())
233 d = provider.bootstrap()
234
235 def verify_machines(machines):
236 (machine,) = machines
237 self.assertTrue(isinstance(machine, DummyMachine))
238 self.assertEquals(machine.instance_id, "bootstrapped-instance-id")
239 log_text = log.getvalue()
240 self.assertIn("Launching Ensemble bootstrap instance", log_text)
241 self.assertNotIn("previously bootstrapped", log_text)
242 d.addCallback(verify_machines)
243 return d80 return d
24481
=== modified file 'ensemble/providers/common/tests/test_utils.py'
--- ensemble/providers/common/tests/test_utils.py 2011-08-18 12:19:22 +0000
+++ ensemble/providers/common/tests/test_utils.py 2011-09-13 10:56:30 +0000
@@ -1,5 +1,4 @@
1import os1import os
2
3from yaml import load2from yaml import load
43
5from twisted.python.failure import Failure4from twisted.python.failure import Failure
@@ -100,7 +99,7 @@
100 scripts = ["wget http://lwn.net > /tmp/out"]99 scripts = ["wget http://lwn.net > /tmp/out"]
101 repositories = ["ppa:ensemble/ppa"]100 repositories = ["ppa:ensemble/ppa"]
102 output = format_cloud_init(101 output = format_cloud_init(
103 "zebra",102 ["zebra"],
104 packages=packages,103 packages=packages,
105 scripts=scripts,104 scripts=scripts,
106 repositories=repositories,105 repositories=repositories,
107106
=== modified file 'ensemble/providers/common/utils.py'
--- ensemble/providers/common/utils.py 2011-08-31 21:12:40 +0000
+++ ensemble/providers/common/utils.py 2011-09-13 10:56:30 +0000
@@ -1,6 +1,5 @@
1import logging1import logging
2import os2import os
3
4from yaml import safe_dump3from yaml import safe_dump
54
6from twisted.python.failure import Failure5from twisted.python.failure import Failure
@@ -40,6 +39,7 @@
40 raise error39 raise error
4140
4241
42# XXX this should either be singular, or return a list
43def get_user_authorized_keys(config):43def get_user_authorized_keys(config):
44 """Locate a public key for the user.44 """Locate a public key for the user.
4545
@@ -85,7 +85,7 @@
8585
86 @param authorized_keys The authorized SSH keys to be used when populating86 @param authorized_keys The authorized SSH keys to be used when populating
87 the newly launched machine's.87 the newly launched machine's.
88 @type string88 @type list of strings
8989
90 @param packages A list of packages to be installed on a machine.90 @param packages A list of packages to be installed on a machine.
91 @type list of strings91 @type list of strings
@@ -104,7 +104,7 @@
104 cloud_config = {104 cloud_config = {
105 "apt-update": True,105 "apt-update": True,
106 "apt-upgrade": True,106 "apt-upgrade": True,
107 "ssh_authorized_keys": [authorized_keys],107 "ssh_authorized_keys": authorized_keys,
108 "packages": [],108 "packages": [],
109 "output": { "all": "| tee -a /var/log/cloud-init-output.log" }}109 "output": { "all": "| tee -a /var/log/cloud-init-output.log" }}
110110
111111
=== modified file 'ensemble/providers/ec2/__init__.py'
--- ensemble/providers/ec2/__init__.py 2011-08-29 18:34:02 +0000
+++ ensemble/providers/ec2/__init__.py 2011-09-13 10:56:30 +0000
@@ -1,7 +1,8 @@
1import os1import os
2import re2import re
33
4from twisted.internet.defer import inlineCallbacks, returnValue4from twisted.internet.defer import fail, inlineCallbacks, returnValue
5
5from txaws.ec2.exception import EC2Error6from txaws.ec2.exception import EC2Error
6from txaws.service import AWSServiceRegion7from txaws.service import AWSServiceRegion
78
@@ -53,13 +54,17 @@
53 def start_machine(self, machine_data, master=False):54 def start_machine(self, machine_data, master=False):
54 """Start a machine in the provider.55 """Start a machine in the provider.
5556
56 @param machine_data: a dictionary of data to pass along to the newly57 `machine_data`: a dictionary describing the machine to be launched.
57 launched machine.
5858
59 @param master if True, machine will initialize the ensemble admin59 @param master: if True, machine will initialize the ensemble admin
60 and run a provisioning agent.60 and run a provisioning agent.
61 """61 """
62 return EC2LaunchMachine(self, master).run(machine_data)62 if "machine-id" not in machine_data:
63 return fail(ProviderError(
64 "Cannot launch a machine without specifying a machine-id"))
65 machine_id = machine_data["machine-id"]
66 constraints = machine_data.get("constraints", {})
67 return EC2LaunchMachine(self, master, constraints).run(machine_id)
6368
64 @inlineCallbacks69 @inlineCallbacks
65 def get_machines(self, instance_ids=()):70 def get_machines(self, instance_ids=()):
6671
=== modified file 'ensemble/providers/ec2/launch.py'
--- ensemble/providers/ec2/launch.py 2011-08-23 09:59:59 +0000
+++ ensemble/providers/ec2/launch.py 2011-09-13 10:56:30 +0000
@@ -1,67 +1,52 @@
1from twisted.internet.defer import inlineCallbacks, returnValue1from twisted.internet.defer import inlineCallbacks, returnValue
2
2from txaws.ec2.exception import EC2Error3from txaws.ec2.exception import EC2Error
34
4from ensemble.errors import ProviderInteractionError5from ensemble.errors import ProviderInteractionError
5from ensemble.providers.common.launch import LaunchMachine6from ensemble.providers.common.launch import LaunchMachine
67
7from .machine import machine_from_instance8from .machine import machine_from_instance
8from .utils import log, get_launch_options9from .utils import get_image_id, log
910
1011
11class EC2LaunchMachine(LaunchMachine):12class EC2LaunchMachine(LaunchMachine):
12 """Amazon EC2 operation for launching an instance"""13 """Amazon EC2 operation for launching an instance"""
1314
14 @inlineCallbacks15 @inlineCallbacks
15 def start_machine(self, machine_variables, machine_id):16 def start_machine(self, machine_id, zookeepers):
16 """Actually launch an instance on EC2.17 """Actually launch an instance on EC2.
1718
18 `machine_variables`: non-provider-specific data, sufficient to19 `machine_id`: the ensemble machine ID
19 define the machine's behaviour once it exists.
2020
21 `machine_id`: the external machine ID (also exists in the21 `zookeepers`: a list of `ProviderMachine`s already running zookeeper,
22 above bag of data)22 which the machine will need to connect to. May be empty.
2323
24 Returns a singe-entry list containing a ProviderMachine for the24 Returns a singe-entry list containing a ProviderMachine for the
25 new instance25 new instance
26 """26 """
27 # Retrieves standard ec2 run_instances arguments, of note27 cloud_init = self._create_cloud_init(machine_id, zookeepers)
28 # image id, and instance type.28 cloud_init.set_provider_type("ec2")
29 machine_options = yield get_launch_options(29 cloud_init.set_instance_id_accessor(
30 self._provider.config,30 "$(curl http://169.254.169.254/1.0/meta-data/instance-id)")
31 **machine_variables)31 user_data = cloud_init.render()
3232
33 # Ensure the ensemble security groups exist (both the overall33 instance_type = self._provider.config.get(
34 # group and the machine-specific group) and are included in34 "default-instance-type", "m1.small")
35 # the machine instance launch options.35 image_id = yield get_image_id(self._provider.config, self._constraints)
36 machine_options["security_groups"] = yield self._ensure_group(36 security_groups = yield self._ensure_groups(machine_id)
37 machine_options["security_groups"],37
38 machine_id)38 instances = yield self._provider.ec2.run_instances(
3939 image_id=image_id,
40 # Launch the instance.40 instance_type=instance_type,
41 instances = yield self._provider.ec2.run_instances(**machine_options)41 min_count=1,
42 returnValue([machine_from_instance(instance)42 max_count=1,
43 for instance in instances])43 security_groups=security_groups,
4444 user_data=user_data)
45 def get_instance_id_command(self):45
46 """Snippet to discover local instance-id46 returnValue([machine_from_instance(i) for i in instances])
47
48 @return: a snippet of shell script that evaluates to the
49 local machine's instance_id.
50 """
51 return "$(curl http://169.254.169.254/1.0/meta-data/instance-id)"
52
53 def get_machine_variables(self, vars_in):
54 vars_deferred = super(
55 EC2LaunchMachine, self).get_machine_variables(vars_in)
56
57 def set_provider_type(vars):
58 vars["ensemble-provider-type"] = "ec2"
59 return vars
60 vars_deferred.addCallback(set_provider_type)
61 return vars_deferred
6247
63 @inlineCallbacks48 @inlineCallbacks
64 def _ensure_group(self, groups, machine_id):49 def _ensure_groups(self, machine_id):
65 """Ensure the ensemble group is the machine launch groups.50 """Ensure the ensemble group is the machine launch groups.
6651
67 Machines launched by ensemble are tagged with a group so they52 Machines launched by ensemble are tagged with a group so they
@@ -71,21 +56,15 @@
71 specific machine security group is created for each machine,56 specific machine security group is created for each machine,
72 so that its firewall rules can be configured per machine.57 so that its firewall rules can be configured per machine.
7358
74 `groups`: The configured security groups for a machine instance.
75
76 `machine_id`: The external machine ID of this machine instance59 `machine_id`: The external machine ID of this machine instance
77 """60 """
78 # Label the instance with the ensemble name.
79 security_groups = yield self._provider.ec2.describe_security_groups()
80 ensemble_group = "ensemble-%s" % self._provider.environment_name61 ensemble_group = "ensemble-%s" % self._provider.environment_name
81 ensemble_machine_group = "ensemble-%s-%s" % (62 ensemble_machine_group = "ensemble-%s-%s" % (
82 self._provider.environment_name, machine_id)63 self._provider.environment_name, machine_id)
64
65 security_groups = yield self._provider.ec2.describe_security_groups()
83 group_ids = [group.name for group in security_groups]66 group_ids = [group.name for group in security_groups]
8467
85 # Ensure the provider group is added to the instance groups.
86 if not ensemble_group in groups:
87 groups.append(ensemble_group)
88
89 # Create the provider group if doesn't exist.68 # Create the provider group if doesn't exist.
90 if not ensemble_group in group_ids:69 if not ensemble_group in group_ids:
91 log.debug("Creating ensemble provider group %s", ensemble_group)70 log.debug("Creating ensemble provider group %s", ensemble_group)
@@ -131,6 +110,6 @@
131 ensemble_machine_group,110 ensemble_machine_group,
132 "Ensemble group for %s machine %s" % (111 "Ensemble group for %s machine %s" % (
133 self._provider.environment_name, machine_id))112 self._provider.environment_name, machine_id))
134 groups.append(ensemble_machine_group)113
135114 returnValue([ensemble_group, ensemble_machine_group])
136 returnValue(groups)115
137116
=== modified file 'ensemble/providers/ec2/tests/common.py'
--- ensemble/providers/ec2/tests/common.py 2011-08-29 02:23:11 +0000
+++ ensemble/providers/ec2/tests/common.py 2011-09-13 10:56:30 +0000
@@ -12,8 +12,6 @@
12from ensemble.providers.ec2 import MachineProvider12from ensemble.providers.ec2 import MachineProvider
13from ensemble.providers.ec2.machine import EC2ProviderMachine13from ensemble.providers.ec2.machine import EC2ProviderMachine
1414
15
16MATCH_AMI = MATCH(lambda image_id: image_id.startswith("ami-"))
17MATCH_GROUP = MATCH(lambda x: x.startswith("ensemble-moon"))15MATCH_GROUP = MATCH(lambda x: x.startswith("ensemble-moon"))
1816
1917
@@ -82,7 +80,7 @@
8280
83class EC2MachineLaunchMixin(object):81class EC2MachineLaunchMixin(object):
8482
85 def _mock_launch_utils(self, **get_ami_kwargs):83 def _mock_launch_utils(self, ami_name="ami-default", **get_ami_kwargs):
86 get_public_key = self.mocker.replace(84 get_public_key = self.mocker.replace(
87 "ensemble.providers.common.utils.get_user_authorized_keys")85 "ensemble.providers.common.utils.get_user_authorized_keys")
8886
@@ -100,7 +98,7 @@
10098
101 def check_kwargs(**kwargs):99 def check_kwargs(**kwargs):
102 self.assertEquals(kwargs, get_ami_kwargs)100 self.assertEquals(kwargs, get_ami_kwargs)
103 return succeed("ami-714ba518")101 return succeed(ami_name)
104 self.mocker.call(check_kwargs)102 self.mocker.call(check_kwargs)
105103
106 def _mock_create_group(self):104 def _mock_create_group(self):
107105
=== added file 'ensemble/providers/ec2/tests/data/bootstrap_cloud_init'
--- ensemble/providers/ec2/tests/data/bootstrap_cloud_init 1970-01-01 00:00:00 +0000
+++ ensemble/providers/ec2/tests/data/bootstrap_cloud_init 2011-09-13 10:56:30 +0000
@@ -0,0 +1,18 @@
1#cloud-config
2apt-update: true
3apt-upgrade: true
4apt_sources:
5- {source: 'ppa:ensemble/ppa'}
6machine-data: {ensemble-provider-type: ec2, ensemble-zookeeper-hosts: 'localhost:2181',
7 machine-id: '0'}
8output: {all: '| tee -a /var/log/cloud-init-output.log'}
9packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-argparse, python-txaws,
10 python-zookeeper, default-jre-headless, zookeeper, zookeeperd]
11runcmd: [sudo apt-get -y install ensemble, sudo mkdir -p /var/lib/ensemble, sudo mkdir
12 -p /var/log/ensemble, 'ensemble-admin initialize --instance-id=$(curl http://169.254.169.254/1.0/meta-data/instance-id)
13 --admin-identity=admin:JbJ6sDGV37EHzbG9FPvttk64cmg=', 'ENSEMBLE_MACHINE_ID=0 ENSEMBLE_ZOOKEEPER=localhost:2181
14 python -m ensemble.agents.machine -n --logfile=/var/log/ensemble/machine-agent.log
15 --pidfile=/var/run/ensemble/machine-agent.pid', 'ENSEMBLE_ZOOKEEPER=localhost:2181
16 python -m ensemble.agents.provision -n --logfile=/var/log/ensemble/provision-agent.log
17 --pidfile=/var/run/ensemble/provision-agent.pid']
18ssh_authorized_keys: [zebra]
019
=== added file 'ensemble/providers/ec2/tests/data/launch_cloud_init'
--- ensemble/providers/ec2/tests/data/launch_cloud_init 1970-01-01 00:00:00 +0000
+++ ensemble/providers/ec2/tests/data/launch_cloud_init 2011-09-13 10:56:30 +0000
@@ -0,0 +1,15 @@
1#cloud-config
2apt-update: true
3apt-upgrade: true
4apt_sources:
5- {source: 'ppa:ensemble/ppa'}
6machine-data: {ensemble-provider-type: ec2, ensemble-zookeeper-hosts: 'es.example.internal:2181',
7 machine-id: '1'}
8output: {all: '| tee -a /var/log/cloud-init-output.log'}
9packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-argparse, python-txaws,
10 python-zookeeper]
11runcmd: [sudo apt-get -y install ensemble, sudo mkdir -p /var/lib/ensemble, sudo mkdir
12 -p /var/log/ensemble, 'ENSEMBLE_MACHINE_ID=1 ENSEMBLE_ZOOKEEPER=es.example.internal:2181
13 python -m ensemble.agents.machine -n --logfile=/var/log/ensemble/machine-agent.log
14 --pidfile=/var/run/ensemble/machine-agent.pid']
15ssh_authorized_keys: [zebra]
016
=== modified file 'ensemble/providers/ec2/tests/test_bootstrap.py'
--- ensemble/providers/ec2/tests/test_bootstrap.py 2011-08-11 05:59:30 +0000
+++ ensemble/providers/ec2/tests/test_bootstrap.py 2011-09-13 10:56:30 +0000
@@ -1,6 +1,6 @@
1import logging1import logging
22import os
3from yaml import dump, load3from yaml import dump
44
5from twisted.internet.defer import succeed5from twisted.internet.defer import succeed
66
@@ -8,12 +8,11 @@
88
9from ensemble.lib.mocker import MATCH9from ensemble.lib.mocker import MATCH
10from ensemble.lib.testing import TestCase10from ensemble.lib.testing import TestCase
11from ensemble.providers.common.launch import (
12 BOOTSTRAP_PACKAGES, DEFAULT_REPOSITORIES, DEFAULT_PACKAGES)
13from ensemble.providers.ec2.machine import EC2ProviderMachine11from ensemble.providers.ec2.machine import EC2ProviderMachine
14from ensemble.state.auth import make_identity12
1513from .common import EC2TestMixin, EC2MachineLaunchMixin
16from .common import EC2TestMixin, EC2MachineLaunchMixin, MATCH_AMI14
15DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "data")
1716
1817
19class EC2BootstrapTest(EC2TestMixin, EC2MachineLaunchMixin, TestCase):18class EC2BootstrapTest(EC2TestMixin, EC2MachineLaunchMixin, TestCase):
@@ -34,55 +33,21 @@
34 MATCH(match_string))33 MATCH(match_string))
35 self.mocker.result(succeed(True))34 self.mocker.result(succeed(True))
3635
37 def _mock_launch(self, machine_id):36 def _mock_launch(self):
38 """Mock launching a bootstrap machine on ec2."""37 """Mock launching a bootstrap machine on ec2."""
39 credentials = "admin:%s" % self.get_config()["admin-secret"]
40 admin_identity = make_identity(credentials)
41
42 def verify_user_data(data):38 def verify_user_data(data):
4339 expect_path = os.path.join(DATA_DIR, "bootstrap_cloud_init")
44 lines = data.split("\n")40 with open(expect_path) as f:
45 self.assertEqual(lines.pop(0), "#cloud-config")41 expect_cloud_init = f.read()
46 config = load("\n".join(lines))42 self.assertEquals(data, expect_cloud_init)
47 repos = [dict(source=r) for r in DEFAULT_REPOSITORIES]
48 self.assertEqual(config["apt_sources"], repos)
49 self.assertEqual(
50 config["packages"],
51 list(DEFAULT_PACKAGES) + BOOTSTRAP_PACKAGES)
52 self.failUnlessIn("admin-identity", config["runcmd"][-3])
53
54 script = (
55 'ensemble-admin initialize --instance-id=%s'
56 ' --admin-identity="%s"') % (
57 "$(curl http://169.254.169.254/1.0/meta-data/instance-id)",
58 admin_identity)
59
60 self.assertEqual(config["runcmd"][-3], script)
61
62 script = (
63 "ENSEMBLE_MACHINE_ID=0 ENSEMBLE_ZOOKEEPER=localhost:2181 "
64 "python -m ensemble.agents.machine -n "
65 "--logfile=/var/log/ensemble/machine-agent.log "
66 "--pidfile=/var/run/ensemble/machine-agent.pid")
67
68 self.assertEqual(config["runcmd"][-2], script)
69
70 provision_agent_script = (
71 "ENSEMBLE_ZOOKEEPER=localhost:2181 "
72 "python -m ensemble.agents.provision -n "
73 "--logfile=/var/log/ensemble/provision-agent.log "
74 "--pidfile=/var/run/ensemble/provision-agent.pid")
75 self.assertEqual(config["runcmd"][-1], provision_agent_script)
76 return True43 return True
7744
78 self.ec2.run_instances(45 self.ec2.run_instances(
79 image_id=MATCH_AMI,46 image_id="ami-default",
80 instance_type="m1.small",47 instance_type="m1.small",
81 max_count=1,48 max_count=1,
82 min_count=1,49 min_count=1,
83 security_groups=[50 security_groups=["ensemble-moon", "ensemble-moon-0"],
84 "%s-%s" % ("ensemble", self.env_name),
85 "%s-%s-%s" % ("ensemble", self.env_name, machine_id)],
86 user_data=MATCH(verify_user_data))51 user_data=MATCH(verify_user_data))
8752
88 def test_launch_bootstrap(self):53 def test_launch_bootstrap(self):
@@ -98,7 +63,7 @@
98 self._mock_create_group()63 self._mock_create_group()
99 self._mock_create_machine_group(0)64 self._mock_create_machine_group(0)
100 self._mock_launch_utils(region="us-east-1")65 self._mock_launch_utils(region="us-east-1")
101 self._mock_launch(0)66 self._mock_launch()
102 self.mocker.result(succeed([]))67 self.mocker.result(succeed([]))
103 self._mock_save()68 self._mock_save()
104 self.mocker.replay()69 self.mocker.replay()
@@ -128,7 +93,7 @@
128 SecurityGroup("ensemble-%s" % self.env_name, "")]))93 SecurityGroup("ensemble-%s" % self.env_name, "")]))
129 self._mock_create_machine_group(0)94 self._mock_create_machine_group(0)
130 self._mock_launch_utils(region="us-east-1")95 self._mock_launch_utils(region="us-east-1")
131 self._mock_launch(0)96 self._mock_launch()
132 self.mocker.result(succeed([]))97 self.mocker.result(succeed([]))
133 self._mock_save()98 self._mock_save()
134 self.mocker.replay()99 self.mocker.replay()
@@ -177,7 +142,7 @@
177 SecurityGroup("ensemble-%s" % self.env_name, "")]))142 SecurityGroup("ensemble-%s" % self.env_name, "")]))
178 self._mock_create_machine_group(0)143 self._mock_create_machine_group(0)
179 self._mock_launch_utils(region="us-east-1")144 self._mock_launch_utils(region="us-east-1")
180 self._mock_launch(0)145 self._mock_launch()
181 self.mocker.result(succeed([self.get_instance("i-foobar")]))146 self.mocker.result(succeed([self.get_instance("i-foobar")]))
182 self._mock_save()147 self._mock_save()
183 self.mocker.replay()148 self.mocker.replay()
184149
=== modified file 'ensemble/providers/ec2/tests/test_launch.py'
--- ensemble/providers/ec2/tests/test_launch.py 2011-08-26 12:24:00 +0000
+++ ensemble/providers/ec2/tests/test_launch.py 2011-09-13 10:56:30 +0000
@@ -1,51 +1,54 @@
1from yaml import load1import os
22
3from twisted.internet.defer import inlineCallbacks, succeed3from twisted.internet.defer import inlineCallbacks, succeed
44
5from txaws.ec2.model import Instance, SecurityGroup5from txaws.ec2.model import Instance, SecurityGroup
66
7from ensemble.errors import EnvironmentNotFound, ProviderInteractionError7from ensemble.errors import (
8from ensemble.providers.common.launch import (8 EnvironmentNotFound, ProviderError, ProviderInteractionError)
9 DEFAULT_REPOSITORIES, DEFAULT_PACKAGES)
10from ensemble.providers.ec2.machine import EC2ProviderMachine9from ensemble.providers.ec2.machine import EC2ProviderMachine
1110
12from ensemble.lib.testing import TestCase11from ensemble.lib.testing import TestCase
13from ensemble.lib.mocker import MATCH12from ensemble.lib.mocker import MATCH
1413
15from .common import EC2TestMixin, EC2MachineLaunchMixin, MATCH_AMI14from .common import EC2TestMixin, EC2MachineLaunchMixin
15
16DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "data")
1617
1718
18class EC2MachineLaunchTest(EC2TestMixin, EC2MachineLaunchMixin, TestCase):19class EC2MachineLaunchTest(EC2TestMixin, EC2MachineLaunchMixin, TestCase):
1920
20 def _mock_launch(self, instance=None, custom_verify=None, machine_id=None):21 def _mock_launch(self, instance, expect_ami="ami-default",
22 expect_instance_type="m1.small"):
2123
22 def verify_user_data(data):24 def verify_user_data(data):
23 lines = data.split("\n")25 expect_path = os.path.join(DATA_DIR, "launch_cloud_init")
24 self.assertEqual(lines.pop(0), "#cloud-config")26 with open(expect_path) as f:
25 config = load("\n".join(lines))27 expect_cloud_init = f.read()
26 repos = [dict(source=r) for r in DEFAULT_REPOSITORIES]28 self.assertEquals(data, expect_cloud_init)
27 self.assertEqual(config["apt_sources"], repos)
28 self.assertEqual(config["packages"], DEFAULT_PACKAGES)
29 self.assertEqual(config["machine-data"]["machine-id"], "machine-1")
30
31 if custom_verify:
32 custom_verify(config)
33 return True29 return True
3430
35 self.ec2.run_instances(31 self.ec2.run_instances(
36 image_id=MATCH_AMI,32 image_id=expect_ami,
37 instance_type="m1.small",33 instance_type=expect_instance_type,
38 max_count=1,34 max_count=1,
39 min_count=1,35 min_count=1,
40 security_groups=[36 security_groups=["ensemble-moon", "ensemble-moon-1"],
41 "%s-%s" % ("ensemble", self.env_name),
42 "%s-%s-%s" % ("ensemble", self.env_name, machine_id)],
43 user_data=MATCH(verify_user_data))37 user_data=MATCH(verify_user_data))
4438
45 if instance:39 self.mocker.result(succeed([instance]))
46 self.mocker.result(succeed([instance]))40
47 else:41 def test_bad_data(self):
48 self.mocker.result(succeed([]))42 self.mocker.replay()
43 d = self.get_provider().start_machine({})
44 self.assertFailure(d, ProviderError)
45
46 def verify(error):
47 self.assertEquals(
48 str(error),
49 "Cannot launch a machine without specifying a machine-id")
50 d.addCallback(verify)
51 return d
4952
50 def test_provider_launch(self):53 def test_provider_launch(self):
51 """54 """
@@ -55,18 +58,17 @@
55 self.ec2.describe_security_groups()58 self.ec2.describe_security_groups()
56 self.mocker.result(succeed([]))59 self.mocker.result(succeed([]))
57 self._mock_create_group()60 self._mock_create_group()
58 self._mock_create_machine_group("machine-1")61 self._mock_create_machine_group("1")
59 self._mock_launch_utils(region="us-east-1")62 self._mock_launch_utils(region="us-east-1")
60 self._mock_get_zookeeper_hosts()63 self._mock_get_zookeeper_hosts()
61 self._mock_launch(64 self._mock_launch(self.get_instance("i-foobar"))
62 self.get_instance("i-foobar"), machine_id="machine-1")
63 self.mocker.replay()65 self.mocker.replay()
6466
65 def verify_result(result):67 def verify_result(result):
66 (machine,) = result68 (machine,) = result
67 self.assert_machine(machine, "i-foobar", "")69 self.assert_machine(machine, "i-foobar", "")
68 provider = self.get_provider()70 provider = self.get_provider()
69 d = provider.start_machine({"machine-id": "machine-1"})71 d = provider.start_machine({"machine-id": "1"})
70 d.addCallback(verify_result)72 d.addCallback(verify_result)
71 return d73 return d
7274
@@ -78,15 +80,14 @@
7880
79 self.ec2.describe_security_groups()81 self.ec2.describe_security_groups()
80 self.mocker.result(succeed([security_group]))82 self.mocker.result(succeed([security_group]))
81 self._mock_create_machine_group("machine-1")83 self._mock_create_machine_group("1")
82 self._mock_launch_utils(region="us-east-1")84 self._mock_launch_utils(region="us-east-1")
83 self._mock_get_zookeeper_hosts()85 self._mock_get_zookeeper_hosts()
84 self._mock_launch(instance, machine_id="machine-1")86 self._mock_launch(instance)
85 self.mocker.replay()87 self.mocker.replay()
8688
87 provider = self.get_provider()89 provider = self.get_provider()
88 provided_machines = yield provider.start_machine(90 provided_machines = yield provider.start_machine({"machine-id": "1"})
89 {"machine-id": "machine-1"})
90 self.assertEqual(len(provided_machines), 1)91 self.assertEqual(len(provided_machines), 1)
91 self.assertTrue(isinstance(provided_machines[0], EC2ProviderMachine))92 self.assertTrue(isinstance(provided_machines[0], EC2ProviderMachine))
92 self.assertEqual(93 self.assertEqual(
@@ -97,21 +98,20 @@
97 """Verify that the launch works if the machine security group exists"""98 """Verify that the launch works if the machine security group exists"""
98 instance = Instance("i-foobar", "running", dns_name="x1.example.com")99 instance = Instance("i-foobar", "running", dns_name="x1.example.com")
99 machine_group = SecurityGroup(100 machine_group = SecurityGroup(
100 "ensemble-moon-machine-1", "some description")101 "ensemble-moon-1", "some description")
101102
102 self.ec2.describe_security_groups()103 self.ec2.describe_security_groups()
103 self.mocker.result(succeed([machine_group]))104 self.mocker.result(succeed([machine_group]))
104 self._mock_create_group()105 self._mock_create_group()
105 self._mock_delete_machine_group("machine-1") # delete existing sg106 self._mock_delete_machine_group("1") # delete existing sg
106 self._mock_create_machine_group("machine-1") # then recreate107 self._mock_create_machine_group("1") # then recreate
107 self._mock_launch_utils(region="us-east-1")108 self._mock_launch_utils(region="us-east-1")
108 self._mock_get_zookeeper_hosts()109 self._mock_get_zookeeper_hosts()
109 self._mock_launch(instance, machine_id="machine-1")110 self._mock_launch(instance)
110 self.mocker.replay()111 self.mocker.replay()
111112
112 provider = self.get_provider()113 provider = self.get_provider()
113 provided_machines = yield provider.start_machine(114 provided_machines = yield provider.start_machine({"machine-id": "1"})
114 {"machine-id": "machine-1"})
115 self.assertEqual(len(provided_machines), 1)115 self.assertEqual(len(provided_machines), 1)
116 self.assertTrue(isinstance(provided_machines[0], EC2ProviderMachine))116 self.assertTrue(isinstance(provided_machines[0], EC2ProviderMachine))
117 self.assertEqual(117 self.assertEqual(
@@ -125,49 +125,27 @@
125 that security group, generally because it is still shutting125 that security group, generally because it is still shutting
126 down."""126 down."""
127 machine_group = SecurityGroup(127 machine_group = SecurityGroup(
128 "ensemble-moon-machine-1", "some description")128 "ensemble-moon-1", "some description")
129129
130 self.ec2.describe_security_groups()130 self.ec2.describe_security_groups()
131 self.mocker.result(succeed([machine_group]))131 self.mocker.result(succeed([machine_group]))
132 self._mock_create_group()132 self._mock_create_group()
133 self._mock_delete_machine_group_was_deleted("machine-1") # sg is gone!133 self._mock_delete_machine_group_was_deleted("1") # sg is gone!
134 self._mock_launch_utils(region="us-east-1")134 self._mock_launch_utils(region="us-east-1")
135 self._mock_get_zookeeper_hosts()135 self._mock_get_zookeeper_hosts()
136 self.mocker.replay()136 self.mocker.replay()
137137
138 provider = self.get_provider()138 provider = self.get_provider()
139 ex = yield self.assertFailure(139 ex = yield self.assertFailure(
140 provider.start_machine({"machine-id": "machine-1"}),140 provider.start_machine({"machine-id": "1"}),
141 ProviderInteractionError)141 ProviderInteractionError)
142 self.assertEqual(142 self.assertEqual(
143 str(ex),143 str(ex),
144 "Unexpected EC2Error deleting security group "144 "Unexpected EC2Error deleting security group "
145 "ensemble-moon-machine-1: There are active instances using "145 "ensemble-moon-1: There are active instances using security group "
146 "security group 'ensemble-moon-machine-1'")146 "'ensemble-moon-1'")
147
148 def test_provider_type_machine_variable(self):
149 """The provider type is available via cloud-init."""
150 self.ec2.describe_security_groups()
151 self.mocker.result(succeed([]))
152 self._mock_create_group()
153 self._mock_create_machine_group("machine-1")
154 self._mock_launch_utils(region="us-east-1")
155 self._mock_get_zookeeper_hosts()
156
157 def verify_provider_type(data):
158 self.assertEqual(
159 data["machine-data"]["ensemble-provider-type"], "ec2")
160
161 self._mock_launch(custom_verify=verify_provider_type,
162 machine_id="machine-1")
163 self.mocker.replay()
164
165 provider = self.get_provider()
166 d = provider.start_machine({"machine-id": "machine-1"})
167 return d
168147
169 def test_launch_with_no_ensemble_s3_state(self):148 def test_launch_with_no_ensemble_s3_state(self):
170 # XXX not really testing anything EC2-specific
171 """149 """
172 Attempting to launch without any ensemble saved state, means150 Attempting to launch without any ensemble saved state, means
173 we can't provide a way for a launched instance to connect151 we can't provide a way for a launched instance to connect
@@ -178,12 +156,11 @@
178 self.mocker.replay()156 self.mocker.replay()
179157
180 provider = self.get_provider()158 provider = self.get_provider()
181 d = provider.start_machine({"machine-id": "machine-1"})159 d = provider.start_machine({"machine-id": "1"})
182 self.assertFailure(d, EnvironmentNotFound)160 self.assertFailure(d, EnvironmentNotFound)
183 return d161 return d
184162
185 def test_launch_with_no_ensemble_zookeeper_hosts(self):163 def test_launch_with_no_ensemble_zookeeper_hosts(self):
186 # XXX not really testing anything EC2-specific
187 """164 """
188 Attempting to launch without any ensemble zookeeper hosts, means165 Attempting to launch without any ensemble zookeeper hosts, means
189 we can't provide a way for a launched instance to connect166 we can't provide a way for a launched instance to connect
@@ -194,55 +171,42 @@
194 self.mocker.replay()171 self.mocker.replay()
195172
196 provider = self.get_provider()173 provider = self.get_provider()
197 d = provider.start_machine({"machine-id": "machine-1"})174 d = provider.start_machine({"machine-id": "1"})
198 self.assertFailure(d, EnvironmentNotFound)175 self.assertFailure(d, EnvironmentNotFound)
199 return d176 return d
200177
201 def test_launch_starts_machine_agent(self):178 def test_launch_options_known_instance_type(self):
202 # XXX not really testing anything EC2-specific
203 """A launch'd machine should have a machine agent."""
204 self.ec2.describe_security_groups()179 self.ec2.describe_security_groups()
205 self.mocker.result(succeed([]))180 self.mocker.result(succeed([]))
206 self._mock_create_group()181 self._mock_create_group()
207 self._mock_create_machine_group("machine-1")182 self._mock_create_machine_group("1")
208 self._mock_launch_utils(region="us-east-1")183 self._mock_launch_utils(region="us-east-1")
209 self._mock_get_zookeeper_hosts()184 self._mock_get_zookeeper_hosts()
210
211 def verify_machine_agent(data):
212 script = ("ENSEMBLE_MACHINE_ID=machine-1 "
213 "ENSEMBLE_ZOOKEEPER=es.example.internal:2181 "
214 "python -m ensemble.agents.machine "
215 "-n --logfile=/var/log/ensemble/machine-agent.log "
216 "--pidfile=/var/run/ensemble/machine-agent.pid")
217 self.assertEqual(script, data["runcmd"][-1])
218
219 self._mock_launch(custom_verify=verify_machine_agent,
220 machine_id="machine-1")
221 self.mocker.replay()
222
223 provider = self.get_provider()
224 d = provider.start_machine({"machine-id": "machine-1"})
225 return d
226
227 def _mock_launch_with_ami_params(self, get_ami_kwargs):
228 self.ec2.describe_security_groups()
229 self.mocker.result(succeed([]))
230 self._mock_create_group()
231 self._mock_create_machine_group("machine-1")
232 self._mock_launch_utils(**get_ami_kwargs)
233 self._mock_get_zookeeper_hosts()
234 self._mock_launch(185 self._mock_launch(
235 self.get_instance("i-foobar"), machine_id="machine-1")186 self.get_instance("i-foobar"), expect_instance_type="m1.ginormous")
187 self.mocker.replay()
188
189 provider = self.get_provider()
190 provider.config["default-instance-type"] = "m1.ginormous"
191 return provider.start_machine({"machine-id": "1"})
192
193 def _mock_launch_with_ami_params(self, get_ami_kwargs, expect_ami=None):
194 expect_ami = expect_ami or "ami-default"
195 self.ec2.describe_security_groups()
196 self.mocker.result(succeed([]))
197 self._mock_create_group()
198 self._mock_create_machine_group("1")
199 self._mock_launch_utils(ami_name=expect_ami, **get_ami_kwargs)
200 self._mock_get_zookeeper_hosts()
201 self._mock_launch(self.get_instance("i-foobar"), expect_ami)
236202
237 def test_launch_options_known_ami(self):203 def test_launch_options_known_ami(self):
238 self._mock_launch_with_ami_params({})204 self._mock_launch_with_ami_params({}, expect_ami="ami-different")
239 self.mocker.replay()205 self.mocker.replay()
240206
241 provider = self.get_provider()207 provider = self.get_provider()
242 # TODO shouldn't something be testing we actually launch208 provider.config["default-image-id"] = "ami-different"
243 # an instance of the expected AMI?209 return provider.start_machine({"machine-id": "1"})
244 provider.config["default-image-id"] = "ami-XXXXXX"
245 return provider.start_machine({"machine-id": "machine-1"})
246210
247 def test_launch_options_region(self):211 def test_launch_options_region(self):
248 self._mock_launch_with_ami_params({"region": "somewhere-else-1"})212 self._mock_launch_with_ami_params({"region": "somewhere-else-1"})
@@ -250,21 +214,22 @@
250214
251 provider = self.get_provider()215 provider = self.get_provider()
252 provider.config["region"] = "somewhere-else-1"216 provider.config["region"] = "somewhere-else-1"
253 return provider.start_machine({"machine-id": "machine-1"})217 return provider.start_machine({"machine-id": "1"})
254218
255 def test_launch_options_other(self):219 def test_launch_options_custom_image_options(self):
256 self._mock_launch_with_ami_params({220 self._mock_launch_with_ami_params({
257 "region": "us-east-1",221 "region": "us-east-1",
258 "ubuntu_release_name": "choleric",222 "ubuntu_release": "choleric",
259 "arch": "quantum86",223 "architecture": "quantum86",
260 "ebs": "maybe",224 "persistent_storage": "maybe",
261 "daily": "perhaps"})225 "daily": "perhaps"})
262 self.mocker.replay()226 self.mocker.replay()
263227
264 provider = self.get_provider()228 provider = self.get_provider()
265 return provider.start_machine({229 constraints = {
266 "machine-id": "machine-1",230 "ubuntu_release": "choleric",
267 "image_release_name": "choleric",231 "architecture": "quantum86",
268 "image_arch": "quantum86",232 "persistent_storage": "maybe",
269 "image_ebs": "maybe",233 "daily": "perhaps"}
270 "image_daily": "perhaps"})234 return provider.start_machine({"machine-id": "1",
235 "constraints": constraints})
271236
=== modified file 'ensemble/providers/ec2/tests/test_utils.py'
--- ensemble/providers/ec2/tests/test_utils.py 2011-08-12 19:54:54 +0000
+++ ensemble/providers/ec2/tests/test_utils.py 2011-09-13 10:56:30 +0000
@@ -1,14 +1,12 @@
1import inspect1import inspect
2import os2import os
33
4
5from twisted.internet.defer import succeed4from twisted.internet.defer import succeed
65
6from ensemble.lib.testing import TestCase
7from ensemble.providers import ec27from ensemble.providers import ec2
8from ensemble.providers.common.utils import format_cloud_init8from ensemble.providers.ec2.utils import get_current_ami, get_image_id
9from ensemble.providers.ec2.utils import get_current_ami, get_launch_options
109
11from ensemble.lib.testing import TestCase
1210
13IMAGE_URI_TEMPLATE = "\11IMAGE_URI_TEMPLATE = "\
14http://uec-images.ubuntu.com/query/%s/server/released.current.txt"12http://uec-images.ubuntu.com/query/%s/server/released.current.txt"
@@ -17,7 +15,7 @@
17 os.path.dirname(inspect.getabsfile(ec2)), "tests", "data")15 os.path.dirname(inspect.getabsfile(ec2)), "tests", "data")
1816
1917
20class EC2UtilsTest(TestCase):18class GetCurrentAmiTest(TestCase):
2119
22 def test_umatched_ami(self):20 def test_umatched_ami(self):
23 """21 """
@@ -28,7 +26,7 @@
28 page(IMAGE_URI_TEMPLATE % "lucid")26 page(IMAGE_URI_TEMPLATE % "lucid")
29 self.mocker.result(succeed(""))27 self.mocker.result(succeed(""))
30 self.mocker.replay()28 self.mocker.replay()
31 d = get_current_ami(ubuntu_release_name="lucid")29 d = get_current_ami(ubuntu_release="lucid")
32 self.failUnlessFailure(d, LookupError)30 self.failUnlessFailure(d, LookupError)
33 return d31 return d
3432
@@ -40,7 +38,7 @@
40 open(os.path.join(IMAGE_DATA_DIR, "lucid.txt")).read()))38 open(os.path.join(IMAGE_DATA_DIR, "lucid.txt")).read()))
4139
42 self.mocker.replay()40 self.mocker.replay()
43 d = get_current_ami(ubuntu_release_name="lucid")41 d = get_current_ami(ubuntu_release="lucid")
4442
45 def verify_result(result):43 def verify_result(result):
46 self.assertEqual(result, "ami-714ba518")44 self.assertEqual(result, "ami-714ba518")
@@ -56,7 +54,7 @@
56 succeed(open(54 succeed(open(
57 os.path.join(IMAGE_DATA_DIR, "lucid.txt")).read()))55 os.path.join(IMAGE_DATA_DIR, "lucid.txt")).read()))
58 self.mocker.replay()56 self.mocker.replay()
59 d = get_current_ami(ubuntu_release_name="lucid", region="us-west-1")57 d = get_current_ami(ubuntu_release="lucid", region="us-west-1")
6058
61 def verify_result(result):59 def verify_result(result):
62 self.assertEqual(result, "ami-cb97c68e")60 self.assertEqual(result, "ami-cb97c68e")
@@ -74,7 +72,7 @@
74 self.mocker.result(succeed(72 self.mocker.result(succeed(
75 open(os.path.join(IMAGE_DATA_DIR, "lucid.txt")).read()))73 open(os.path.join(IMAGE_DATA_DIR, "lucid.txt")).read()))
76 self.mocker.replay()74 self.mocker.replay()
77 d = get_current_ami(ubuntu_release_name="lucid", ebs=False)75 d = get_current_ami(ubuntu_release="lucid", persistent_storage=False)
7876
79 def verify_result(result):77 def verify_result(result):
80 self.assertEqual(result, "ami-2d4aa444")78 self.assertEqual(result, "ami-2d4aa444")
@@ -82,101 +80,37 @@
82 d.addCallback(verify_result)80 d.addCallback(verify_result)
83 return d81 return d
8482
85 def test_get_launch_options(self):83
86 """84class GetImageIdTest(TestCase):
87 F{get_launch_options}, returns a dictionary of arguments for the ec285
88 client's run instance method.86 def test_default_image_id(self):
89 """87 d = get_image_id({"default-image-id": "ami-burble"}, {})
90 config = {}88 d.addCallback(self.assertEquals, "ami-burble")
91 config["default-image-id"] = "ami-foobar"89 return d
92 config["default-instance-type"] = "m1.medium"90
93 authorized_keys = "zebra"91 def test_no_constraints(self):
94 repositories = ["ppa:ensemble/ppa"]92 get_current_ami_m = self.mocker.replace(get_current_ami)
95 packages = ["python-zookeeper", "zookeeperd"]93 get_current_ami_m(region="us-east-1")
9694 self.mocker.result(succeed("ami-giggle"))
97 d = get_launch_options(95 self.mocker.replay()
98 config,96
99 authorized_keys=authorized_keys,97 d = get_image_id({}, {})
100 packages=packages,98 d.addCallback(self.assertEquals, "ami-giggle")
101 repositories=repositories,99 return d
102 magic=[1, 2, 3])100
103101 def test_uses_constraints(self):
104 cloud_init_config = format_cloud_init(102 get_current_ami_m = self.mocker.replace(get_current_ami)
105 authorized_keys, packages, repositories,103 get_current_ami_m(ubuntu_release="serendipitous", architecture="x512",
106 data={"magic": [1, 2, 3]})104 daily=False, persistent_storage=True,
107105 region="blah-north-6")
108 def verify_result(result):106 self.mocker.result(succeed("ami-tinkle"))
109 self.assertTrue(isinstance(result, dict))107 self.mocker.replay()
110 self.assertEqual(result["image_id"], "ami-foobar")108
111 self.assertEqual(result["user_data"], cloud_init_config)109 constraints = {
112 self.assertEqual(result["security_groups"], [])110 "architecture": "x512",
113111 "ubuntu_release": "serendipitous",
114 d.addCallback(verify_result)112 "persistent_storage": True,
115 return d113 "daily": False}
116114 d = get_image_id({"region": "blah-north-6"}, constraints)
117 def test_get_launch_options_defaults(self):115 d.addCallback(self.assertEquals, "ami-tinkle")
118 """
119 F{get_launch_options}, returns a dictionary of arguments for
120 the ec2 client's run instance method. A known set of default
121 is utilized.
122 """
123
124 page = self.mocker.replace("twisted.web.client.getPage")
125 page(IMAGE_URI_TEMPLATE % "natty")
126 self.mocker.result(
127 succeed(open(
128 os.path.join(IMAGE_DATA_DIR, "natty.txt")).read()))
129 self.mocker.replay()
130
131 config = {}
132 d = get_launch_options(config, "zebra")
133
134 def verify_result(result):
135 self.assertTrue(isinstance(result, dict))
136 self.assertEqual(result["image_id"], "ami-06ad526f")
137 self.assertEqual(result["instance_type"], "m1.small")
138
139 d.addCallback(verify_result)
140 return d
141
142 def test_get_launch_options_with_params(self):
143 """
144 Image parameters can be specified to determine which ami is to
145 be launched. Image paramters specified are not passed to the
146 instance via user data.
147 """
148 page = self.mocker.replace("twisted.web.client.getPage")
149 page(IMAGE_URI_TEMPLATE % "natty")
150 self.mocker.result(
151 succeed(open(
152 os.path.join(IMAGE_DATA_DIR, "natty.txt")).read()))
153 self.mocker.replay()
154
155 d = get_launch_options(
156 {"default-instance-type": "m1.large"},
157 "zebra", image_arch="amd64", image_ebs=False)
158
159 def verify_result(result):
160 self.assertTrue(isinstance(result, dict))
161 self.assertEqual(result["image_id"], "ami-68ad5201")
162 self.assertEqual(result["instance_type"], "m1.large")
163 self.assertNotIn(
164 "arch", result["user_data"])
165 self.assertNotIn(
166 "ebs", result["user_data"])
167
168 d.addCallback(verify_result)
169 return d
170
171 def test_configured_default_image_has_precedence_over_region(self):
172 """If the server machine image is specified it takes precendence over
173 region."""
174 d = get_launch_options(
175 {"region": "eu-west-1", "default-image-id": "21down"},
176 authorized_keys="")
177
178 def verify_result(result):
179 self.assertEqual(result["image_id"], "21down")
180
181 d.addCallback(verify_result)
182 return d116 return d
183117
=== modified file 'ensemble/providers/ec2/utils.py'
--- ensemble/providers/ec2/utils.py 2011-08-14 19:24:03 +0000
+++ ensemble/providers/ec2/utils.py 2011-09-13 10:56:30 +0000
@@ -5,22 +5,11 @@
5from twisted.web.client import getPage5from twisted.web.client import getPage
6from twisted.internet.defer import succeed6from twisted.internet.defer import succeed
77
8from ensemble.providers.common.utils import format_cloud_init
9
10log = logging.getLogger("ensemble.ec2")8log = logging.getLogger("ensemble.ec2")
119
12# default image for each region.10_CURRENT_IMAGE_URI_TEMPLATE = (
13AMI_REGION_MAP = {11 "http://uec-images.ubuntu.com/query/"
14 "us-east-1": "ami-d2ba45bb",12 "%(ubuntu_release_name)s/%(variant)s/%(version)s.current.txt")
15 "us-west-1": "ami-15693a50",
16 "ap-northeast-1": "ami-80bb1181",
17 "ap-southeast-1": "ami-b4671ee6",
18 "eu-west-1": "ami-0991a67d"
19 }
20
21BASE_IMAGE_URI = "http://uec-images.ubuntu.com/query/"
22CURRENT_IMAGE_URI = BASE_IMAGE_URI + \
23 "%(ubuntu_release_name)s/%(variant)s/%(version)s.current.txt"
2413
2514
26def get_region_uri(region):15def get_region_uri(region):
@@ -28,17 +17,18 @@
28 return "https://ec2.%s.amazonaws.com" % region17 return "https://ec2.%s.amazonaws.com" % region
2918
3019
31def get_current_ami(ubuntu_release_name="natty", arch="i386", ebs=True,20# XXX ideally should come from latest available or client release name.
32 region="us-east-1", daily=False, desktop=False,21def get_current_ami(ubuntu_release="natty", architecture="i386",
33 url_fetch=None):22 persistent_storage=True, region="us-east-1", daily=False,
23 desktop=False, url_fetch=None):
34 """Get the latest ami for the last release of ubuntu."""24 """Get the latest ami for the last release of ubuntu."""
35 data = {}25 data = {}
36 data["ubuntu_release_name"] = ubuntu_release_name26 data["ubuntu_release_name"] = ubuntu_release
37 data["version"] = daily and "daily" or "released"27 data["version"] = daily and "daily" or "released"
38 data["variant"] = desktop and "desktop" or "server"28 data["variant"] = desktop and "desktop" or "server"
39 ebs_match = ebs and "ebs" or "instance-store"29 ebs_match = persistent_storage and "ebs" or "instance-store"
4030
41 url = CURRENT_IMAGE_URI % data31 url = _CURRENT_IMAGE_URI_TEMPLATE % data
42 url_fetch = url_fetch or getPage32 url_fetch = url_fetch or getPage
4333
44 d = url_fetch(url)34 d = url_fetch(url)
@@ -48,60 +38,18 @@
48 for tokens in csv.reader(data_stream, "excel-tab"):38 for tokens in csv.reader(data_stream, "excel-tab"):
49 if tokens[4] != ebs_match:39 if tokens[4] != ebs_match:
50 continue40 continue
51 if tokens[5] == arch and tokens[6] == region:41 if tokens[5] == architecture and tokens[6] == region:
52 return tokens[7]42 return tokens[7]
53 raise LookupError((ubuntu_release_name, arch, region,43 raise LookupError((ubuntu_release, architecture, region,
54 data["version"], data["variant"]))44 data["version"], data["variant"]))
5545
56 d.addCallback(extract_ami)46 d.addCallback(extract_ami)
57 return d47 return d
5848
5949
60def get_launch_options(config, authorized_keys, packages=None,50def get_image_id(config, constraints):
61 repositories=None, scripts=None, **kw):51 image_id = config.get("default-image-id", None)
62 """Determine launch options for the machine.52 if image_id:
6353 return succeed(image_id)
64 Given an ensemble environment configuration, extract the proper machine
65 information to be used for launching the machine via the ec2 api.
66 """
67
68 instance_type = config.get("default-instance-type", "m1.small")
69
70 # XXX ideally should come from latest available or client release name.
71 region = config.get("region", "us-east-1")54 region = config.get("region", "us-east-1")
72 image_id = config.get("default-image-id", None)55 return get_current_ami(region=region, **constraints)
73
74 # Lookup a standard ami by region, if none is specified, use any
75 # specified image specification to locate an image, and remove the
76 # image specification so it doesn't propogate to user data.
77 if not image_id:
78 params = {}
79 for a_name, p_name in (
80 ("image_release_name", "ubuntu_release_name"),
81 ("image_arch", "arch"),
82 ("image_ebs", "ebs"),
83 ("image_daily", "daily")):
84 if a_name in kw:
85 params[p_name] = kw[a_name]
86 del kw[a_name]
87 params["region"] = region
88 d = get_current_ami(**params)
89 else:
90 d = succeed(image_id)
91
92 user_data = format_cloud_init(
93 authorized_keys=authorized_keys,
94 packages=packages,
95 repositories=repositories,
96 scripts=scripts,
97 data=kw)
98
99 def on_machine_image(current_image_id):
100 return {"image_id": current_image_id,
101 "min_count": 1,
102 "max_count": 1,
103 "instance_type": instance_type,
104 "user_data": user_data,
105 "security_groups": []}
106 d.addCallback(on_machine_image)
107 return d
10856
=== modified file 'ensemble/providers/orchestra/__init__.py'
--- ensemble/providers/orchestra/__init__.py 2011-08-25 16:41:03 +0000
+++ ensemble/providers/orchestra/__init__.py 2011-09-13 10:56:30 +0000
@@ -1,6 +1,6 @@
1import logging1import logging
22
3from twisted.internet.defer import inlineCallbacks, returnValue, succeed3from twisted.internet.defer import fail, inlineCallbacks, returnValue, succeed
44
5from ensemble.errors import ProviderError5from ensemble.errors import ProviderError
6from ensemble.providers.common.base import MachineProviderBase6from ensemble.providers.common.base import MachineProviderBase
@@ -29,13 +29,16 @@
29 def start_machine(self, machine_data, master=False):29 def start_machine(self, machine_data, master=False):
30 """Start a machine in the provider.30 """Start a machine in the provider.
3131
32 @param machine_data: a dictionary of data to pass along to the newly32 `machine_data`: a dictionary describing the machine to be launched.
33 launched machine.
3433
35 @param master: if True, machine will initialize the ensemble admin34 `master`: if True, machine will initialize the ensemble admin and run
36 and run a provisioning agent.35 a provisioning agent, in addition to running a machine agent
37 """36 """
38 return OrchestraLaunchMachine(self, master).run(machine_data)37 if "machine-id" not in machine_data:
38 return fail(ProviderError(
39 "Cannot launch a machine without specifying a machine-id"))
40 machine_id = machine_data["machine-id"]
41 return OrchestraLaunchMachine(self, master).run(machine_id)
3942
40 @inlineCallbacks43 @inlineCallbacks
41 def get_machines(self, instance_ids=()):44 def get_machines(self, instance_ids=()):
4245
=== modified file 'ensemble/providers/orchestra/launch.py'
--- ensemble/providers/orchestra/launch.py 2011-08-11 23:27:45 +0000
+++ ensemble/providers/orchestra/launch.py 2011-09-13 10:56:30 +0000
@@ -5,14 +5,8 @@
5from twisted.internet.defer import inlineCallbacks, returnValue5from twisted.internet.defer import inlineCallbacks, returnValue
66
7from ensemble.providers.common.launch import LaunchMachine7from ensemble.providers.common.launch import LaunchMachine
8from ensemble.providers.common.utils import format_cloud_init8
9from ensemble.providers.orchestra.machine import machine_from_dict9from .machine import machine_from_dict
10
11_INSTANCE_ID_VAR = "ENSEMBLE_INSTANCE_ID"
12
13_SET_INSTANCE_ID_TEMPLATE = "export %s=%%s" % _INSTANCE_ID_VAR
14
15_CLOUD_INIT_KEYS = "authorized_keys packages repositories scripts".split()
1610
17_LATE_COMMAND_TEMPLATE = """11_LATE_COMMAND_TEMPLATE = """
18seed_d=/var/lib/cloud/seed/nocloud-net12seed_d=/var/lib/cloud/seed/nocloud-net
@@ -36,17 +30,6 @@
36 "' %s /root/late-command")30 "' %s /root/late-command")
3731
3832
39def _cloud_init_vars(raw_vars):
40 # XXX this is nasty; see bug#820892
41 vars = raw_vars.copy()
42 data = {}
43 for key in vars.keys():
44 if key not in _CLOUD_INIT_KEYS:
45 data[key] = vars.pop(key)
46 vars["data"] = data
47 return vars
48
49
50def _base64_gzip(content):33def _base64_gzip(content):
51 gzipped = StringIO()34 gzipped = StringIO()
52 gzip_file = GzipFile(fileobj=gzipped, mode="wb", compresslevel=9)35 gzip_file = GzipFile(fileobj=gzipped, mode="wb", compresslevel=9)
@@ -62,28 +45,34 @@
62class OrchestraLaunchMachine(LaunchMachine):45class OrchestraLaunchMachine(LaunchMachine):
6346
64 @inlineCallbacks47 @inlineCallbacks
65 def start_machine(self, variables, machine_id):48 def start_machine(self, machine_id, zookeepers):
49 """Actually launch a Cobbler system.
50
51 `machine_id`: the ensemble machine ID
52
53 `zookeepers`: a list of `ProviderMachine`s already running zookeeper,
54 which the machine will need to connect to. May be empty.
55
56 Returns a singe-entry list containing a ProviderMachine for the
57 new instance
58 """
66 cobbler = self._provider.cobbler59 cobbler = self._provider.cobbler
67 instance_id = yield cobbler.acquire_system()60 instance_id = yield cobbler.acquire_system()
68 ks_meta = self._build_ks_meta(instance_id, variables)61
62 cloud_init = self._create_cloud_init(machine_id, zookeepers)
63 cloud_init.set_provider_type("orchestra")
64 cloud_init.set_instance_id_accessor(instance_id)
65 ks_meta = self._build_ks_meta(
66 cloud_init.render(), machine_id, instance_id)
67
69 yield cobbler.set_on_system(instance_id, "ks_meta", ks_meta)68 yield cobbler.set_on_system(instance_id, "ks_meta", ks_meta)
70 yield cobbler.start_system(instance_id)69 yield cobbler.start_system(instance_id)
71 (info,) = yield cobbler.describe_systems(instance_id)70 (info,) = yield cobbler.describe_systems(instance_id)
72 returnValue([machine_from_dict(info)])71 returnValue([machine_from_dict(info)])
7372
74 def get_instance_id_command(self):73 def _build_ks_meta(self, user_data, machine_id, instance_id):
75 return "$" + _INSTANCE_ID_VAR
76
77 def _build_ks_meta(self, instance_id, variables):
78 # XXX it's a little bit dirty to modify scripts at this stage;
79 # but we don't actually know the instance_id at get_variables time.
80 set_instance_id = _SET_INSTANCE_ID_TEMPLATE % instance_id
81 variables["scripts"].insert(0, set_instance_id)
82
83 cloud_init_vars = _cloud_init_vars(variables)
84 user_data = format_cloud_init(**cloud_init_vars)
85 late_command = _LATE_COMMAND_TEMPLATE % (instance_id, user_data)74 late_command = _LATE_COMMAND_TEMPLATE % (instance_id, user_data)
86 encoded = _base64_gzip(late_command)75 encoded = _base64_gzip(late_command)
87 ensemble_late_command = _KSMETA_LATE_COMMAND_TEMPLATE % encoded76 ensemble_late_command = _KSMETA_LATE_COMMAND_TEMPLATE % encoded
88 return _ks_meta(MACHINE_ID=variables["machine-id"],77 return _ks_meta(
89 ENSEMBLE_LATE_COMMAND=ensemble_late_command)78 MACHINE_ID=machine_id, ENSEMBLE_LATE_COMMAND=ensemble_late_command)
9079
=== modified file 'ensemble/providers/orchestra/tests/common.py'
--- ensemble/providers/orchestra/tests/common.py 2011-08-11 23:27:45 +0000
+++ ensemble/providers/orchestra/tests/common.py 2011-09-13 10:56:30 +0000
@@ -108,7 +108,8 @@
108 late_command = _extract_late_command(leftover)108 late_command = _extract_late_command(leftover)
109109
110 expect_path = os.path.join(DATA_DIR, late_command_filename)110 expect_path = os.path.join(DATA_DIR, late_command_filename)
111 expect_late_command = open(expect_path).read()111 with open(expect_path) as f:
112 expect_late_command = f.read()
112 self.assertEquals(late_command, expect_late_command)113 self.assertEquals(late_command, expect_late_command)
113 return True114 return True
114 return verify115 return verify
115116
=== modified file 'ensemble/providers/orchestra/tests/data/bootstrap_late_command'
--- ensemble/providers/orchestra/tests/data/bootstrap_late_command 2011-09-07 22:06:57 +0000
+++ ensemble/providers/orchestra/tests/data/bootstrap_late_command 2011-09-13 10:56:30 +0000
@@ -11,13 +11,13 @@
11apt-upgrade: true11apt-upgrade: true
12apt_sources:12apt_sources:
13- {source: 'ppa:ensemble/ppa'}13- {source: 'ppa:ensemble/ppa'}
14machine-data: {ensemble-zookeeper-hosts: 'localhost:2181', machine-id: '0'}14machine-data: {ensemble-provider-type: orchestra, ensemble-zookeeper-hosts: 'localhost:2181',
15 machine-id: '0'}
15output: {all: '| tee -a /var/log/cloud-init-output.log'}16output: {all: '| tee -a /var/log/cloud-init-output.log'}
16packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-argparse, python-txaws,17packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-argparse, python-txaws,
17 python-zookeeper, bzr, default-jre-headless, zookeeper, zookeeperd]18 python-zookeeper, default-jre-headless, zookeeper, zookeeperd]
18runcmd: [export ENSEMBLE_INSTANCE_ID=winston-uid, sudo apt-get -y install ensemble,19runcmd: [sudo apt-get -y install ensemble, sudo mkdir -p /var/lib/ensemble, sudo mkdir
19 sudo mkdir -p /var/lib/ensemble, sudo mkdir -p /var/log/ensemble, 'ensemble-admin20 -p /var/log/ensemble, 'ensemble-admin initialize --instance-id=winston-uid --admin-identity=admin:qRBXC1ubEEUqRL6wcBhgmc9xkaY=',
20 initialize --instance-id=$ENSEMBLE_INSTANCE_ID --admin-identity="admin:qRBXC1ubEEUqRL6wcBhgmc9xkaY="',
21 'ENSEMBLE_MACHINE_ID=0 ENSEMBLE_ZOOKEEPER=localhost:2181 python -m ensemble.agents.machine21 'ENSEMBLE_MACHINE_ID=0 ENSEMBLE_ZOOKEEPER=localhost:2181 python -m ensemble.agents.machine
22 -n --logfile=/var/log/ensemble/machine-agent.log --pidfile=/var/run/ensemble/machine-agent.pid',22 -n --logfile=/var/log/ensemble/machine-agent.log --pidfile=/var/run/ensemble/machine-agent.pid',
23 'ENSEMBLE_ZOOKEEPER=localhost:2181 python -m ensemble.agents.provision -n --logfile=/var/log/ensemble/provision-agent.log23 'ENSEMBLE_ZOOKEEPER=localhost:2181 python -m ensemble.agents.provision -n --logfile=/var/log/ensemble/provision-agent.log
2424
=== modified file 'ensemble/providers/orchestra/tests/data/launch_late_command'
--- ensemble/providers/orchestra/tests/data/launch_late_command 2011-09-07 22:06:57 +0000
+++ ensemble/providers/orchestra/tests/data/launch_late_command 2011-09-13 10:56:30 +0000
@@ -11,13 +11,14 @@
11apt-upgrade: true11apt-upgrade: true
12apt_sources:12apt_sources:
13- {source: 'ppa:ensemble/ppa'}13- {source: 'ppa:ensemble/ppa'}
14machine-data: {ensemble-zookeeper-hosts: 'jennifer:2181', machine-id: '42'}14machine-data: {ensemble-provider-type: orchestra, ensemble-zookeeper-hosts: 'jennifer:2181',
15 machine-id: '42'}
15output: {all: '| tee -a /var/log/cloud-init-output.log'}16output: {all: '| tee -a /var/log/cloud-init-output.log'}
16packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-argparse, python-txaws,17packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-argparse, python-txaws,
17 python-zookeeper]18 python-zookeeper]
18runcmd: [export ENSEMBLE_INSTANCE_ID=winston-uid, sudo apt-get -y install ensemble,19runcmd: [sudo apt-get -y install ensemble, sudo mkdir -p /var/lib/ensemble, sudo mkdir
19 sudo mkdir -p /var/lib/ensemble, sudo mkdir -p /var/log/ensemble, 'ENSEMBLE_MACHINE_ID=4220 -p /var/log/ensemble, 'ENSEMBLE_MACHINE_ID=42 ENSEMBLE_ZOOKEEPER=jennifer:2181
20 ENSEMBLE_ZOOKEEPER=jennifer:2181 python -m ensemble.agents.machine -n --logfile=/var/log/ensemble/machine-agent.log21 python -m ensemble.agents.machine -n --logfile=/var/log/ensemble/machine-agent.log
21 --pidfile=/var/run/ensemble/machine-agent.pid']22 --pidfile=/var/run/ensemble/machine-agent.pid']
22ssh_authorized_keys: [this-is-a-public-key]23ssh_authorized_keys: [this-is-a-public-key]
2324
2425
=== modified file 'ensemble/providers/orchestra/tests/test_launch.py'
--- ensemble/providers/orchestra/tests/test_launch.py 2011-08-26 12:24:00 +0000
+++ ensemble/providers/orchestra/tests/test_launch.py 2011-09-13 10:56:30 +0000
@@ -11,6 +11,12 @@
11 def test_bad_data(self):11 def test_bad_data(self):
12 d = self.get_provider().start_machine({})12 d = self.get_provider().start_machine({})
13 self.assertFailure(d, ProviderError)13 self.assertFailure(d, ProviderError)
14
15 def verify(error):
16 self.assertEquals(
17 str(error),
18 "Cannot launch a machine without specifying a machine-id")
19 d.addCallback(verify)
14 return d20 return d
1521
16 def test_no_zookeeper(self):22 def test_no_zookeeper(self):

Subscribers

People subscribed via source and target branches

to status/vote changes: