Merge lp:~gz/pyjuju/openstack_provider into lp:pyjuju
- openstack_provider
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Kapil Thangavelu |
Approved revision: | 539 |
Merged at revision: | 557 |
Proposed branch: | lp:~gz/pyjuju/openstack_provider |
Merge into: | lp:pyjuju |
Diff against target: |
3680 lines (+3386/-10) 31 files modified
juju/agents/provision.py (+1/-0) juju/control/status.py (+1/-1) juju/environment/config.py (+48/-2) juju/errors.py (+1/-1) juju/providers/common/findzookeepers.py (+2/-1) juju/providers/ec2/tests/test_launch.py (+1/-1) juju/providers/openstack/__init__.py (+3/-0) juju/providers/openstack/client.py (+470/-0) juju/providers/openstack/credentials.py (+101/-0) juju/providers/openstack/files.py (+84/-0) juju/providers/openstack/launch.py (+131/-0) juju/providers/openstack/machine.py (+68/-0) juju/providers/openstack/ports.py (+179/-0) juju/providers/openstack/provider.py (+189/-0) juju/providers/openstack/tests/__init__.py (+206/-0) juju/providers/openstack/tests/test_bootstrap.py (+213/-0) juju/providers/openstack/tests/test_client.py (+75/-0) juju/providers/openstack/tests/test_credentials.py (+216/-0) juju/providers/openstack/tests/test_files.py (+103/-0) juju/providers/openstack/tests/test_getmachines.py (+115/-0) juju/providers/openstack/tests/test_launch.py (+113/-0) juju/providers/openstack/tests/test_machine.py (+110/-0) juju/providers/openstack/tests/test_ports.py (+396/-0) juju/providers/openstack/tests/test_provider.py (+116/-0) juju/providers/openstack/tests/test_shutdown.py (+130/-0) juju/providers/openstack/tests/test_state.py (+58/-0) juju/providers/openstack_s3/__init__.py (+48/-0) juju/providers/openstack_s3/tests/test_provider.py (+122/-0) juju/state/initialize.py (+4/-0) juju/unit/address.py (+26/-3) juju/unit/tests/test_address.py (+56/-1) |
To merge this branch: | bzr merge lp:~gz/pyjuju/openstack_provider |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Kapil Thangavelu (community) | Approve | ||
Review via email: mp+110860@code.launchpad.net |
Commit message
Description of the change
OpenStack Provider
Implementation of a provider using native OpenStack apis, and using swift for file storage.
There is still some important features to complete here, but the core is done and the
remaining gaps documented.
Also included is an openstack_s3 provider fudge that uses the Nova api, with the S3 api
for storage. This allows for running against deployments using nova-objectstore rather
than swift, which currently includes canonistack.
The basics are modelled on the EC2 provider implementation, with the addition of a client
module that handles the details of the OpenStack api in the manner of txaws. Along the way
I also refactored the security_groups module into helper class for port management.
Also tested against hpcloud. rackspace public cloud support for a future branch.
Kapil Thangavelu (hazmat) wrote : | # |
Kapil Thangavelu (hazmat) wrote : | # |
Oh.. and btw this is awesome! :-)
Martin Packman (gz) wrote : | # |
Please take a look.
Martin Packman (gz) wrote : | # |
Reviewers: mp+110860_
Message:
Please take a look.
Description:
Implementation of a provider using native OpenStack apis, and using
swift for file storage. There is still some important features to
complete here, but the core is done and the remaining gaps documented.
Also included is an openstack_s3 provider fudge that uses the Nova api,
with the S3 api for storage. This allows for running against deployments
using nova-objectstore rather than swift, which currently includes
canonistack.
The basics are modelled on the EC2 provider implementation, with the
addition of a client module that handles the details of the OpenStack
api in the manner of txaws. Along the way I also refactored the
security_groups module into helper class for port management.
I've made an effort to document all the tricky bits as clearly as
possible, to guard against important details getting lost in
translation. See those docstrings for a deeper description of the
implementation.
Apologies for the size of the delta, all feedback welcome.
https:/
(do not edit description out of merge proposal)
Please review this at https:/
Affected files:
A [revision details]
M juju/environmen
M juju/errors.py
M juju/providers/
M juju/providers/
A juju/providers/
A juju/providers/
A juju/providers/
A juju/providers/
A juju/providers/
A juju/providers/
A juju/providers/
A juju/providers/
A juju/providers/
A juju/providers/
A juju/providers/
A juju/providers/
A juju/providers/
A juju/providers/
A juju/providers/
A juju/providers/
A juju/providers/
A juju/providers/
A juju/providers/
A juju/providers/
M juju/unit/
M juju/unit/
Kapil Thangavelu (hazmat) wrote : | # |
Hi Martin,
this is a big branch so i'm going to break up the review into multiple
passes. Overall it looks good, but i have a few questions about some of
the behavior ic wrt to waiting for post-boot network setup and md server
availability (bootstrap node instance id access) that i'd like to
discuss with you and document.
https:/
File juju/environmen
https:/
juju/environmen
given constraint support default-
config option.
https:/
File juju/errors.py (right):
https:/
juju/errors.py:162: ", ".join(map(str, self.instance_
looks fine, but curious why needed, instance ids should always be
integers.
https:/
File juju/providers/
https:/
juju/providers/
style minors, deferred transient isn't needed.
https:/
juju/providers/
response.
haven't seen this style before, preferable to just index 0 off the
result.
https:/
juju/providers/
self._lookup_
please document the kw params supported (sec groups and ipv4).
https:/
juju/providers/
what's this for?
https:/
File juju/providers/
https:/
juju/providers/
its likely the original error is more interesting at this point.
https:/
File juju/providers/
https:/
juju/providers/
is the md service not available?
https:/
juju/providers/
StringIO(
have you run into this in practice?, it seems hard to achieve, but good
to note nonetheless.
https:/
juju/providers/
deferred.callback, None)
ouch!.. you have to wait for the servers to boot to attach a public...
Martin Packman (gz) wrote : | # |
Thanks for the comments! Have replied to some below, will act on the
others.
https:/
File juju/errors.py (right):
https:/
juju/errors.py:162: ", ".join(map(str, self.instance_
Instance ids are normally UUIDs and stored as strings, but Nova
currently also allows the integer form of the ec2 id. This shouldn't
come up in production, but is exercised in tests as writing out UUIDs is
annoying.
https:/
File juju/providers/
https:/
juju/providers/
Serves no purpose now, was added when I was experimenting with ways to
solve the problem of juju needing a public ip.
https:/
File juju/providers/
https:/
juju/providers/
Right, the swift backend should probably raise exceptions then this
block could reraise the original one.
https:/
File juju/providers/
https:/
juju/providers/
The metadata service only exposes the EC2 form of the server id, but the
rest of the code deals with the UUID form. Sticking the UUID in file
storage stops juju getting confused with two different ids for the same
server. Trick should no longer be required with the Folsom release, as
the metadata service will be adding some OpenStack values.
https:/
juju/providers/
StringIO(
Not had any problems in testing, the conditions should heavily favour
the correct outcome. Sticking a small string in the object store should
beat the server boot process even giving it a head start.
https:/
juju/providers/
deferred.callback, None)
Doesn't need to finish booting, but doesn't work till the network setup
has been done, which is not synchronous on creating the server.
This is one the reasons I'd really like the public ip assignment to move
to the port handling code, and juju to not expect a public address for
all units.
https:/
File juju/providers/
https:/
juju/providers/
I saw that helper but it wasn't obvious what it was adding. I think the
correct behaviour here requires usi...
Clint Byrum (clint-fewbar) wrote : | # |
Just wanted to add some testing results..
1) openstack_s3 works perfectly w/ canonistack. Need to provide a map in documentation of Env Var -> environments.yaml values, as they are not clear.
2) I was able to coax the branch with some tweaks to work with hpcloud (which, I believe, is a very old OpenStack:
lp:~clint-fewbar/juju/openstack_provider_fixes
Note that there's a stupid amount of debug logging that should be removed.
Even w/ that branch, one still must manually delete everything on destroy-
Anyway, nice work, I'm sure there is all kinds of improvement to it, but in general it works GREAT. I'd love to see this in juju AS-IS.
Martin Packman (gz) wrote : | # |
> Just wanted to add some testing results..
Thanks for doing this Clint!
> 1) openstack_s3 works perfectly w/ canonistack. Need to provide a map in
> documentation of Env Var -> environments.yaml values, as they are not clear.
Yes. Is there a good place to document these? With some recent changes, most should be taken from the local environment rather than needing to be copied into the juju yaml file at least.
> 2) I was able to coax the branch with some tweaks to work with hpcloud (which,
> I believe, is a very old OpenStack:
>
> lp:~clint-fewbar/juju/openstack_provider_fixes
This use useful, these are areas I knew the code was weak but hadn't seen it breaking. A couple of queries to help me improve the coverage:
* What exactly went wrong when wrongly setting Accept as json all the time? Swift I expect?
* What is the configured OS_REGION vs the one returned from keystone in the serviceCatalog?
> Note that there's a stupid amount of debug logging that should be removed.
Looking at where you've added that logging is actually useful. Currently the client logs requests and responses, but only after the authentication step to avoid leaking passwords. More targetted debug logging there, given the number of times auth has needed tweaking, would be a good addition.
> Even w/ that branch, one still must manually delete everything on destroy-
> environment, and for some reason machine 0 doesn't get hostname details.
There are some failing tests for the shutdown code, hopefully those cover the issue here but I'll make a note to check when they're passing and if there's more to be done.
Martin Packman (gz) wrote : | # |
Please take a look.
Kapil Thangavelu (hazmat) wrote : | # |
review round 2
https:/
File juju/environmen
https:/
juju/environmen
This could use a doc string, at least describing the correspondence to
ostack rels and what it entails wrt to the ostack setup, ie. is this a
user choice or a provider configuration matter. Eventually we'll start
dropping support for old ostack releases, and it will be good to
understand what bits correspond to what so we can keep this tidy.
EDIT: a ref to credentials.py for details would work
https:/
File juju/providers/
https:/
juju/providers/
There's a broken test with this change not addressed by the branch. Its
not clear if this is adding value, rather than just leaking impl
details. i assume this is to help distinguish the int/str form of
openstack id provider.
https:/
File juju/providers/
https:/
juju/providers/
reader.
please don't name instance attributes the same as framework methods,
thats very confusing, it looks like your yielding on a method instead of
its invocation here.
its good to keep in mind and perhaps note here, if connectionLost method
callstack should return a deferred twisted will not wait on it before
proceeding and processing other things.
this method is also lacking any tests.
https:/
juju/providers/
parts):
minor. make_url should be prolly be private, since it requires
self.services which is not initialized in the constructor, so requires
internal knowledge to use properly in the correct initialization order.
https:/
File juju/providers/
https:/
juju/providers/
'auth-url'
re TODO b/c?
https:/
File juju/providers/
https:/
juju/providers/
expectation here?
already documented in the module doc string.
https:/
File juju/providers/
https:/
Martin Packman (gz) wrote : | # |
Thanks Kapil, is really useful input.
https:/
File juju/environmen
https:/
juju/environmen
On 2012/07/03 17:25:44, hazmat wrote:
> EDIT: a ref to credentials.py for details would work
Will add, most users should be able to ignore this setting and have the
right thing autodetected.
https:/
File juju/providers/
https:/
juju/providers/
On 2012/07/03 17:25:44, hazmat wrote:
> There's a broken test with this change not addressed by the branch.
Its not
> clear if this is adding value, rather than just leaking impl details.
i assume
> this is to help distinguish the int/str form of openstack id provider.
Have changed this back and forth as I saw that test fail before then
forgot about it and needed to fix int ids. Will resolve in some sensible
manner.
https:/
File juju/providers/
https:/
juju/providers/
reader.
Right, still some rearrangements needed here for sanity.
https:/
juju/providers/
parts):
On 2012/07/03 17:25:44, hazmat wrote:
> minor. make_url should be prolly be private, since it requires
self.services
> which is not initialized in the constructor, so requires internal
knowledge to
> use properly in the correct initialization order.
Sounds sensible. _SwiftClient/
somewhat special case.
https:/
File juju/providers/
https:/
juju/providers/
'auth-url'
On 2012/07/03 17:25:44, hazmat wrote:
> re TODO b/c?
Because 'nova-uri' is wrong, in a modern openstack deployment it's a
keystone identity service url and used to get credentials for swift as
well. There aren't legacy compat issues in juju so we may as well use a
less misleading name.
https:/
File juju/providers/
https:/
juju/providers/
expectation here?
On 2012/07/03 17:25:44, hazmat wrote:
> already documented in the module doc string.
Right, will remove this.
Martin Packman (gz) wrote : | # |
Please take a look.
- 537. By Martin Packman
-
Tweaks to test_launch
- 538. By Martin Packman
-
Rename _NovaClient method post_empty to clearer post_no_data
- 539. By Martin Packman
-
Make default-series required for openstack providers too
Kapil Thangavelu (hazmat) wrote : | # |
i've put together lp:~hazmat/juju/openstack_provider which fixes a few issues i found in testing on hpcloud and canonistack. Floating ip usage is off by default, as public clouds provide them by default, and speeds things up a bit. Internal access/private net secgroup was missing. Also includes the hp tweak branch with compatibility/
anyways, with that branch and trunk merged and tests passing, i'm good with this. it works flawlessly with hpcloud and canonistack to a lesser extent due to some incredible floating ip behavior vagaries inspite of correct state. the rspace stuff can wait for the future against trunk.
thanks for persevering with this martin, it rocks!
- 540. By Martin Packman
-
Merge work in progress changes by hazmat
- 541. By Martin Packman
-
Fix tests after changes to shutdown and security groups
- 542. By Martin Packman
-
Teeny cleanups of merged changes
- 543. By Martin Packman
-
Revert change to test_findzookeepers now the old form is used again
Preview Diff
1 | === modified file 'juju/agents/provision.py' |
2 | --- juju/agents/provision.py 2012-03-20 10:13:22 +0000 |
3 | +++ juju/agents/provision.py 2012-07-18 19:47:20 +0000 |
4 | @@ -195,6 +195,7 @@ |
5 | for instance_id in unused: |
6 | log.info("Shutting down machine id:%s ...", instance_id) |
7 | machine = provider_machines[instance_id] |
8 | + |
9 | try: |
10 | yield self.provider.shutdown_machine(machine) |
11 | except ProviderError: |
12 | |
13 | === modified file 'juju/control/status.py' |
14 | --- juju/control/status.py 2012-05-18 21:41:15 +0000 |
15 | +++ juju/control/status.py 2012-07-18 19:47:20 +0000 |
16 | @@ -543,7 +543,7 @@ |
17 | m["agent-state"] = "not-started" |
18 | except ProviderError: |
19 | # The provider doesn't have machine information |
20 | - self.log.error( |
21 | + self.log.exception( |
22 | "Machine provider information missing: machine %s" % ( |
23 | machine_state.id)) |
24 | |
25 | |
26 | === modified file 'juju/environment/config.py' |
27 | --- juju/environment/config.py 2012-04-11 01:17:53 +0000 |
28 | +++ juju/environment/config.py 2012-07-18 19:47:20 +0000 |
29 | @@ -6,8 +6,8 @@ |
30 | from juju.environment.errors import EnvironmentsConfigError |
31 | from juju.errors import FileAlreadyExists, FileNotFound |
32 | from juju.lib.schema import ( |
33 | - Constant, Dict, KeyDict, OAuthString, OneOf, SchemaError, SelectDict, |
34 | - String) |
35 | + Constant, Dict, Int, KeyDict, OAuthString, OneOf, SchemaError, SelectDict, |
36 | + String, Bool) |
37 | |
38 | DEFAULT_CONFIG_PATH = "~/.juju/environments.yaml" |
39 | |
40 | @@ -23,6 +23,14 @@ |
41 | |
42 | _EITHER_PLACEMENT = OneOf(Constant("unassigned"), Constant("local")) |
43 | |
44 | +# See juju.providers.openstack.credentials for definition and more details |
45 | +_OPENSTACK_AUTH_MODE = OneOf( |
46 | + Constant("userpass"), |
47 | + Constant("keypair"), |
48 | + Constant("legacy"), |
49 | + Constant("rax"), |
50 | + ) |
51 | + |
52 | SCHEMA = KeyDict({ |
53 | "default": String(), |
54 | "environments": Dict(String(), SelectDict("type", { |
55 | @@ -49,6 +57,44 @@ |
56 | optional=[ |
57 | "access-key", "secret-key", "region", "ec2-uri", "s3-uri", |
58 | "placement", "ssl-hostname-verification"]), |
59 | + "openstack": KeyDict({ |
60 | + "control-bucket": String(), |
61 | + "admin-secret": String(), |
62 | + "access-key": String(), |
63 | + "secret-key": String(), |
64 | + "default-instance-type": String(), |
65 | + "default-image-id": OneOf(String(), Int()), |
66 | + "auth-url": String(), |
67 | + "project-name": String(), |
68 | + "use-floating-ip": Bool(), |
69 | + "placement": _EITHER_PLACEMENT, |
70 | + "auth-mode": _OPENSTACK_AUTH_MODE, |
71 | + "region": String(), |
72 | + "default-series": String(), |
73 | + }, |
74 | + optional=[ |
75 | + "access-key", "secret-key", "auth-url", "project-name", |
76 | + "placement", "auth-mode", "region", "use-floating-ip"]), |
77 | + "openstack_s3": KeyDict({ |
78 | + "control-bucket": String(), |
79 | + "admin-secret": String(), |
80 | + "access-key": String(), |
81 | + "secret-key": String(), |
82 | + "default-instance-type": String(), |
83 | + "default-image-id": OneOf(String(), Int()), |
84 | + "auth-url": String(), |
85 | + "placement": _EITHER_PLACEMENT, |
86 | + "combined-key": String(), |
87 | + "s3-uri": String(), |
88 | + "use-floating-ip": Bool(), |
89 | + "auth-mode": _OPENSTACK_AUTH_MODE, |
90 | + "region": String(), |
91 | + "default-series": String(), |
92 | + }, |
93 | + optional=[ |
94 | + "access-key", "secret-key", "combined-key", "auth-url", |
95 | + "s3-uri", "project-name", "placement", "auth-mode", "region", |
96 | + "use-floating-ip"]), |
97 | "orchestra": KeyDict({ |
98 | "orchestra-server": String(), |
99 | "orchestra-user": String(), |
100 | |
101 | === modified file 'juju/errors.py' |
102 | --- juju/errors.py 2012-03-28 07:33:22 +0000 |
103 | +++ juju/errors.py 2012-07-18 19:47:20 +0000 |
104 | @@ -159,7 +159,7 @@ |
105 | def __str__(self): |
106 | return "Cannot find machine%s: %s" % ( |
107 | "" if len(self.instance_ids) == 1 else "s", |
108 | - ", ".join(self.instance_ids)) |
109 | + ", ".join(map(str, self.instance_ids))) |
110 | |
111 | |
112 | class ProviderInteractionError(ProviderError): |
113 | |
114 | === modified file 'juju/providers/common/findzookeepers.py' |
115 | --- juju/providers/common/findzookeepers.py 2011-09-15 18:50:23 +0000 |
116 | +++ juju/providers/common/findzookeepers.py 2012-07-18 19:47:20 +0000 |
117 | @@ -36,5 +36,6 @@ |
118 | |
119 | if machines: |
120 | returnValue(machines) |
121 | + |
122 | raise EnvironmentNotFound("machines are not running (%s)" |
123 | - % ", ".join(missing_instance_ids)) |
124 | + % ", ".join(map(str, missing_instance_ids))) |
125 | |
126 | === modified file 'juju/providers/ec2/tests/test_launch.py' |
127 | --- juju/providers/ec2/tests/test_launch.py 2012-07-05 21:49:12 +0000 |
128 | +++ juju/providers/ec2/tests/test_launch.py 2012-07-18 19:47:20 +0000 |
129 | @@ -67,7 +67,7 @@ |
130 | def test_provider_launch(self): |
131 | """ |
132 | The provider can be used to launch a machine with a minimal set of |
133 | - required packages, repositories, and and security groups. |
134 | + required packages, repositories, and security groups. |
135 | """ |
136 | self.ec2.describe_security_groups() |
137 | self.mocker.result(succeed([])) |
138 | |
139 | === added directory 'juju/providers/openstack' |
140 | === added file 'juju/providers/openstack/__init__.py' |
141 | --- juju/providers/openstack/__init__.py 1970-01-01 00:00:00 +0000 |
142 | +++ juju/providers/openstack/__init__.py 2012-07-18 19:47:20 +0000 |
143 | @@ -0,0 +1,3 @@ |
144 | +"""Support for using OpenStack as a cloud provider for juju""" |
145 | + |
146 | +from .provider import MachineProvider |
147 | |
148 | === added file 'juju/providers/openstack/client.py' |
149 | --- juju/providers/openstack/client.py 1970-01-01 00:00:00 +0000 |
150 | +++ juju/providers/openstack/client.py 2012-07-18 19:47:20 +0000 |
151 | @@ -0,0 +1,470 @@ |
152 | +"""Client for talking to OpenStack APIs using twisted |
153 | + |
154 | +This is not a complete implemention of all interfaces, just what juju needs. |
155 | + |
156 | +There is a fair bit of code cleanup and feature implementation to do here |
157 | +still. |
158 | + |
159 | +* Must check https certificates, can use code in txaws to do this. |
160 | +* Must support user/password authentication with keystone as well as keypair. |
161 | +* Want a ProviderInteractionError subclass that can include the extra details |
162 | + returned in json form when something goes wrong and is raised by clients. |
163 | +* Request flow and json handling in general needs polish. |
164 | +* Need to prevent concurrent authentication attempts. |
165 | +* Need to limit concurrent http api requests to 4 or something reasonable, |
166 | + can use DeferredSemaphore for this. |
167 | +* Should really have authentication retry logic in case the token expires. |
168 | +* Would be nice to use Agent keep alive support that twisted 12.1.0 added. |
169 | +""" |
170 | + |
171 | +import base64 |
172 | +import json |
173 | +import logging |
174 | +import operator |
175 | +import urllib |
176 | + |
177 | +import twisted |
178 | +from twisted.internet import ( |
179 | + defer, |
180 | + interfaces, |
181 | + protocol, |
182 | + reactor, |
183 | + ) |
184 | +from twisted.web import ( |
185 | + client, |
186 | + http_headers, |
187 | + ) |
188 | +from zope.interface import implements |
189 | + |
190 | +from juju import errors |
191 | + |
192 | + |
193 | +log = logging.getLogger("juju.openstack") |
194 | + |
195 | + |
196 | +# Need the right juju version number, not exposed within python package. |
197 | +_USER_AGENT = "juju/%s twisted/%s" % ("0.5", twisted.__version__) |
198 | + |
199 | + |
200 | +class BytestringProducer(object): |
201 | + """Wrap basic bytestring as a needlessly fancy twisted producer.""" |
202 | + |
203 | + implements(interfaces.IProducer) |
204 | + |
205 | + def __init__(self, bytestring): |
206 | + self.content = bytestring |
207 | + self.length = len(bytestring) |
208 | + |
209 | + def pauseProducing(self): |
210 | + """Nothing to do if production is paused""" |
211 | + |
212 | + def startProducing(self, consumer): |
213 | + """Write entire contents when production starts""" |
214 | + consumer.write(self.content) |
215 | + return defer.succeed(None) |
216 | + |
217 | + def stopProducing(self): |
218 | + """Nothing to do when production halts""" |
219 | + |
220 | + |
221 | +class ResponseReader(protocol.Protocol): |
222 | + """Protocol object suitable for use with Response.deliverBody |
223 | + |
224 | + The 'onConnectionLost' deferred will be called back once the connection |
225 | + is shut down with all the bytes from the body collected at that point. |
226 | + """ |
227 | + |
228 | + def __init__(self): |
229 | + self.onConnectionLost = defer.Deferred() |
230 | + |
231 | + def connectionMade(self): |
232 | + self.data = [] |
233 | + |
234 | + def dataReceived(self, data): |
235 | + self.data.append(data) |
236 | + |
237 | + def connectionLost(self, reason): |
238 | + """Called on connection shut down |
239 | + |
240 | + Here 'reason' can be one of ResponseDone, PotentialDataLost, or |
241 | + ResponseFailed, but currently there is no fancy handling of these. |
242 | + """ |
243 | + self.onConnectionLost.callback("".join(self.data)) |
244 | + |
245 | + |
246 | +@defer.inlineCallbacks |
247 | +def request(method, url, extra_headers=(), body=None): |
248 | + headers = http_headers.Headers({ |
249 | + # GZ 2012-07-03: Previously passed Accept: application/json header |
250 | + # here, but not always the right thing. Bad for swift? |
251 | + "User-Agent": [_USER_AGENT], |
252 | + }) |
253 | + for header, value in extra_headers: |
254 | + headers.setRawHeaders(header, [value]) |
255 | + if body is not None: |
256 | + if isinstance(body, dict): |
257 | + content_type = "application/json" |
258 | + body = json.dumps(body) |
259 | + elif isinstance(body, str): |
260 | + content_type = "application/octet-stream" |
261 | + headers.setRawHeaders("Content-Type", [content_type]) |
262 | + body = BytestringProducer(body) |
263 | + response = yield client.Agent(reactor).request(method, url, headers, body) |
264 | + if response.length == 0: |
265 | + defer.returnValue((response, "")) |
266 | + reader = ResponseReader() |
267 | + response.deliverBody(reader) |
268 | + body = yield reader.onConnectionLost |
269 | + defer.returnValue((response, body)) |
270 | + |
271 | + |
272 | +class _OpenStackClient(object): |
273 | + |
274 | + def __init__(self, credentials): |
275 | + self.credentials = credentials |
276 | + log.debug("openstack: using auth-mode %r with %s", credentials.mode, |
277 | + credentials.url) |
278 | + if credentials.mode == "keypair": |
279 | + self.authenticate = self.authenticate_v2_keypair |
280 | + elif credentials.mode == "legacy": |
281 | + self.authenticate = self.authenticate_v1 |
282 | + elif credentials.mode == "rax": |
283 | + self.authenticate = self.authenticate_rax_auth |
284 | + else: |
285 | + self.authenticate = self.authenticate_v2_userpass |
286 | + self.token = None |
287 | + |
288 | + def _make_url(self, service, parts): |
289 | + """Form full url from path components to service endpoint url""" |
290 | + # GZ 2012-07-03: Need to ensure either services is populated or catch |
291 | + # error here and propogate as one useful for users. |
292 | + endpoint = self.services[service] |
293 | + if not endpoint[-1] == "/": |
294 | + endpoint += "/" |
295 | + if isinstance(parts, str): |
296 | + return endpoint + parts |
297 | + quoted_parts = [] |
298 | + for part in parts: |
299 | + if not isinstance(part, str): |
300 | + part = urllib.quote(unicode(part).encode("utf-8"), "/~") |
301 | + quoted_parts.append(part) |
302 | + url = endpoint + "/".join(quoted_parts) |
303 | + log.debug('access %s @ %s', service, url) |
304 | + return url |
305 | + |
306 | + @defer.inlineCallbacks |
307 | + def authenticate_v1(self): |
308 | + deferred = request( |
309 | + "GET", |
310 | + self.credentials.url, |
311 | + extra_headers=[ |
312 | + ("X-Auth-User", self.credentials.username), |
313 | + ("X-Auth-Key", self.credentials.access_key), |
314 | + ], |
315 | + ) |
316 | + response, body = yield deferred |
317 | + if response.code != 204: |
318 | + raise errors.ProviderInteractionError("Failed to authenticate") |
319 | + # TODO: check response has right headers |
320 | + [nova_url] = response.headers.getRawHeaders("X-Server-Management-Url") |
321 | + self.services = {"compute": self.nova_url} |
322 | + # No swift_url set as that is not supported |
323 | + [self.token] = response.headers.getRawHeaders("X-Auth-Token") |
324 | + |
325 | + def authenticate_v2_keypair(self): |
326 | + deferred = request( |
327 | + "POST", |
328 | + self.credentials.url + "tokens", |
329 | + body={"auth": { |
330 | + "apiAccessKeyCredentials": { |
331 | + "accessKey": self.credentials.access_key, |
332 | + "secretKey": self.credentials.secret_key, |
333 | + }, |
334 | + "tenantName": self.credentials.project_name, |
335 | + }} |
336 | + ) |
337 | + return deferred.addCallback(self._handle_v2_auth) |
338 | + |
339 | + def authenticate_v2_userpass(self): |
340 | + deferred = request( |
341 | + "POST", |
342 | + self.credentials.url + "tokens", |
343 | + body={"auth": { |
344 | + "passwordCredentials": { |
345 | + "username": self.credentials.username, |
346 | + "password": self.credentials.password, |
347 | + }, |
348 | + "tenantName": self.credentials.project_name, |
349 | + }} |
350 | + ) |
351 | + return deferred.addCallback(self._handle_v2_auth) |
352 | + |
353 | + def authenticate_rax_auth(self): |
354 | + # openstack is not a product, but a kit for making snowflakes. |
355 | + deferred = request( |
356 | + "POST", |
357 | + self.credentials.url + "tokens", |
358 | + body={"auth": { |
359 | + "RAX-KSKEY:apiKeyCredentials": { |
360 | + "username": self.credentials.username, |
361 | + "apiKey": self.credentials.password, |
362 | + "tenantName": self.credentials.project_name}}} |
363 | + ) |
364 | + return deferred.addCallback(self._handle_v2_auth) |
365 | + |
366 | + def _handle_v2_auth(self, result): |
367 | + access_details = self._json(result, 200, 'access') |
368 | + token_details = access_details["token"] |
369 | + self.token = token_details["id"] |
370 | + |
371 | + # TODO: care about token_details["expires"] |
372 | + # Don't need to we're not preserving tokens. |
373 | + services = [] |
374 | + log.debug("openstack: authenticated til %r", token_details['expires']) |
375 | + region = self.credentials.region |
376 | + # HP cloud uses both az-1.region-a.geo-1 and region-a.geo-1 forms, not |
377 | + # clear what should be in config or what the correct logic is. |
378 | + if region is not None: |
379 | + base_region = region.split('.', 1)[-1] |
380 | + # GZ: 2012-07-03: Should split extraction of endpoints, add logging, |
381 | + # and make more robust. |
382 | + for catalog in access_details["serviceCatalog"]: |
383 | + for endpoint in catalog["endpoints"]: |
384 | + if region is not None and region != endpoint["region"]: |
385 | + if base_region != endpoint["region"]: |
386 | + continue |
387 | + services.append((catalog["type"], str(endpoint["publicURL"]))) |
388 | + break |
389 | + |
390 | + if not services: |
391 | + raise errors.ProviderInteractionError("No suitable endpoints") |
392 | + |
393 | + self.services = dict(services) |
394 | + |
395 | + def is_authenticated(self): |
396 | + return self.token is not None |
397 | + |
398 | + @defer.inlineCallbacks |
399 | + def authed_request(self, method, url, headers=None, body=None): |
400 | + log.debug("openstack: %s %r", method, url) |
401 | + request_headers = [("X-Auth-Token", self.token)] |
402 | + if headers: |
403 | + request_headers += headers |
404 | + response, body = yield request(method, url, request_headers, body) |
405 | + log.debug("openstack: %d %r", response.code, body) |
406 | + defer.returnValue((response, body)) |
407 | + |
408 | + def _empty(self, result, code): |
409 | + response, body = result |
410 | + if response.code != code: |
411 | + # XXX: This is a deeply unhelpful error, need context from request |
412 | + raise errors.ProviderInteractionError("Unexpected %d: %r" % ( |
413 | + response.code, body)) |
414 | + |
415 | + def _json(self, result, code, root=None): |
416 | + response, body = result |
417 | + if response.code != code: |
418 | + raise errors.ProviderInteractionError("Unexpected %d: %r" % ( |
419 | + response.code, body)) |
420 | + type_headers = response.headers.getRawHeaders("Content-Type") |
421 | + |
422 | + found = False |
423 | + for h in type_headers: |
424 | + if 'application/json' in h: |
425 | + found = True |
426 | + if not found: |
427 | + raise errors.ProviderInteractionError( |
428 | + "Expected json response got %s" % type_headers) |
429 | + |
430 | + data = json.loads(body) |
431 | + if root is not None: |
432 | + return data[root] |
433 | + return data |
434 | + |
435 | + |
436 | +class _NovaClient(object): |
437 | + |
438 | + def __init__(self, client): |
439 | + self._client = client |
440 | + |
441 | + @defer.inlineCallbacks |
442 | + def request(self, method, parts, headers=None, body=None): |
443 | + if not self._client.is_authenticated(): |
444 | + yield self._client.authenticate() |
445 | + url = self._client._make_url("compute", parts) |
446 | + result = yield self._client.authed_request(method, url, headers, body) |
447 | + defer.returnValue(result) |
448 | + |
449 | + def delete(self, parts, code=202): |
450 | + deferred = self.request("DELETE", parts) |
451 | + return deferred.addCallback(self._client._empty, code) |
452 | + |
453 | + def get(self, parts, root, code=200): |
454 | + deferred = self.request("GET", parts) |
455 | + return deferred.addCallback(self._client._json, code, root) |
456 | + |
457 | + def post(self, parts, jsonobj, root, code=200): |
458 | + deferred = self.request("POST", parts, None, jsonobj) # XXX |
459 | + return deferred.addCallback(self._client._json, code, root) |
460 | + |
461 | + def post_no_data(self, parts, root, code=200): |
462 | + deferred = self.request("POST", parts, None, "") # XXX |
463 | + return deferred.addCallback(self._client._json, code, root) |
464 | + |
465 | + def post_no_result(self, parts, jsonobj, code=202): |
466 | + deferred = self.request("POST", parts, None, jsonobj) # XXX |
467 | + return deferred.addCallback(self._client._empty, code) |
468 | + |
469 | + def list_flavors(self): |
470 | + return self.get("flavors", "flavors") |
471 | + |
472 | + def get_server(self, server_id): |
473 | + return self.get(["servers", server_id], "server") |
474 | + |
475 | + # GZ 2012-05-31: Appending detail isn't a known path for some reason. |
476 | + def get_server_detail(self, server_id): |
477 | + return self.get(["servers", server_id, "detail"], "server") |
478 | + |
479 | + def list_servers(self): |
480 | + return self.get(["servers"], "servers") |
481 | + |
482 | + def list_servers_detail(self): |
483 | + return self.get(["servers", "detail"], "servers") |
484 | + |
485 | + def delete_server(self, server_id): |
486 | + return self.delete(["servers", server_id], code=204) |
487 | + |
488 | + def run_server(self, image_id, flavor_id, name, security_group_names=None, |
489 | + user_data=None): |
490 | + server = { |
491 | + 'name': name, |
492 | + 'flavorRef': flavor_id, |
493 | + 'imageRef': image_id, |
494 | + } |
495 | + if user_data is not None: |
496 | + server["user_data"] = base64.b64encode(user_data) |
497 | + if security_group_names is not None: |
498 | + server["security_groups"] = [{'name': n} |
499 | + for n in security_group_names] |
500 | + return self.post(["servers"], {'server': server}, |
501 | + root="server", code=202) |
502 | + |
503 | + def get_server_security_groups(self, server_id): |
504 | + d = self.get( |
505 | + ["servers", server_id, "os-security-groups"], |
506 | + root="security_groups") |
507 | + # 2012-07-12: Workaround lack of this api in HP cloud |
508 | + d.addErrback( |
509 | + lambda f: self.get_server(server_id).addCallback( |
510 | + operator.itemgetter("security_groups"))) |
511 | + return d |
512 | + |
513 | + def list_security_groups(self): |
514 | + return self.get(["os-security-groups"], "security_groups") |
515 | + |
516 | + def create_security_group(self, name, description): |
517 | + return self.post("os-security-groups", { |
518 | + 'security_group': { |
519 | + 'name': name, |
520 | + 'description': description, |
521 | + } |
522 | + }, |
523 | + root="security_group") |
524 | + |
525 | + def delete_security_group(self, group_id): |
526 | + return self.delete(["os-security-groups", group_id]) |
527 | + |
528 | + def add_security_group_rule(self, parent_group_id, **kwargs): |
529 | + rule = {'parent_group_id': parent_group_id} |
530 | + using_group = "group_id" in kwargs |
531 | + if using_group: |
532 | + rule['group_id'] = kwargs['group_id'] |
533 | + elif "cidr" in kwargs: |
534 | + rule['cidr'] = kwargs['cidr'] |
535 | + if not using_group or "ip_protocol" in kwargs: |
536 | + rule['ip_protocol'] = kwargs['ip_protocol'] |
537 | + rule['from_port'] = kwargs['from_port'] |
538 | + rule['to_port'] = kwargs['to_port'] |
539 | + return self.post("os-security-group-rules", |
540 | + {'security_group_rule': rule}, |
541 | + root="security_group_rule") |
542 | + |
543 | + def delete_security_group_rule(self, rule_id): |
544 | + return self.delete(["os-security-group-rules", rule_id]) |
545 | + |
546 | + def add_server_security_group(self, server_id, group_name): |
547 | + return self.post_no_result(["servers", server_id, "action"], { |
548 | + "addSecurityGroup": { |
549 | + "name": group_name, |
550 | + }}) |
551 | + |
552 | + def remove_server_security_group(self, server_id, group_name): |
553 | + return self.post_no_result(["servers", server_id, "action"], { |
554 | + "removeSecurityGroup": { |
555 | + "name": group_name, |
556 | + }}) |
557 | + |
558 | + def list_floating_ips(self): |
559 | + return self.get(["os-floating-ips"], "floating_ips") |
560 | + |
561 | + def get_floating_ip(self, ip_id): |
562 | + return self.get(["os-floating-ips", ip_id], "floating_ip") |
563 | + |
564 | + def allocate_floating_ip(self): |
565 | + return self.post_no_data(["os-floating-ips"], "floating_ip") |
566 | + |
567 | + def delete_floating_ip(self, ip_id): |
568 | + return self.delete(["os-floating-ips", ip_id]) |
569 | + |
570 | + def add_floating_ip(self, server_id, addr): |
571 | + return self.post_no_result(["servers", server_id, "action"], { |
572 | + 'addFloatingIp': { |
573 | + 'address': addr, |
574 | + }}) |
575 | + |
576 | + def remove_floating_ip(self, server_id, addr): |
577 | + return self.post_no_result(["servers", server_id, "action"], { |
578 | + 'removeFloatingIp': { |
579 | + 'address': addr, |
580 | + }}) |
581 | + |
582 | + |
583 | +class _SwiftClient(object): |
584 | + |
585 | + def __init__(self, client): |
586 | + self._client = client |
587 | + |
588 | + @defer.inlineCallbacks |
589 | + def request(self, method, parts, headers=None, body=None): |
590 | + if not self._client.is_authenticated(): |
591 | + yield self._client.authenticate() |
592 | + url = self._client._make_url("object-store", parts) |
593 | + result = yield self._client.authed_request(method, url, headers, body) |
594 | + defer.returnValue(result) |
595 | + |
596 | + def public_object_url(self, container, object_name): |
597 | + if not self._client.is_authenticated(): |
598 | + raise ValueError("Need to have authenticated to get object url") |
599 | + return self._client._make_url("object-store", [container, object_name]) |
600 | + |
601 | + def put_container(self, container_name): |
602 | + # Juju expects there to be a (semi) public url for some objects. This |
603 | + # could probably be more restrictive or placed in a seperate container |
604 | + # with some refactoring, but for now just make everything public. |
605 | + read_acl_header = ("X-Container-Read", ".r:*") |
606 | + return self.request("PUT", [container_name], [read_acl_header], "") |
607 | + |
608 | + def delete_container(self, container_name): |
609 | + return self.request("DELETE", [container_name]) |
610 | + |
611 | + def head_object(self, container, object_name): |
612 | + return self.request("HEAD", [container, object_name]) |
613 | + |
614 | + def get_object(self, container, object_name): |
615 | + return self.request("GET", [container, object_name]) |
616 | + |
617 | + def delete_object(self, container, object_name): |
618 | + return self.request("DELETE", [container, object_name]) |
619 | + |
620 | + def put_object(self, container, object_name, bytestring): |
621 | + return self.request("PUT", [container, object_name], None, bytestring) |
622 | |
623 | === added file 'juju/providers/openstack/credentials.py' |
624 | --- juju/providers/openstack/credentials.py 1970-01-01 00:00:00 +0000 |
625 | +++ juju/providers/openstack/credentials.py 2012-07-18 19:47:20 +0000 |
626 | @@ -0,0 +1,101 @@ |
627 | +"""Handling of the credentials needed to authenticate with the OpenStack api |
628 | + |
629 | +Supports several different sets of credentials different auth modes need: |
630 | +* 'legacy' is built into nova and deprecated in favour of using keystone |
631 | +* 'keypair' works with the HP public cloud implemention of keystone |
632 | +* 'userpass' is the way keystone seems to want to do authentication generally |
633 | +""" |
634 | + |
635 | +import os |
636 | + |
637 | + |
638 | +class OpenStackCredentials(object): |
639 | + """Encapsulation of credentials used to authenticate with OpenStack""" |
640 | + |
641 | + _config_vars = { |
642 | + 'auth-url': ("OS_AUTH_URL", "NOVA_URL"), |
643 | + 'username': ("OS_USERNAME", "NOVA_USERNAME"), |
644 | + 'password': ("OS_PASSWORD", "NOVA_PASSWORD"), |
645 | + # HP exposes both a numeric id and a name for tenants, passed back |
646 | + # as tenantId and tenantName. Use the name only for simplicity. |
647 | + 'project-name': ("OS_TENANT_NAME", "NOVA_PROJECT_NAME", |
648 | + "NOVA_PROJECT_ID"), |
649 | + 'region': ("OS_REGION_NAME", "NOVA_REGION_NAME", "NOVA_REGION"), |
650 | + # The key variables don't seem to have modern OS_ prefixed aliases |
651 | + 'access-key': ("NOVA_API_KEY",), |
652 | + 'secret-key': ("EC2_SECRET_KEY", "AWS_SECRET_ACCESS_KEY"), |
653 | + # A usable mode can normally be guessed, but may be configured |
654 | + 'auth-mode': (), |
655 | + } |
656 | + |
657 | + # Really, legacy auth could pass in the project id and keystone doesn't |
658 | + # require it, but this is what the client expects for now. |
659 | + _modes = { |
660 | + 'userpass': ('username', 'password', 'project-name'), |
661 | + 'rax': ('username', 'password', 'project-name'), |
662 | + 'keypair': ('access-key', 'secret-key', 'project-name'), |
663 | + 'legacy': ('username', 'access-key'), |
664 | + } |
665 | + |
666 | + _version_to_mode = { |
667 | + "v2.0": 'userpass', |
668 | + "v1.1": 'legacy', |
669 | + "v1.0": 'legacy', |
670 | + } |
671 | + |
672 | + def __init__(self, creds_dict): |
673 | + url = creds_dict.get("auth-url") |
674 | + if not url: |
675 | + raise ValueError("Missing config 'auth-url' for OpenStack api") |
676 | + mode = creds_dict.get("auth-mode") |
677 | + if mode is None: |
678 | + mode = self._guess_auth_mode(url) |
679 | + elif mode not in self._modes: |
680 | + # The juju.environment.config layer should raise a pretty error |
681 | + raise ValueError("Unknown 'auth-mode' value %r" % (self.mode,)) |
682 | + missing_keys = [key for key in self._modes[mode] |
683 | + if not creds_dict.get(key)] |
684 | + if missing_keys: |
685 | + raise ValueError("Missing config %s required for %s auth" % ( |
686 | + ", ".join(map(repr, missing_keys)), mode)) |
687 | + self.url = url |
688 | + self.mode = mode |
689 | + for key in self._config_vars: |
690 | + if key not in ("auth-url", "auth-mode"): |
691 | + setattr(self, key.replace("-", "_"), creds_dict.get(key)) |
692 | + |
693 | + @classmethod |
694 | + def _guess_auth_mode(cls, url): |
695 | + """Pick a mode based on the version at the end of `url` given""" |
696 | + final_part = url.rstrip("/").rsplit("/", 1)[-1] |
697 | + try: |
698 | + return cls._version_to_mode[final_part] |
699 | + except KeyError: |
700 | + raise ValueError( |
701 | + "Missing config 'auth-mode' as unknown version" |
702 | + " in 'auth-url' given: " + url) |
703 | + |
704 | + @classmethod |
705 | + def _get(cls, config, key): |
706 | + """Retrieve `key` from `config` if present or in matching envvars""" |
707 | + val = config.get(key) |
708 | + if val is None: |
709 | + for env_key in cls._config_vars[key]: |
710 | + |
711 | + val = os.environ.get(env_key) |
712 | + if val: |
713 | + return val |
714 | + return val |
715 | + |
716 | + @classmethod |
717 | + def from_environment(cls, config): |
718 | + """Create credentials from `config` falling back to environment""" |
719 | + return cls(dict((k, cls._get(config, k)) for k in cls._config_vars)) |
720 | + |
721 | + def set_config_defaults(self, data): |
722 | + """Populate `data` with these credentials where not already set""" |
723 | + for key in self._config_vars: |
724 | + if key not in data: |
725 | + val = getattr(self, key.replace("auth-", "").replace("-", "_")) |
726 | + if val is not None: |
727 | + data[key] = val |
728 | |
729 | === added file 'juju/providers/openstack/files.py' |
730 | --- juju/providers/openstack/files.py 1970-01-01 00:00:00 +0000 |
731 | +++ juju/providers/openstack/files.py 2012-07-18 19:47:20 +0000 |
732 | @@ -0,0 +1,84 @@ |
733 | +"""OpenStack provider file storage on Swift |
734 | + |
735 | +Basically a limited wrapper around the underlying api calls, with a few added |
736 | +quirks. There's some specific handling for 404 responses, raises FileNotFound |
737 | +on GET, and on PUT attempts to create the container then retries. |
738 | + |
739 | +Expects file-like objects for data. This isn't terribly useful as it doesn't |
740 | +fit well with the twisted model for chunking http data and most objects are |
741 | +small anyway. |
742 | + |
743 | +The main complication is the get_url method, which requires the generation of |
744 | +link that can be used by any http client to fetch a particular object without |
745 | +authentication. This is not possible in the general case in swift, however |
746 | +there are a few ways around the problem: |
747 | + |
748 | +* The stub nova-objectstore service doesn't check access anyway, see lp:947374 |
749 | +* The tempurl swift middleware if enabled can do this with some advance setup |
750 | +* A container can have a more permissive ACL applying to all objects within |
751 | + |
752 | +All of these require touching the network to at least get the swift endpoint |
753 | +from the identity service, which as get_url doesn't return a deferred is |
754 | +problematic. In practice it's only used after putting an object however, so |
755 | +just raising if client has not yet been authenticated is good enough. |
756 | +""" |
757 | + |
758 | +from cStringIO import StringIO |
759 | + |
760 | +from twisted.internet import defer |
761 | + |
762 | +from juju import errors |
763 | + |
764 | + |
765 | +class FileStorage(object): |
766 | + """Swift-backed :class:`FileStorage` abstraction""" |
767 | + |
768 | + def __init__(self, swift, container): |
769 | + self._swift = swift |
770 | + self._container = container |
771 | + |
772 | + def get_url(self, name): |
773 | + return self._swift.public_object_url(self._container, name) |
774 | + |
775 | + @defer.inlineCallbacks |
776 | + def get(self, name): |
777 | + """Get a file object from Swift. |
778 | + |
779 | + :param unicode name: S3 key for the desired file |
780 | + |
781 | + :return: an open file object |
782 | + :rtype: :class:`twisted.internet.defer.Deferred` |
783 | + |
784 | + :raises: :exc:`juju.errors.FileNotFound` if the file doesn't exist |
785 | + """ |
786 | + response, body = yield self._swift.get_object(self._container, name) |
787 | + if response.code == 404: |
788 | + raise errors.FileNotFound(name) |
789 | + if response.code != 200: |
790 | + raise errors.ProviderInteractionError( |
791 | + "Couldn't fetch object %r %r" % (response.code, body)) |
792 | + defer.returnValue(StringIO(body)) |
793 | + |
794 | + @defer.inlineCallbacks |
795 | + def put(self, remote_path, file_object): |
796 | + """Upload a file to Swift. |
797 | + |
798 | + :param unicode remote_path: key on which to store the content |
799 | + |
800 | + :param file_object: open file object containing the content |
801 | + |
802 | + :rtype: :class:`twisted.internet.defer.Deferred` |
803 | + """ |
804 | + data = file_object.read() |
805 | + response, body = yield self._swift.put_object(self._container, |
806 | + remote_path, data) |
807 | + if response.code == 404: |
808 | + response, body = yield self._swift.put_container(self._container) |
809 | + if response.code != 201: |
810 | + raise errors.ProviderInteractionError( |
811 | + "Couldn't create container %r" % (self._container,)) |
812 | + response, body = yield self._swift.put_object(self._container, |
813 | + remote_path, data) |
814 | + if response.code != 201: |
815 | + raise errors.ProviderInteractionError( |
816 | + "Couldn't create object %r %r" % (response.code, remote_path)) |
817 | |
818 | === added file 'juju/providers/openstack/launch.py' |
819 | --- juju/providers/openstack/launch.py 1970-01-01 00:00:00 +0000 |
820 | +++ juju/providers/openstack/launch.py 2012-07-18 19:47:20 +0000 |
821 | @@ -0,0 +1,131 @@ |
822 | +"""Helpers for creating servers catered to Juju needs with OpenStack |
823 | + |
824 | +Specific notes: |
825 | +* Expects a public address for each machine, as that's what EC2 promises, |
826 | + would be good to weaken this requirement. |
827 | +* Creates a per-machine security group in case need to poke ports open later, |
828 | + as EC2 doesn't support changing groups later, but OpenStack does. |
829 | +* Needs to tell cloud-init how to get server id, currently in essex metadata |
830 | + service gives i-08x style only, so cheat and use filestorage. |
831 | +* Config must specify a flavor, as these vary between Openstack deployments. |
832 | + Working out constraints should resolve this. |
833 | +* Config must specify an image id, as there's no standard way of looking up |
834 | + from distro series across clouds yet in essex. |
835 | +* Would be really nice to put the service name in the server name, but it's |
836 | + not passed down into LaunchMachine currently. |
837 | + |
838 | +There are some race issues with the current setup: |
839 | +* Storing of server id needs to complete before cloud-init does the lookup. |
840 | +* A floating ip may be assigned to another server before the current one |
841 | + finishes launching and can use the available ip itself. |
842 | +""" |
843 | + |
844 | +from cStringIO import StringIO |
845 | + |
846 | +from twisted.internet import ( |
847 | + defer, |
848 | + reactor, |
849 | + ) |
850 | + |
851 | +from juju.errors import ProviderError, ProviderInteractionError |
852 | +from juju.lib import twistutils |
853 | +from juju.providers.common.launch import LaunchMachine |
854 | + |
855 | +from .machine import machine_from_instance, get_server_status |
856 | + |
857 | +from .client import log |
858 | + |
859 | + |
860 | +class NovaLaunchMachine(LaunchMachine): |
861 | + """OpenStack Nova operation for creating a server""" |
862 | + |
863 | + _DELAY_FOR_ADDRESSES = 5 # seconds |
864 | + |
865 | + @defer.inlineCallbacks |
866 | + def start_machine(self, machine_id, zookeepers): |
867 | + """Actually launch an instance on Nova. |
868 | + |
869 | + :param str machine_id: the juju machine ID to assign |
870 | + |
871 | + :param zookeepers: the machines currently running zookeeper, to which |
872 | + the new machine will need to connect |
873 | + :type zookeepers: list of |
874 | + :class:`juju.providers.openstack.machine.NovaProviderMachine` |
875 | + |
876 | + :return: a singe-entry list containing a |
877 | + :class:`juju.providers.openstack.machine.NovaProviderMachine` |
878 | + representing the newly-launched machine |
879 | + :rtype: :class:`twisted.internet.defer.Deferred` |
880 | + """ |
881 | + cloud_init = self._create_cloud_init(machine_id, zookeepers) |
882 | + cloud_init.set_provider_type(self._provider.provider_type) |
883 | + filestorage = self._provider.get_file_storage() |
884 | + # Only the master is required to get its own instance id like this. |
885 | + if self._master: |
886 | + id_name = "juju_master_id" |
887 | + cloud_init.set_instance_id_accessor("$(curl %s)" % ( |
888 | + filestorage.get_url(id_name),)) |
889 | + user_data = cloud_init.render() |
890 | + |
891 | + # For openstack deployments, really need image id configured as there |
892 | + # are no standards to provide a fallback value. |
893 | + image_id = self._provider.config.get("default-image-id") |
894 | + if image_id is None: |
895 | + raise ProviderError("Need to specify a default-image-id") |
896 | + |
897 | + security_groups = ( |
898 | + yield self._provider.port_manager.ensure_groups(machine_id)) |
899 | + |
900 | + # Until constraints are implemented, need to a configured instance |
901 | + # type and resolve it to a flavor id here. |
902 | + flavor_name = self._provider.config.get("default-instance-type") |
903 | + if flavor_name is None: |
904 | + raise ProviderError("Need to specify a default-instance-type") |
905 | + flavors = yield self._provider.nova.list_flavors() |
906 | + flavor_map = dict((f['name'], f['id']) for f in flavors) |
907 | + if not flavor_name in flavor_map: |
908 | + ProviderError("Unknown instance type given: %r" % (flavor_name,)) |
909 | + |
910 | + server = yield self._provider.nova.run_server( |
911 | + name="juju %s instance %s" % |
912 | + (self._provider.environment_name, machine_id,), |
913 | + image_id=image_id, |
914 | + flavor_id=flavor_map[flavor_name], |
915 | + security_group_names=security_groups, |
916 | + user_data=user_data) |
917 | + |
918 | + if self._master: |
919 | + yield filestorage.put(id_name, StringIO(str(server['id']))) |
920 | + |
921 | + # For private clouds allow an option of attaching public |
922 | + # floating ips to all the machines. None of the extant public |
923 | + # clouds need this. |
924 | + if self._provider.config.get('use-floating-ip'): |
925 | + # Not possible to attach a floating ip to a newly booted |
926 | + # server, must wait for networking to be ready when some |
927 | + # kind of address exists. |
928 | + while not server.get('addresses'): |
929 | + status = get_server_status(server) |
930 | + if status != "pending": |
931 | + raise ProviderInteractionError( |
932 | + "Server out of pending status " |
933 | + "without addresses set: %r" % server) |
934 | + # Bad, bad place to be doing a wait loop directly |
935 | + yield twistutils.sleep(self._DELAY_FOR_ADDRESSES) |
936 | + log.debug("Waited for %d seconds for networking on server %r", |
937 | + self._DELAY_FOR_ADDRESSES, server['id']) |
938 | + server = yield self._provider.nova.get_server(server['id']) |
939 | + yield _assign_floating_ip(self._provider, server['id']) |
940 | + |
941 | + defer.returnValue([machine_from_instance(server)]) |
942 | + |
943 | + |
944 | +@defer.inlineCallbacks |
945 | +def _assign_floating_ip(provider, server_id): |
946 | + floating_ips = yield provider.nova.list_floating_ips() |
947 | + for floating_ip in floating_ips: |
948 | + if floating_ip['instance_id'] is None: |
949 | + break |
950 | + else: |
951 | + floating_ip = yield provider.nova.allocate_floating_ip() |
952 | + yield provider.nova.add_floating_ip(server_id, floating_ip['ip']) |
953 | |
954 | === added file 'juju/providers/openstack/machine.py' |
955 | --- juju/providers/openstack/machine.py 1970-01-01 00:00:00 +0000 |
956 | +++ juju/providers/openstack/machine.py 2012-07-18 19:47:20 +0000 |
957 | @@ -0,0 +1,68 @@ |
958 | +"""Helpers for mapping Nova api results to the juju machine abstraction""" |
959 | + |
960 | +from juju.machine import ProviderMachine |
961 | + |
962 | + |
963 | +_SERVER_STATE_MAP = { |
964 | + None: 'pending', |
965 | + 'ACTIVE': 'running', |
966 | + 'BUILD': 'pending', |
967 | + 'BUILDING': 'pending', |
968 | + 'REBUILDING': 'pending', |
969 | + 'DELETED': 'terminated', |
970 | + 'STOPPED': 'stopped', |
971 | + } |
972 | + |
973 | + |
974 | +class NovaProviderMachine(ProviderMachine): |
975 | + """Nova-specific ProviderMachine implementation""" |
976 | + |
977 | + |
978 | +def get_server_status(server): |
979 | + status = server.get('status') |
980 | + if status is not None and "(" in status: |
981 | + status = status.split("(", 1)[0] |
982 | + return _SERVER_STATE_MAP.get(status, 'unknown') |
983 | + |
984 | + |
985 | +def get_server_addresses(server): |
986 | + private_addr = public_addr = None |
987 | + addresses = server.get("addresses") |
988 | + if addresses is not None: |
989 | + # Issue with some setups, have custom network only, use as private |
990 | + network = () |
991 | + for name in sorted(addresses): |
992 | + if name not in ("private", "public"): |
993 | + network = addresses[name] |
994 | + if network: |
995 | + break |
996 | + network = addresses.get("private", network) |
997 | + for address in network: |
998 | + if address.get("version", 0) == 4: |
999 | + private_addr = address['addr'] |
1000 | + break |
1001 | + # Issue with HP cloud, public address is second in private network |
1002 | + network = addresses.get("public", network[1:]) |
1003 | + for address in network: |
1004 | + if address.get("version", 0) == 4: |
1005 | + public_addr = address['addr'] |
1006 | + return private_addr, public_addr |
1007 | + |
1008 | + |
1009 | +def machine_from_instance(server): |
1010 | + """Create an :class:`NovaProviderMachine` from a server details dict |
1011 | + |
1012 | + :param server: a dictionary of server info as given by the Nova api |
1013 | + |
1014 | + :return: a matching :class:`NovaProviderMachine` |
1015 | + """ |
1016 | + private_addr, public_addr = get_server_addresses(server) |
1017 | + # Juju assumes it always needs a public address and loops waiting for one. |
1018 | + # In fact a private address is generally fine provided it can be sshed to. |
1019 | + if public_addr is None and private_addr is not None: |
1020 | + public_addr = private_addr |
1021 | + return NovaProviderMachine( |
1022 | + server['id'], |
1023 | + public_addr, |
1024 | + private_addr, |
1025 | + get_server_status(server)) |
1026 | |
1027 | === added file 'juju/providers/openstack/ports.py' |
1028 | --- juju/providers/openstack/ports.py 1970-01-01 00:00:00 +0000 |
1029 | +++ juju/providers/openstack/ports.py 2012-07-18 19:47:20 +0000 |
1030 | @@ -0,0 +1,179 @@ |
1031 | +"""Manage port access to machines using Nova security group extension |
1032 | + |
1033 | +The mechanism is based on the existing scheme used by the EC2 provider. |
1034 | + |
1035 | +Each machine is launched with two security groups, a juju group that is shared |
1036 | +across all machines and allows access to 22/tcp for ssh, and a machine group |
1037 | +just for that server so ports can be opened and closed on an individual level. |
1038 | + |
1039 | +There is some mismatch between the port hole poking and security group models: |
1040 | +* A new security group is created for every machine |
1041 | +* Rules are not shared between service units but set up again each launch |
1042 | +* Support for port ranges is not exposed |
1043 | + |
1044 | +The Nova security group module follows the EC2 example quite closely, but as |
1045 | +of Essex it's still under contrib and has a number of quirks: |
1046 | +* To run a server with, or add or remove groups from a server, 'name' is used |
1047 | +* To get details, delete, or add or remove rules from a group, 'id' is needed |
1048 | + |
1049 | +The only way of getting 'id' if 'name' is known is by listing all groups then |
1050 | +looking at the details of the one with the matching name. |
1051 | +""" |
1052 | + |
1053 | +from twisted.internet import ( |
1054 | + defer, |
1055 | + ) |
1056 | + |
1057 | +from juju import errors |
1058 | + |
1059 | +from .client import log |
1060 | + |
1061 | + |
1062 | +class NovaPortManager(object): |
1063 | + """Mapping of port-based juju interface to Nova security group actions |
1064 | + |
1065 | + There is the potential to record some state on the instance to reduce api |
1066 | + round-trips when, for instance, launching multiple machines at once, but |
1067 | + for now |
1068 | + """ |
1069 | + |
1070 | + def __init__(self, nova, environment_name): |
1071 | + self.nova = nova |
1072 | + self.tag = environment_name |
1073 | + |
1074 | + def _juju_group_name(self): |
1075 | + return "juju-%s" % (self.tag,) |
1076 | + |
1077 | + def _machine_group_name(self, machine_id): |
1078 | + return "juju-%s-%s" % (self.tag, machine_id) |
1079 | + |
1080 | + @defer.inlineCallbacks |
1081 | + def _get_machine_group(self, machine, machine_id): |
1082 | + """Get details of the machine specific security group |
1083 | + |
1084 | + As only the name of the group can be derived, this means listing every |
1085 | + security group for that server and seeing which has a matching name. |
1086 | + """ |
1087 | + group_name = self._machine_group_name(machine_id) |
1088 | + server_id = machine.instance_id |
1089 | + groups = yield self.nova.get_server_security_groups(server_id) |
1090 | + for group in groups: |
1091 | + if group['name'] == group_name: |
1092 | + defer.returnValue(group) |
1093 | + raise errors.ProviderInteractionError( |
1094 | + "Missing security group %r for machine %r" % |
1095 | + (group_name, server_id)) |
1096 | + |
1097 | + @defer.inlineCallbacks |
1098 | + def open_port(self, machine, machine_id, port, protocol="tcp"): |
1099 | + """Allow access to a port for the given machine only""" |
1100 | + group = yield self._get_machine_group(machine, machine_id) |
1101 | + yield self.nova.add_security_group_rule(group['id'], |
1102 | + ip_protocol=protocol, from_port=port, to_port=port) |
1103 | + log.debug("Opened %s/%s on machine %r", |
1104 | + port, protocol, machine.instance_id) |
1105 | + |
1106 | + @defer.inlineCallbacks |
1107 | + def close_port(self, machine, machine_id, port, protocol="tcp"): |
1108 | + """Revoke access to a port for the given machine only""" |
1109 | + group = yield self._get_machine_group(machine, machine_id) |
1110 | + for rule in group["rules"]: |
1111 | + if (port == rule["from_port"] == rule["to_port"] and |
1112 | + rule["ip_protocol"] == protocol): |
1113 | + yield self.nova.delete_security_group_rule(rule["id"]) |
1114 | + log.debug("Closed %s/%s on machine %r", |
1115 | + port, protocol, machine.instance_id) |
1116 | + return |
1117 | + raise errors.ProviderInteractionError( |
1118 | + "Couldn't close unopened %s/%s on machine %r", |
1119 | + port, protocol, machine.instance_id) |
1120 | + |
1121 | + @defer.inlineCallbacks |
1122 | + def get_opened_ports(self, machine, machine_id): |
1123 | + """Get a set of opened port/protocol pairs for a machine""" |
1124 | + group = yield self._get_machine_group(machine, machine_id) |
1125 | + opened_ports = set() |
1126 | + for rule in group.get("rules", []): |
1127 | + if not rule.get("group"): |
1128 | + protocol = rule["ip_protocol"] |
1129 | + from_port = rule["from_port"] |
1130 | + to_port = rule["to_port"] |
1131 | + if from_port == to_port: |
1132 | + opened_ports.add((from_port, protocol)) |
1133 | + defer.returnValue(opened_ports) |
1134 | + |
1135 | + @defer.inlineCallbacks |
1136 | + def ensure_groups(self, machine_id): |
1137 | + """Get names of the security groups for a machine, creating if needed |
1138 | + |
1139 | + If the juju group already exists, it is assumed to be correctly set up. |
1140 | + If the machine group already exists, it is deleted then recreated. |
1141 | + """ |
1142 | + security_groups = yield self.nova.list_security_groups() |
1143 | + groups_by_name = dict((sg['name'], sg['id']) for sg in security_groups) |
1144 | + |
1145 | + juju_group = self._juju_group_name() |
1146 | + if not juju_group in groups_by_name: |
1147 | + log.debug("Creating juju security group %s", juju_group) |
1148 | + sg = yield self.nova.create_security_group(juju_group, |
1149 | + "juju group for %s" % (self.tag,)) |
1150 | + # Add external ssh access |
1151 | + yield self.nova.add_security_group_rule(sg['id'], |
1152 | + ip_protocol="tcp", from_port=22, to_port=22) |
1153 | + # Add internal group access |
1154 | + yield self.nova.add_security_group_rule( |
1155 | + parent_group_id=sg['id'], group_id=sg['id'], |
1156 | + ip_protocol="tcp", from_port=1, to_port=65535) |
1157 | + |
1158 | + machine_group = self._machine_group_name(machine_id) |
1159 | + if machine_group in groups_by_name: |
1160 | + yield self.nova.delete_security_group( |
1161 | + groups_by_name[machine_group]) |
1162 | + log.debug("Creating machine security group %s", machine_group) |
1163 | + yield self.nova.create_security_group(machine_group, |
1164 | + "juju group for %s machine %s" % (self.tag, machine_id)) |
1165 | + |
1166 | + defer.returnValue([juju_group, machine_group]) |
1167 | + |
1168 | + @defer.inlineCallbacks |
1169 | + def get_machine_groups(self, machine, with_juju_group=False): |
1170 | + try: |
1171 | + ret = yield self.get_machine_groups_pure(machine, with_juju_group) |
1172 | + except errors.ProviderInteractionError, e: |
1173 | + # XXX: Need to wire up treatment of 500s properly in client |
1174 | + if getattr(e, "kind", None) == "computeError": |
1175 | + try: |
1176 | + yield self.nova.get_server(machine.instance_id) |
1177 | + except errors.ProviderInteractionError, e: |
1178 | + pass # just rebinding e |
1179 | + if True or getattr(e, "kind", None) == "itemNotFound": |
1180 | + defer.returnValue(None) |
1181 | + raise |
1182 | + defer.returnValue(ret) |
1183 | + |
1184 | + @defer.inlineCallbacks |
1185 | + def get_machine_groups_pure(self, machine, with_juju_group=False): |
1186 | + server_id = machine.instance_id |
1187 | + groups = yield self.nova.get_server_security_groups(server_id) |
1188 | + juju_group = self._juju_group_name() |
1189 | + groups_by_name = dict((g['name'], g['id']) for g in groups |
1190 | + if g['name'].startswith(juju_group)) |
1191 | + if juju_group not in groups_by_name: |
1192 | + # Not a juju machine, shouldn't touch |
1193 | + defer.returnValue(None) |
1194 | + if not with_juju_group: |
1195 | + groups_by_name.pop(juju_group) |
1196 | + # else assumption: only one remaining group, is the machine group |
1197 | + defer.returnValue(groups_by_name) |
1198 | + |
1199 | + @defer.inlineCallbacks |
1200 | + def delete_juju_group(self): |
1201 | + security_groups = yield self.nova.list_security_groups() |
1202 | + juju_group = self._juju_group_name() |
1203 | + for group in security_groups: |
1204 | + if group['name'] == juju_group: |
1205 | + break |
1206 | + else: |
1207 | + log.debug("Can't delete missing juju group") |
1208 | + return |
1209 | + yield self.nova.delete_security_group(group['id']) |
1210 | |
1211 | === added file 'juju/providers/openstack/provider.py' |
1212 | --- juju/providers/openstack/provider.py 1970-01-01 00:00:00 +0000 |
1213 | +++ juju/providers/openstack/provider.py 2012-07-18 19:47:20 +0000 |
1214 | @@ -0,0 +1,189 @@ |
1215 | +"""Provider interface implementation for OpenStack backend |
1216 | + |
1217 | +Much of the logic is implemented in sibling modules, but the overall model is |
1218 | +exposed here. |
1219 | + |
1220 | +Still in need of work here: |
1221 | +* Implement constraints using the Nova flavors api. This will always mean an |
1222 | + api call rather than hard coding values as is done with EC2. Things like |
1223 | + memory and cpu count are broadly equivalent, but there's no guarentee what |
1224 | + details are exposed and ranking by price will generally not be an option. |
1225 | +""" |
1226 | + |
1227 | +import logging |
1228 | + |
1229 | +from twisted.internet import defer |
1230 | + |
1231 | +from juju import errors |
1232 | +from juju.providers.common.base import MachineProviderBase |
1233 | + |
1234 | +from .client import _OpenStackClient, _NovaClient, _SwiftClient |
1235 | +from . import credentials |
1236 | +from .files import FileStorage |
1237 | +from .launch import NovaLaunchMachine |
1238 | +from .machine import ( |
1239 | + NovaProviderMachine, get_server_status, machine_from_instance |
1240 | + ) |
1241 | +from .ports import NovaPortManager |
1242 | + |
1243 | + |
1244 | +log = logging.getLogger("juju.openstack") |
1245 | + |
1246 | + |
1247 | +class MachineProvider(MachineProviderBase): |
1248 | + """MachineProvider for use in an OpenStack environment""" |
1249 | + |
1250 | + Credentials = credentials.OpenStackCredentials |
1251 | + |
1252 | + def __init__(self, environment_name, config): |
1253 | + super(MachineProvider, self).__init__(environment_name, config) |
1254 | + self.credentials = self.Credentials.from_environment(config) |
1255 | + client = _OpenStackClient(self.credentials) |
1256 | + self.nova = _NovaClient(client) |
1257 | + self.swift = _SwiftClient(client) |
1258 | + self.port_manager = NovaPortManager(self.nova, environment_name) |
1259 | + |
1260 | + @property |
1261 | + def provider_type(self): |
1262 | + return "openstack" |
1263 | + |
1264 | + def get_serialization_data(self): |
1265 | + """Get provider configuration suitable for serialization. |
1266 | + |
1267 | + Also fills in credential information that may have earlier been |
1268 | + extracted from the environment. |
1269 | + """ |
1270 | + data = super(MachineProvider, self).get_serialization_data() |
1271 | + self.credentials.set_config_defaults(data) |
1272 | + return data |
1273 | + |
1274 | + def get_file_storage(self): |
1275 | + """Retrieve a Swift-backed :class:`FileStorage`.""" |
1276 | + return FileStorage(self.swift, self.config["control-bucket"]) |
1277 | + |
1278 | + def start_machine(self, machine_data, master=False): |
1279 | + """Start an OpenStack machine. |
1280 | + |
1281 | + :param dict machine_data: desired characteristics of the new machine; |
1282 | + it must include a "machine-id" key, and may include a "constraints" |
1283 | + key to specify the underlying OS and hardware. |
1284 | + |
1285 | + :param bool master: if True, machine will initialize the juju admin |
1286 | + and run a provisioning agent, in addition to running a machine |
1287 | + agent. |
1288 | + """ |
1289 | + return NovaLaunchMachine.launch(self, machine_data, master) |
1290 | + |
1291 | + @defer.inlineCallbacks |
1292 | + def get_machines(self, instance_ids=()): |
1293 | + """List machines running in the provider. |
1294 | + |
1295 | + :param list instance_ids: ids of instances you want to get. Leave empty |
1296 | + to list every |
1297 | + :class:`juju.providers.openstack.machine.NovaProviderMachine` owned |
1298 | + by this provider. |
1299 | + |
1300 | + :return: a list of |
1301 | + :class:`juju.providers.openstack.machine.NovaProviderMachine` |
1302 | + instances |
1303 | + :rtype: :class:`twisted.internet.defer.Deferred` |
1304 | + |
1305 | + :raises: :exc:`juju.errors.MachinesNotFound` |
1306 | + """ |
1307 | + if len(instance_ids) == 1: |
1308 | + try: |
1309 | + instances = [(yield self.nova.get_server(instance_ids[0]))] |
1310 | + except errors.ProviderInteractionError, e: |
1311 | + # XXX: Need to wire up treatment of 404s properly in client |
1312 | + if True or getattr(e, "kind", None) == "itemNotFound": |
1313 | + raise errors.MachinesNotFound(set(instance_ids)) |
1314 | + raise |
1315 | + instance_ids = frozenset(instance_ids) |
1316 | + else: |
1317 | + instances = yield self.nova.list_servers() |
1318 | + if instance_ids: |
1319 | + instance_ids = frozenset(instance_ids) |
1320 | + instances = [instance for instance in instances |
1321 | + if instance['id'] in instance_ids] |
1322 | + |
1323 | + # Only want to deal with servers that were created by juju, checking |
1324 | + # the name begins with the prefix launch uses is good enough. |
1325 | + name_prefix = "juju %s instance " % (self.environment_name,) |
1326 | + machines = [] |
1327 | + for instance in instances: |
1328 | + if (instance['name'].startswith(name_prefix) and |
1329 | + get_server_status(instance) in ("running", "pending")): |
1330 | + machines.append(machine_from_instance(instance)) |
1331 | + |
1332 | + if instance_ids: |
1333 | + # We were asked for a specific list of machines, and if we can't |
1334 | + # completely fulfil that request we should blow up. |
1335 | + missing = instance_ids.difference(m.instance_id for m in machines) |
1336 | + if missing: |
1337 | + raise errors.MachinesNotFound(missing) |
1338 | + |
1339 | + defer.returnValue(machines) |
1340 | + |
1341 | + @defer.inlineCallbacks |
1342 | + def _delete_machine(self, machine, full=False): |
1343 | + server_id = machine.instance_id |
1344 | + server = yield self.nova.get_server(server_id) |
1345 | + if not server['name'].startswith( |
1346 | + "juju %s instance" % self.environment_name): |
1347 | + raise errors.MachinesNotFound(set([machine.instance_id])) |
1348 | + yield self.nova.delete_server(server_id) |
1349 | + defer.returnValue(machine) |
1350 | + |
1351 | + def shutdown_machine(self, machine): |
1352 | + if not isinstance(machine, NovaProviderMachine): |
1353 | + raise errors.ProviderError( |
1354 | + "Need a NovaProviderMachine to shutdown not: %r" % (machine,)) |
1355 | + # EC2 provider re-gets the machine to see if it's still in existance |
1356 | + # and can be shutdown, instead just handle an error? 404-ish? |
1357 | + return self._delete_machine(machine) |
1358 | + |
1359 | + @defer.inlineCallbacks |
1360 | + def destroy_environment(self): |
1361 | + """Terminate all associated machines and security groups. |
1362 | + |
1363 | + The super defintion of this method terminates each machine in |
1364 | + the environment; this needs to be augmented here by also |
1365 | + removing the security group for the environment. |
1366 | + |
1367 | + :rtype: :class:`twisted.internet.defer.Deferred` |
1368 | + """ |
1369 | + machines = yield self.get_machines() |
1370 | + deleted_machines = yield defer.gatherResults( |
1371 | + [self._delete_machine(m, True) for m in machines]) |
1372 | + yield self.save_state({}) |
1373 | + defer.returnValue(deleted_machines) |
1374 | + |
1375 | + def shutdown_machines(self, machines): |
1376 | + """Terminate machines associated with this provider. |
1377 | + |
1378 | + :param machines: machines to shut down |
1379 | + :type machines: list of |
1380 | + :class:`juju.providers.openstack.machine.NovaProviderMachine` |
1381 | + |
1382 | + :return: list of terminated |
1383 | + :class:`juju.providers.openstack.machine.NovaProviderMachine` |
1384 | + instances |
1385 | + :rtype: :class:`twisted.internet.defer.Deferred` |
1386 | + """ |
1387 | + # XXX: need to actually handle errors as non-terminated machines |
1388 | + # and not include them in the resulting list |
1389 | + return defer.gatherResults( |
1390 | + [self.shutdown_machine(m) for m in machines], consumeErrors=True) |
1391 | + |
1392 | + def open_port(self, machine, machine_id, port, protocol="tcp"): |
1393 | + """Authorizes `port` using `protocol` on EC2 for `machine`.""" |
1394 | + return self.port_manager.open_port(machine, machine_id, port, protocol) |
1395 | + |
1396 | + def close_port(self, machine, machine_id, port, protocol="tcp"): |
1397 | + """Revokes `port` using `protocol` on EC2 for `machine`.""" |
1398 | + return self.port_manager.close_port( |
1399 | + machine, machine_id, port, protocol) |
1400 | + |
1401 | + def get_opened_ports(self, machine, machine_id): |
1402 | + """Returns a set of open (port, proto) pairs for `machine`.""" |
1403 | + return self.port_manager.get_opened_ports(machine, machine_id) |
1404 | |
1405 | === added directory 'juju/providers/openstack/tests' |
1406 | === added file 'juju/providers/openstack/tests/__init__.py' |
1407 | --- juju/providers/openstack/tests/__init__.py 1970-01-01 00:00:00 +0000 |
1408 | +++ juju/providers/openstack/tests/__init__.py 2012-07-18 19:47:20 +0000 |
1409 | @@ -0,0 +1,206 @@ |
1410 | +"""Infrastructure shared by all tests for OpenStack provider""" |
1411 | + |
1412 | +import json |
1413 | + |
1414 | +from twisted.internet import ( |
1415 | + defer, |
1416 | + ) |
1417 | +from twisted.web import ( |
1418 | + http, |
1419 | + http_headers, |
1420 | + ) |
1421 | + |
1422 | +from juju import errors |
1423 | +from juju.lib import ( |
1424 | + mocker, |
1425 | + ) |
1426 | + |
1427 | +from juju.providers.openstack import ( |
1428 | + client, |
1429 | + files, |
1430 | + machine, |
1431 | + ports, |
1432 | + provider, |
1433 | + ) |
1434 | + |
1435 | + |
1436 | +class FakeResponse(object): |
1437 | + |
1438 | + def __init__(self, code): |
1439 | + if code not in http.RESPONSES: |
1440 | + raise ValueError("Unknown http code: %r" % (code,)) |
1441 | + self.code = code |
1442 | + self.headers = http_headers.Headers( |
1443 | + {"Content-Type": ["application/json"]}) |
1444 | + |
1445 | + |
1446 | +class OpenStackTestMixin(object): |
1447 | + """Helpers for test classes that want to exercise the OpenStack APIs |
1448 | + |
1449 | + This goes all the way down to the http layer, which is really a little too |
1450 | + low for most of what the test cases want to assert. With some cleanups to |
1451 | + the client code, tests could instead mock out client methods. |
1452 | + """ |
1453 | + |
1454 | + environment_name = "testing" |
1455 | + api_url = "http://test.invalid/v2.0/" |
1456 | + |
1457 | + def setUp(self): |
1458 | + self._mock_request = self.mocker.replace( |
1459 | + "juju.providers.openstack.client.request", |
1460 | + passthrough=False) |
1461 | + self.mocker.order() |
1462 | + self.nova_url = self.api_url + "nova" |
1463 | + self.swift_url = self.api_url + "swift" |
1464 | + self._mock_request("POST", self.api_url + "tokens", body=mocker.ANY) |
1465 | + self.mocker.result(defer.succeed((FakeResponse(200), |
1466 | + json.dumps({"access": { |
1467 | + "token": {"id": "tok", "expires": "2030-01-01T00:00:00"}, |
1468 | + "serviceCatalog": [ |
1469 | + { |
1470 | + "type": "compute", |
1471 | + "endpoints": [{"publicURL": self.nova_url}] |
1472 | + }, |
1473 | + { |
1474 | + "type": "object-store", |
1475 | + "endpoints": [{"publicURL": self.swift_url}] |
1476 | + } |
1477 | + ] |
1478 | + }})))) |
1479 | + # Clear the environment so provider won't pick up config from envvars |
1480 | + self.change_environment() |
1481 | + |
1482 | + def get_config(self): |
1483 | + return { |
1484 | + "type": "openstack", |
1485 | + "admin-secret": "password", |
1486 | + "access-key": "90abcdef", |
1487 | + "secret-key": "xxxxxxxx", |
1488 | + "auth-mode": "keypair", |
1489 | + "auth-url": self.api_url, |
1490 | + "project-name": "test_project", |
1491 | + "control-bucket": self.environment_name, |
1492 | + "default-instance-type": "standard.xsmall", |
1493 | + "default-image-id": 42, |
1494 | + "use-floating-ip": True, |
1495 | + } |
1496 | + |
1497 | + def get_provider(self): |
1498 | + """Return the openstack machine provider. |
1499 | + |
1500 | + This should only be invoked after mocker is in replay mode. |
1501 | + """ |
1502 | + return provider.MachineProvider(self.environment_name, |
1503 | + self.get_config()) |
1504 | + |
1505 | + def make_server(self, server_id, name=None, status="ACTIVE"): |
1506 | + if name is None: |
1507 | + # Would be machine id rather than server id really but will do. |
1508 | + name = "juju testing instance " + str(server_id) |
1509 | + return { |
1510 | + "id": server_id, |
1511 | + "name": name, |
1512 | + "status": status, |
1513 | + "addresses": {}, |
1514 | + } |
1515 | + |
1516 | + def assert_not_found(self, deferred, server_ids): |
1517 | + self.assertFailure(deferred, errors.MachinesNotFound) |
1518 | + return deferred.addCallback(self._check_not_found, server_ids) |
1519 | + |
1520 | + def _check_not_found(self, error, server_ids): |
1521 | + self.assertEqual(error.instance_ids, server_ids) |
1522 | + |
1523 | + def _mock_nova(self, method, path, body): |
1524 | + self._mock_request(method, |
1525 | + "%s/%s" % (self.nova_url, path), |
1526 | + [("X-Auth-Token", "tok")], |
1527 | + body) |
1528 | + |
1529 | + def _mock_swift(self, method, path, body, extra_headers=None): |
1530 | + headers = [("X-Auth-Token", "tok")] |
1531 | + if extra_headers is not None: |
1532 | + headers += extra_headers |
1533 | + url = "%s/%s" % (self.swift_url, path) |
1534 | + self._mock_request(method, url, headers, body) |
1535 | + |
1536 | + def expect_nova_get(self, path, code=200, response=""): |
1537 | + self._mock_nova("GET", path, None) |
1538 | + if not isinstance(response, str): |
1539 | + response = json.dumps(response) |
1540 | + self.mocker.result(defer.succeed((FakeResponse(code), response))) |
1541 | + |
1542 | + def expect_nova_post(self, path, body, code=200, response=""): |
1543 | + self._mock_nova("POST", path, body) |
1544 | + if not isinstance(response, str): |
1545 | + response = json.dumps(response) |
1546 | + self.mocker.result(defer.succeed((FakeResponse(code), response))) |
1547 | + |
1548 | + def expect_nova_delete(self, path, code=202, response=""): |
1549 | + self._mock_nova("DELETE", path, None) |
1550 | + self.mocker.result(defer.succeed((FakeResponse(code), response))) |
1551 | + |
1552 | + def expect_swift_get(self, path, code=200, response=""): |
1553 | + self._mock_swift("GET", path, None) |
1554 | + self.mocker.result(defer.succeed((FakeResponse(code), response))) |
1555 | + |
1556 | + def expect_swift_put(self, path, body, code=201, response=""): |
1557 | + self._mock_swift("PUT", path, body) |
1558 | + self.mocker.result(defer.succeed((FakeResponse(code), response))) |
1559 | + |
1560 | + def expect_swift_put_container(self, container, code=201, response=""): |
1561 | + self._mock_swift("PUT", container, "", [('X-Container-Read', '.r:*')]) |
1562 | + self.mocker.result(defer.succeed((FakeResponse(code), response))) |
1563 | + |
1564 | + |
1565 | +class MockedProvider(object): |
1566 | + |
1567 | + provider_type = "openstack" |
1568 | + environment_name = "testing" |
1569 | + api_url = "http://testing.invalid/2.0/" |
1570 | + |
1571 | + default_config = { |
1572 | + "type": "openstack", |
1573 | + "admin-secret": "asecret", |
1574 | + "username": "auser", |
1575 | + "password": "xxxxxxxx", |
1576 | + "auth-url": api_url, |
1577 | + "project-name": "aproject", |
1578 | + "control-bucket": environment_name, |
1579 | + "default-instance-type": "standard.xsmall", |
1580 | + "default-image-id": 42, |
1581 | + } |
1582 | + |
1583 | + def __init__(self, mocker, config=None): |
1584 | + if config is None: |
1585 | + config = dict(self.default_config) |
1586 | + self.config = config |
1587 | + self.nova = mocker.proxy(client._NovaClient(None), passthrough=False) |
1588 | + self.swift = mocker.proxy(client._SwiftClient(None), passthrough=False) |
1589 | + self.port_manager = mocker.proxy( |
1590 | + ports.NovaPortManager(self.nova, self.environment_name)) |
1591 | + self.provider_actions = mocker.mock() |
1592 | + self.mocker = mocker |
1593 | + |
1594 | + def get_file_storage(self): |
1595 | + return files.FileStorage(self.swift, self.config['control-bucket']) |
1596 | + |
1597 | + def __getattr__(self, attr): |
1598 | + return getattr(self.provider_actions, attr) |
1599 | + |
1600 | + def expect_swift_public_object_url(self, object_name): |
1601 | + container_name = self.config['control-bucket'] |
1602 | + event = self.swift.public_object_url(container_name, object_name) |
1603 | + self.mocker.result("%sswift/%s" % (self.api_url, object_name)) |
1604 | + return event |
1605 | + |
1606 | + def expect_swift_put(self, object_name, body, code=201, response=""): |
1607 | + container_name = self.config['control-bucket'] |
1608 | + event = self.swift.put_object(container_name, object_name, body) |
1609 | + self.mocker.result(defer.succeed((FakeResponse(code), response))) |
1610 | + return event |
1611 | + |
1612 | + def expect_zookeeper_machines(self, iid): |
1613 | + event = self.provider_actions.get_zookeeper_machines() |
1614 | + self.mocker.result(defer.succeed([machine.NovaProviderMachine(iid)])) |
1615 | + return event |
1616 | |
1617 | === added file 'juju/providers/openstack/tests/test_bootstrap.py' |
1618 | --- juju/providers/openstack/tests/test_bootstrap.py 1970-01-01 00:00:00 +0000 |
1619 | +++ juju/providers/openstack/tests/test_bootstrap.py 2012-07-18 19:47:20 +0000 |
1620 | @@ -0,0 +1,213 @@ |
1621 | +"""Tests for bootstrapping juju on openstack |
1622 | + |
1623 | +Bootstrap touches a lot of the other parts of the provider, including machine |
1624 | +launching, security groups and so on. Testing this in an end-to-end fashion as |
1625 | +is done currently duplicates many checks from other more focussed tests. |
1626 | +""" |
1627 | + |
1628 | +import logging |
1629 | +import yaml |
1630 | + |
1631 | +from juju.lib import ( |
1632 | + mocker, |
1633 | + testing, |
1634 | + ) |
1635 | +from juju.machine import constraints |
1636 | + |
1637 | +from juju.providers.openstack.machine import NovaProviderMachine |
1638 | + |
1639 | +from juju.providers.openstack.tests import OpenStackTestMixin |
1640 | + |
1641 | + |
1642 | +class OpenStackBootstrapTest(OpenStackTestMixin, testing.TestCase): |
1643 | + |
1644 | + def expect_verify(self): |
1645 | + self.expect_swift_put("testing/bootstrap-verify", |
1646 | + "storage is writable") |
1647 | + |
1648 | + def expect_provider_state_fresh(self): |
1649 | + self.expect_swift_get("testing/provider-state") |
1650 | + self.expect_verify() |
1651 | + |
1652 | + def expect_create_group(self): |
1653 | + self.expect_nova_post("os-security-groups", |
1654 | + {'security_group': { |
1655 | + 'name': 'juju-testing', |
1656 | + 'description': 'juju group for testing', |
1657 | + }}, |
1658 | + response={'security_group': { |
1659 | + 'id': 1, |
1660 | + }}) |
1661 | + self.expect_nova_post("os-security-group-rules", |
1662 | + {'security_group_rule': { |
1663 | + 'parent_group_id': 1, |
1664 | + 'ip_protocol': "tcp", |
1665 | + 'from_port': 22, |
1666 | + 'to_port': 22, |
1667 | + }}, |
1668 | + response={'security_group_rule': { |
1669 | + 'id': 144, 'parent_group_id': 1, |
1670 | + }}) |
1671 | + self.expect_nova_post("os-security-group-rules", |
1672 | + {'security_group_rule': { |
1673 | + 'parent_group_id': 1, |
1674 | + 'group_id': 1, |
1675 | + 'ip_protocol': "tcp", |
1676 | + 'from_port': 1, |
1677 | + 'to_port': 65535, |
1678 | + }}, |
1679 | + response={'security_group_rule': { |
1680 | + 'id': 145, 'parent_group_id': 1, |
1681 | + }}) |
1682 | + |
1683 | + def expect_create_machine_group(self, machine_id): |
1684 | + machine = str(machine_id) |
1685 | + self.expect_nova_post("os-security-groups", |
1686 | + {'security_group': { |
1687 | + 'name': 'juju-testing-' + machine, |
1688 | + 'description': 'juju group for testing machine ' + machine, |
1689 | + }}, |
1690 | + response={'security_group': { |
1691 | + 'id': 2, |
1692 | + }}) |
1693 | + |
1694 | + def _match_server(self, data): |
1695 | + userdata = data['server'].pop('user_data').decode("base64") |
1696 | + self.assertEqual("#cloud-config", userdata.split("\n", 1)[0]) |
1697 | + cloud_init = yaml.load(userdata) |
1698 | + # TODO: assertions on cloud-init content |
1699 | + self.assertEqual({'server': { |
1700 | + 'flavorRef': 1, |
1701 | + 'imageRef': 42, |
1702 | + 'name': 'juju testing instance 0', |
1703 | + 'security_groups': [ |
1704 | + {'name': 'juju-testing'}, |
1705 | + {'name': 'juju-testing-0'}, |
1706 | + ], |
1707 | + }}, |
1708 | + data) |
1709 | + return True |
1710 | + |
1711 | + def expect_launch(self): |
1712 | + self.expect_nova_get("flavors", |
1713 | + response={'flavors': [{'id': 1, 'name': "standard.xsmall"}]}) |
1714 | + self.expect_nova_post("servers", mocker.MATCH(self._match_server), |
1715 | + code=202, response={'server': { |
1716 | + 'id': '1000', |
1717 | + 'status': "PENDING", |
1718 | + 'addresses': {'private': [{'version': 4, 'addr': "4.4.4.4"}]}, |
1719 | + }}) |
1720 | + self.expect_swift_put("testing/juju_master_id", "1000") |
1721 | + self.expect_nova_get("os-floating-ips", |
1722 | + response={'floating_ips': [ |
1723 | + {'id': 80, 'instance_id': None, 'ip': "8.8.8.8"} |
1724 | + ]}) |
1725 | + self.expect_nova_post("servers/1000/action", |
1726 | + {"addFloatingIp": {"address": "8.8.8.8"}}, code=202) |
1727 | + self.expect_swift_put("testing/provider-state", |
1728 | + yaml.dump({'zookeeper-instances': ['1000']})) |
1729 | + |
1730 | + def _check_machine(self, machine_list): |
1731 | + [machine] = machine_list |
1732 | + self.assertTrue(isinstance(machine, NovaProviderMachine)) |
1733 | + self.assertEqual(machine.instance_id, '1000') |
1734 | + |
1735 | + def bootstrap(self): |
1736 | + provider = self.get_provider() |
1737 | + constraint_set = constraints.ConstraintSet(provider.provider_type) |
1738 | + return provider.bootstrap(constraint_set.load({})) |
1739 | + |
1740 | + def test_bootstrap_clean(self): |
1741 | + """Bootstrap from a clean slate makes groups and zookeeper instance""" |
1742 | + self.expect_provider_state_fresh() |
1743 | + self.expect_nova_get("os-security-groups", |
1744 | + response={'security_groups': []}) |
1745 | + self.expect_create_group() |
1746 | + self.expect_create_machine_group(0) |
1747 | + self.expect_launch() |
1748 | + self.mocker.replay() |
1749 | + |
1750 | + log = self.capture_logging("juju.common", level=logging.DEBUG) |
1751 | + deferred = self.bootstrap() |
1752 | + deferred.addCallback(self._check_machine) |
1753 | + def check_log(_): |
1754 | + log_text = log.getvalue() |
1755 | + self.assertIn("Launching juju bootstrap instance", log_text) |
1756 | + self.assertNotIn("previously bootstrapped", log_text) |
1757 | + return deferred.addCallback(check_log) |
1758 | + |
1759 | + def test_bootstrap_existing_group(self): |
1760 | + """Bootstrap reuses an exisiting provider security group""" |
1761 | + self.expect_provider_state_fresh() |
1762 | + self.expect_nova_get("os-security-groups", |
1763 | + response={'security_groups': [ |
1764 | + {'name': "juju-testing", 'id': 1}, |
1765 | + ]}) |
1766 | + self.expect_create_machine_group(0) |
1767 | + self.expect_launch() |
1768 | + self.mocker.replay() |
1769 | + return self.bootstrap().addCallback(self._check_machine) |
1770 | + |
1771 | + def test_bootstrap_existing_machine_group(self): |
1772 | + """Bootstrap deletes and remakes an existing machine security group""" |
1773 | + self.expect_provider_state_fresh() |
1774 | + self.expect_nova_get("os-security-groups", |
1775 | + response={'security_groups': [ |
1776 | + {'name': "juju-testing-0", 'id': 3}, |
1777 | + ]}) |
1778 | + self.expect_create_group() |
1779 | + self.expect_nova_delete("os-security-groups/3") |
1780 | + self.expect_create_machine_group(0) |
1781 | + self.expect_launch() |
1782 | + self.mocker.replay() |
1783 | + return self.bootstrap().addCallback(self._check_machine) |
1784 | + |
1785 | + def test_existing_machine(self): |
1786 | + """A preexisting zookeeper instance is returned if present""" |
1787 | + self.expect_swift_get("testing/provider-state", |
1788 | + response=yaml.dump({'zookeeper-instances': ['1000']})) |
1789 | + self.expect_nova_get("servers/1000", |
1790 | + response={'server': { |
1791 | + 'id': '1000', |
1792 | + 'name': 'juju testing instance 0', |
1793 | + 'state': "RUNNING" |
1794 | + }}) |
1795 | + self.mocker.replay() |
1796 | + |
1797 | + log = self.capture_logging("juju.common") |
1798 | + self.capture_logging("juju.openstack") # Drop to avoid stderr kipple |
1799 | + deferred = self.bootstrap() |
1800 | + deferred.addCallback(self._check_machine) |
1801 | + def check_log(_): |
1802 | + self.assertEqual("juju environment previously bootstrapped.\n", |
1803 | + log.getvalue()) |
1804 | + return deferred.addCallback(check_log) |
1805 | + |
1806 | + def test_existing_machine_missing(self): |
1807 | + """Bootstrap overwrites existing zookeeper if instance is present""" |
1808 | + self.expect_swift_get("testing/provider-state", |
1809 | + response=yaml.dump({'zookeeper-instances': [3000]})) |
1810 | + self.expect_nova_get("servers/3000", code=404, |
1811 | + response={'itemNotFound': |
1812 | + {'message': "The resource could not be found.", 'code': 404} |
1813 | + }) |
1814 | + self.expect_verify() |
1815 | + self.expect_nova_get("os-security-groups", |
1816 | + response={'security_groups': [ |
1817 | + {'name': "juju-testing", 'id': 1}, |
1818 | + {'name': "juju-testing-0", 'id': 3}, |
1819 | + ]}) |
1820 | + self.expect_nova_delete("os-security-groups/3") |
1821 | + self.expect_create_machine_group(0) |
1822 | + self.expect_launch() |
1823 | + self.mocker.replay() |
1824 | + |
1825 | + log = self.capture_logging("juju.common", level=logging.DEBUG) |
1826 | + self.capture_logging("juju.openstack") # Drop to avoid stderr kipple |
1827 | + deferred = self.bootstrap() |
1828 | + deferred.addCallback(self._check_machine) |
1829 | + def check_log(_): |
1830 | + log_text = log.getvalue() |
1831 | + self.assertIn("Launching juju bootstrap instance", log_text) |
1832 | + self.assertNotIn("previously bootstrapped", log_text) |
1833 | + return deferred.addCallback(check_log) |
1834 | |
1835 | === added file 'juju/providers/openstack/tests/test_client.py' |
1836 | --- juju/providers/openstack/tests/test_client.py 1970-01-01 00:00:00 +0000 |
1837 | +++ juju/providers/openstack/tests/test_client.py 2012-07-18 19:47:20 +0000 |
1838 | @@ -0,0 +1,75 @@ |
1839 | +"""Tests for OpenStack API twisted client""" |
1840 | + |
1841 | +from juju.lib import testing |
1842 | + |
1843 | +from juju.providers.openstack import client |
1844 | + |
1845 | + |
1846 | +class StubClient(client._OpenStackClient): |
1847 | + |
1848 | + def __init__(self): |
1849 | + self.url = "http://testing.invalid" |
1850 | + |
1851 | + make_url = client._OpenStackClient._make_url |
1852 | + |
1853 | + |
1854 | +class TestMakeUrl(testing.TestCase): |
1855 | + |
1856 | + def setUp(self): |
1857 | + self.client = StubClient() |
1858 | + self.client.services = { |
1859 | + "compute": self.client.url + "/nova", |
1860 | + "object-store": self.client.url + "/swift", |
1861 | + } |
1862 | + |
1863 | + def test_list_str(self): |
1864 | + self.assertEqual("http://testing.invalid/nova/servers", |
1865 | + self.client.make_url("compute", ["servers"])) |
1866 | + self.assertEqual("http://testing.invalid/swift/container/object", |
1867 | + self.client.make_url("object-store", ["container", "object"])) |
1868 | + |
1869 | + def test_list_int(self): |
1870 | + self.assertEqual("http://testing.invalid/nova/servers/1000", |
1871 | + self.client.make_url("compute", ["servers", 1000])) |
1872 | + self.assertEqual("http://testing.invalid/nova/servers/1000/detail", |
1873 | + self.client.make_url("compute", ["servers", 1000, "detail"])) |
1874 | + |
1875 | + def test_list_unicode(self): |
1876 | + url = self.client.make_url("object-store", ["container", u"\xa7"]) |
1877 | + self.assertIsInstance(url, str) |
1878 | + self.assertEqual("http://testing.invalid/swift/container/%C2%A7", url) |
1879 | + |
1880 | + def test_str(self): |
1881 | + self.assertEqual("http://testing.invalid/nova/servers", |
1882 | + self.client.make_url("compute", "servers")) |
1883 | + self.assertEqual("http://testing.invalid/swift/container/object", |
1884 | + self.client.make_url("object-store", "container/object")) |
1885 | + |
1886 | + def test_trailing_slash(self): |
1887 | + self.client.services["object-store"] += "/" |
1888 | + self.assertEqual("http://testing.invalid/nova/container", |
1889 | + self.client.make_url("compute", "container")) |
1890 | + self.assertEqual("http://testing.invalid/nova/container/object", |
1891 | + self.client.make_url("compute", ["container", "object"])) |
1892 | + |
1893 | + |
1894 | +class TestPlan(testing.TestCase): |
1895 | + """Ideas for tests needed""" |
1896 | + |
1897 | + # auth request without auth |
1898 | + # get bytes, content-length 0, return "" |
1899 | + # get bytes, not ResponseDone, raise wrapped in ProviderError (with any bytes?) |
1900 | + # get bytes, type json, return bytes |
1901 | + # get json, content length 0, raise ProviderError |
1902 | + # get json, bad header (several forms), raise ProviderError (with bytes) |
1903 | + # get json, not ResponseDone, raise wrapped in ProviderError (with any bytes?) |
1904 | + # get json, undecodable, raise wrapped in ProviderError with bytes |
1905 | + # get json, mismatching root, raise ProviderError with bytes or json? |
1906 | + # wrong code, no json header, raise ProviderError with bytes |
1907 | + # wrong code, not ResponseDone, raise ProviderError from code with any bytes |
1908 | + # wrong code, undecodable, raise ProviderError from code with bytes |
1909 | + # wrong code, has mystery root, raise ProviderError from code with bytes or json? |
1910 | + # wrong code, has good root, no message |
1911 | + # wrong code, has good root, no code |
1912 | + # wrong code, has good root, differing code |
1913 | + # wrong code, has good root, message, and matching code |
1914 | |
1915 | === added file 'juju/providers/openstack/tests/test_credentials.py' |
1916 | --- juju/providers/openstack/tests/test_credentials.py 1970-01-01 00:00:00 +0000 |
1917 | +++ juju/providers/openstack/tests/test_credentials.py 2012-07-18 19:47:20 +0000 |
1918 | @@ -0,0 +1,216 @@ |
1919 | +"""Tests for handling of OpenStack credentials in config and environment""" |
1920 | + |
1921 | +from juju.lib import ( |
1922 | + testing, |
1923 | + ) |
1924 | +from juju.providers.openstack import ( |
1925 | + credentials, |
1926 | + ) |
1927 | + |
1928 | + |
1929 | +class OpenStackCredentialsTests(testing.TestCase): |
1930 | + |
1931 | + def test_required_url(self): |
1932 | + e = self.assertRaises(ValueError, credentials.OpenStackCredentials, {}) |
1933 | + self.assertIn("Missing config 'auth-url'", str(e)) |
1934 | + |
1935 | + def test_required_mode_if_unguessable(self): |
1936 | + e = self.assertRaises(ValueError, credentials.OpenStackCredentials, { |
1937 | + 'auth-url': "http://example.com", |
1938 | + }) |
1939 | + self.assertIn("Missing config 'auth-mode'", str(e)) |
1940 | + |
1941 | + def test_legacy(self): |
1942 | + creds = credentials.OpenStackCredentials({ |
1943 | + 'auth-url': "http://example.com", |
1944 | + 'auth-mode': "legacy", |
1945 | + 'username': "luser", |
1946 | + 'access-key': "laccess", |
1947 | + }) |
1948 | + self.assertEqual(creds.url, "http://example.com") |
1949 | + self.assertEqual(creds.mode, "legacy") |
1950 | + self.assertEqual(creds.username, "luser") |
1951 | + self.assertEqual(creds.access_key, "laccess") |
1952 | + |
1953 | + def test_legacy_required_username(self): |
1954 | + e = self.assertRaises(ValueError, credentials.OpenStackCredentials, { |
1955 | + 'auth-url': "http://example.com", |
1956 | + 'auth-mode': "legacy", |
1957 | + 'access-key': "laccess", |
1958 | + }) |
1959 | + self.assertIn("Missing config 'username'", str(e)) |
1960 | + |
1961 | + def test_legacy_required_access_key(self): |
1962 | + e = self.assertRaises(ValueError, credentials.OpenStackCredentials, { |
1963 | + 'auth-url': "http://example.com", |
1964 | + 'auth-mode': "legacy", |
1965 | + 'username': "luser", |
1966 | + }) |
1967 | + self.assertIn("Missing config 'access-key'", str(e)) |
1968 | + |
1969 | + # v1.0 auth is gone from the upstream codebase so maybe remove support |
1970 | + def test_legacy_guess_v1_0(self): |
1971 | + creds = credentials.OpenStackCredentials({ |
1972 | + 'auth-url': "http://example.com/v1.0/", |
1973 | + 'username': "luser", |
1974 | + 'access-key': "laccess", |
1975 | + }) |
1976 | + self.assertEqual(creds.mode, "legacy") |
1977 | + |
1978 | + def test_legacy_guess_v1_1(self): |
1979 | + creds = credentials.OpenStackCredentials({ |
1980 | + 'auth-url': "http://example.com/v1.1/", |
1981 | + 'username': "luser", |
1982 | + 'access-key': "laccess", |
1983 | + }) |
1984 | + self.assertEqual(creds.mode, "legacy") |
1985 | + |
1986 | + def test_userpass(self): |
1987 | + creds = credentials.OpenStackCredentials({ |
1988 | + 'auth-url': "http://example.com", |
1989 | + 'auth-mode': "userpass", |
1990 | + 'username': "uuser", |
1991 | + 'password': "upass", |
1992 | + 'project-name': "uproject", |
1993 | + }) |
1994 | + self.assertEqual(creds.url, "http://example.com") |
1995 | + self.assertEqual(creds.mode, "userpass") |
1996 | + self.assertEqual(creds.username, "uuser") |
1997 | + self.assertEqual(creds.password, "upass") |
1998 | + self.assertEqual(creds.project_name, "uproject") |
1999 | + |
2000 | + def test_userpass_guess_v2_0_no_slash(self): |
2001 | + creds = credentials.OpenStackCredentials({ |
2002 | + 'auth-url': "http://example.com/v2.0", |
2003 | + 'username': "uuser", |
2004 | + 'password': "upass", |
2005 | + 'project-name': "uproject", |
2006 | + }) |
2007 | + self.assertEqual(creds.mode, "userpass") |
2008 | + |
2009 | + def test_userpass_guess_v2_0_slash(self): |
2010 | + creds = credentials.OpenStackCredentials({ |
2011 | + 'auth-url': "http://example.com/v2.0/", |
2012 | + 'username': "uuser", |
2013 | + 'password': "upass", |
2014 | + 'project-name': "uproject", |
2015 | + }) |
2016 | + self.assertEqual(creds.mode, "userpass") |
2017 | + |
2018 | + def test_keypair(self): |
2019 | + creds = credentials.OpenStackCredentials({ |
2020 | + 'auth-url': "http://example.com", |
2021 | + 'auth-mode': "keypair", |
2022 | + 'access-key': "kaccess", |
2023 | + 'secret-key': "ksecret", |
2024 | + 'project-name': "kproject", |
2025 | + }) |
2026 | + self.assertEqual(creds.url, "http://example.com") |
2027 | + self.assertEqual(creds.mode, "keypair") |
2028 | + self.assertEqual(creds.access_key, "kaccess") |
2029 | + self.assertEqual(creds.secret_key, "ksecret") |
2030 | + self.assertEqual(creds.project_name, "kproject") |
2031 | + |
2032 | + |
2033 | +class FromEnvironmentTests(testing.TestCase): |
2034 | + |
2035 | + def test_required_url(self): |
2036 | + self.change_environment() |
2037 | + e = self.assertRaises(ValueError, |
2038 | + credentials.OpenStackCredentials.from_environment, {}) |
2039 | + self.assertIn("Missing config 'auth-url'", str(e)) |
2040 | + |
2041 | + def test_required_mode_if_unguessable(self): |
2042 | + self.change_environment(**{"NOVA_URL": "http://example.com"}) |
2043 | + e = self.assertRaises(ValueError, |
2044 | + credentials.OpenStackCredentials.from_environment, {}) |
2045 | + self.assertIn("Missing config 'auth-mode'", str(e)) |
2046 | + |
2047 | + def test_legacy(self): |
2048 | + self.change_environment(**{ |
2049 | + "NOVA_URL": "http://example.com/v1.1/", |
2050 | + "NOVA_USERNAME": "euser", |
2051 | + "NOVA_API_KEY": "ekey", |
2052 | + }) |
2053 | + creds = credentials.OpenStackCredentials.from_environment({}) |
2054 | + self.assertEqual(creds.mode, "legacy") |
2055 | + self.assertEqual(creds.username, "euser") |
2056 | + self.assertEqual(creds.access_key, "ekey") |
2057 | + |
2058 | + def test_keypair(self): |
2059 | + self.change_environment(**{ |
2060 | + "NOVA_URL": "http://example.com/v2.0/", |
2061 | + "NOVA_API_KEY": "eaccess", |
2062 | + "NOVA_PROJECT_NAME": "eproject", |
2063 | + "NOVA_PROJECT_ID": "349212", |
2064 | + "NOVA_REGION_NAME": "eregion", |
2065 | + "EC2_SECRET_KEY": "esecret", |
2066 | + }) |
2067 | + creds = credentials.OpenStackCredentials.from_environment({ |
2068 | + 'auth-mode': "keypair", |
2069 | + }) |
2070 | + self.assertEqual(creds.access_key, "eaccess") |
2071 | + self.assertEqual(creds.secret_key, "esecret") |
2072 | + self.assertEqual(creds.project_name, "eproject") |
2073 | + self.assertEqual(creds.region, "eregion") |
2074 | + |
2075 | + def test_userpass(self): |
2076 | + self.change_environment(**{ |
2077 | + "OS_AUTH_URL": "http://example.com/v2.0/", |
2078 | + "OS_USERNAME": "euser", |
2079 | + "OS_PASSWORD": "epass", |
2080 | + "OS_TENANT_NAME": "eproject", |
2081 | + "OS_REGION_NAME": "eregion", |
2082 | + }) |
2083 | + creds = credentials.OpenStackCredentials.from_environment({}) |
2084 | + self.assertEqual(creds.mode, "userpass") |
2085 | + self.assertEqual(creds.username, "euser") |
2086 | + self.assertEqual(creds.password, "epass") |
2087 | + self.assertEqual(creds.project_name, "eproject") |
2088 | + self.assertEqual(creds.region, "eregion") |
2089 | + |
2090 | + def test_prefer_os_auth_url(self): |
2091 | + self.change_environment(**{ |
2092 | + "NOVA_URL": "http://example.com/v1.1/", |
2093 | + "NOVA_API_KEY": "eaccess", |
2094 | + "OS_AUTH_URL": "http://example.com/v2.0/", |
2095 | + "OS_USERNAME": "euser", |
2096 | + "OS_PASSWORD": "epass", |
2097 | + "OS_TENANT_NAME": "eproject", |
2098 | + }) |
2099 | + creds = credentials.OpenStackCredentials.from_environment({}) |
2100 | + self.assertEqual(creds.url, "http://example.com/v2.0/") |
2101 | + self.assertEqual(creds.mode, "userpass") |
2102 | + |
2103 | + |
2104 | +class SetConfigDefaultsTests(testing.TestCase): |
2105 | + |
2106 | + def test_set_all(self): |
2107 | + config = { |
2108 | + 'auth-url': "http://example.com", |
2109 | + 'auth-mode': "legacy", |
2110 | + 'username': "luser", |
2111 | + 'access-key': "laccess", |
2112 | + } |
2113 | + creds = credentials.OpenStackCredentials(config) |
2114 | + new_config = {} |
2115 | + creds.set_config_defaults(new_config) |
2116 | + self.assertEqual(config, new_config) |
2117 | + |
2118 | + def test_set_only_missing(self): |
2119 | + config = { |
2120 | + 'auth-url': "http://example.com/v1.1/", |
2121 | + 'username': "luser", |
2122 | + 'access-key': "laccess", |
2123 | + } |
2124 | + creds = credentials.OpenStackCredentials(config) |
2125 | + new_config = { |
2126 | + 'username': "nuser", |
2127 | + } |
2128 | + creds.set_config_defaults(new_config) |
2129 | + self.assertEqual({ |
2130 | + 'auth-url': "http://example.com/v1.1/", |
2131 | + 'auth-mode': "legacy", |
2132 | + 'username': "nuser", |
2133 | + 'access-key': "laccess", |
2134 | + }, new_config) |
2135 | |
2136 | === added file 'juju/providers/openstack/tests/test_files.py' |
2137 | --- juju/providers/openstack/tests/test_files.py 1970-01-01 00:00:00 +0000 |
2138 | +++ juju/providers/openstack/tests/test_files.py 2012-07-18 19:47:20 +0000 |
2139 | @@ -0,0 +1,103 @@ |
2140 | +"""Tests for file storage backend based on Swift""" |
2141 | + |
2142 | +from cStringIO import StringIO |
2143 | + |
2144 | +from twisted.internet import ( |
2145 | + defer, |
2146 | + ) |
2147 | + |
2148 | +from juju import errors |
2149 | +from juju.lib import ( |
2150 | + testing, |
2151 | + ) |
2152 | +from juju.providers.openstack.tests import OpenStackTestMixin |
2153 | + |
2154 | + |
2155 | +class FileStorageTestCase(OpenStackTestMixin, testing.TestCase): |
2156 | + |
2157 | + def get_storage(self): |
2158 | + provider = self.get_provider() |
2159 | + storage = provider.get_file_storage() |
2160 | + return storage |
2161 | + |
2162 | + def test_put_file(self): |
2163 | + """A file can be put in the storage""" |
2164 | + content = "some text" |
2165 | + self.expect_swift_put("testing/object", content) |
2166 | + self.mocker.replay() |
2167 | + return self.get_storage().put("object", StringIO(content)) |
2168 | + |
2169 | + def test_put_file_unicode(self): |
2170 | + """A file with a unicode name is put in UTF-8 url encoded form""" |
2171 | + content = "some text" |
2172 | + self.expect_swift_put("testing/%C2%A7", content) |
2173 | + self.mocker.replay() |
2174 | + return self.get_storage().put(u"\xa7", StringIO(content)) |
2175 | + |
2176 | + def test_put_file_create_container(self): |
2177 | + """The container will be created if it doesn't exist yet""" |
2178 | + content = "some text" |
2179 | + self.expect_swift_put("testing/object", content, code=404) |
2180 | + self.expect_swift_put_container("testing") |
2181 | + self.expect_swift_put("testing/object", content) |
2182 | + self.mocker.replay() |
2183 | + return self.get_storage().put("object", StringIO(content)) |
2184 | + |
2185 | + def test_put_file_unknown_error(self): |
2186 | + """Unexpected errors from client propogate""" |
2187 | + content = "some text" |
2188 | + self._mock_swift("PUT", "testing/object", content) |
2189 | + self.mocker.result(defer.fail(ValueError("Something unexpected"))) |
2190 | + self.mocker.replay() |
2191 | + deferred = self.get_storage().put("object", StringIO(content)) |
2192 | + return self.assertFailure(deferred, ValueError) |
2193 | + |
2194 | + @defer.inlineCallbacks |
2195 | + def test_get_url(self): |
2196 | + """A url can be generated for any stored file.""" |
2197 | + self.mocker.replay() |
2198 | + storage = self.get_storage() |
2199 | + yield storage._swift._client.authenticate() |
2200 | + url = storage.get_url("object") |
2201 | + self.assertEqual(self.swift_url + "/testing/object", url) |
2202 | + |
2203 | + @defer.inlineCallbacks |
2204 | + def test_get_url_unicode(self): |
2205 | + """A url can be generated for *any* stored file.""" |
2206 | + self.mocker.replay() |
2207 | + storage = self.get_storage() |
2208 | + yield storage._swift._client.authenticate() |
2209 | + url = storage.get_url(u"\xa7") |
2210 | + self.assertEqual(self.swift_url + "/testing/%C2%A7", url) |
2211 | + |
2212 | + @defer.inlineCallbacks |
2213 | + def test_get_file(self): |
2214 | + """Retrieving a file returns a file-like object with the content""" |
2215 | + content = "some text" |
2216 | + self.expect_swift_get("testing/object", response=content) |
2217 | + self.mocker.replay() |
2218 | + result = yield self.get_storage().get("object") |
2219 | + self.assertEqual(result.read(), content) |
2220 | + |
2221 | + @defer.inlineCallbacks |
2222 | + def test_get_file_unicode(self): |
2223 | + """Retrieving a file with a unicode key uses a UTF-8 url""" |
2224 | + content = "some text" |
2225 | + self.expect_swift_get(u"testing/%C2%A7", response=content) |
2226 | + self.mocker.replay() |
2227 | + result = yield self.get_storage().get(u"\xa7") |
2228 | + self.assertEqual(result.read(), content) |
2229 | + |
2230 | + def test_get_file_nonexistant(self): |
2231 | + """Retrieving a nonexistant file raises a file not found error.""" |
2232 | + self.expect_swift_get(u"testing/missing", code=404) |
2233 | + self.mocker.replay() |
2234 | + deferred = self.get_storage().get("missing") |
2235 | + return self.assertFailure(deferred, errors.FileNotFound) |
2236 | + |
2237 | + def test_get_file_error(self): |
2238 | + """An error from the client results is attributed to the provider""" |
2239 | + self.expect_swift_get(u"testing/unavailable", code=500) |
2240 | + self.mocker.replay() |
2241 | + deferred = self.get_storage().get("unavailable") |
2242 | + return self.assertFailure(deferred, errors.ProviderInteractionError) |
2243 | |
2244 | === added file 'juju/providers/openstack/tests/test_getmachines.py' |
2245 | --- juju/providers/openstack/tests/test_getmachines.py 1970-01-01 00:00:00 +0000 |
2246 | +++ juju/providers/openstack/tests/test_getmachines.py 2012-07-18 19:47:20 +0000 |
2247 | @@ -0,0 +1,115 @@ |
2248 | +"""Tests for OpenStack provider method for listing live juju machines""" |
2249 | + |
2250 | +from juju.lib import testing |
2251 | +from juju.providers.openstack.machine import NovaProviderMachine |
2252 | +from juju.providers.openstack.tests import OpenStackTestMixin |
2253 | + |
2254 | + |
2255 | +class GetMachinesTests(OpenStackTestMixin, testing.TestCase): |
2256 | + |
2257 | + def check_machines(self, machines, expected): |
2258 | + machine_details = [] |
2259 | + for m in machines: |
2260 | + self.assertTrue(isinstance(m, NovaProviderMachine)) |
2261 | + machine_details.append((m.instance_id, m.state)) |
2262 | + machine_details.sort() |
2263 | + expected.sort() |
2264 | + self.assertEqual(expected, machine_details) |
2265 | + |
2266 | + def test_all_none(self): |
2267 | + self.expect_nova_get("servers", response={"servers": []}) |
2268 | + self.mocker.replay() |
2269 | + return self.get_provider().get_machines().addCallback( |
2270 | + self.check_machines, []) |
2271 | + |
2272 | + def test_all_single(self): |
2273 | + self.expect_nova_get("servers", response={"servers": [ |
2274 | + self.make_server(1000), |
2275 | + ]}) |
2276 | + self.mocker.replay() |
2277 | + return self.get_provider().get_machines().addCallback( |
2278 | + self.check_machines, [(1000, 'running')]) |
2279 | + |
2280 | + def test_all_multiple(self): |
2281 | + self.expect_nova_get("servers", response={"servers": [ |
2282 | + self.make_server(1001), |
2283 | + self.make_server(1002, status="BUILDING"), |
2284 | + ]}) |
2285 | + self.mocker.replay() |
2286 | + return self.get_provider().get_machines().addCallback( |
2287 | + self.check_machines, [(1001, 'running'), (1002, 'pending')]) |
2288 | + |
2289 | + def test_all_some_dead(self): |
2290 | + self.expect_nova_get("servers", response={"servers": [ |
2291 | + self.make_server(1001, status="BUILDING"), |
2292 | + self.make_server(1002, status="DELETED"), |
2293 | + ]}) |
2294 | + self.mocker.replay() |
2295 | + return self.get_provider().get_machines().addCallback( |
2296 | + self.check_machines, [(1001, 'pending')]) |
2297 | + |
2298 | + def test_all_some_other(self): |
2299 | + self.expect_nova_get("servers", response={"servers": [ |
2300 | + self.make_server(1001, name="nova started server"), |
2301 | + self.make_server(1002), |
2302 | + ]}) |
2303 | + self.mocker.replay() |
2304 | + return self.get_provider().get_machines().addCallback( |
2305 | + self.check_machines, [(1002, 'running')]) |
2306 | + |
2307 | + def test_two_none(self): |
2308 | + self.expect_nova_get("servers", response={"servers": []}) |
2309 | + self.mocker.replay() |
2310 | + deferred = self.get_provider().get_machines([1001, 1002]) |
2311 | + return self.assert_not_found(deferred, [1001, 1002]) |
2312 | + |
2313 | + def test_two_some_dead(self): |
2314 | + self.expect_nova_get("servers", response={"servers": [ |
2315 | + self.make_server(1001, status="BUILDING"), |
2316 | + self.make_server(1002, status="DELETED"), |
2317 | + ]}) |
2318 | + self.mocker.replay() |
2319 | + deferred = self.get_provider().get_machines([1001, 1002]) |
2320 | + return self.assert_not_found(deferred, [1002]) |
2321 | + |
2322 | + def test_two_some_other(self): |
2323 | + self.expect_nova_get("servers", response={"servers": [ |
2324 | + self.make_server(1001, name="nova started server"), |
2325 | + self.make_server(1002), |
2326 | + ]}) |
2327 | + self.mocker.replay() |
2328 | + deferred = self.get_provider().get_machines([1001, 1002]) |
2329 | + return self.assert_not_found(deferred, [1001]) |
2330 | + |
2331 | + def test_one_running(self): |
2332 | + self.expect_nova_get("servers/1000", |
2333 | + response={"server": self.make_server(1000)}) |
2334 | + self.mocker.replay() |
2335 | + deferred = self.get_provider().get_machines([1000]) |
2336 | + return deferred.addCallback(self.check_machines, [(1000, 'running')]) |
2337 | + |
2338 | + def test_one_dead(self): |
2339 | + self.expect_nova_get("servers/1000", |
2340 | + response={"server": self.make_server(1000, status="DELETED")}) |
2341 | + self.mocker.replay() |
2342 | + deferred = self.get_provider().get_machines([1000]) |
2343 | + return self.assert_not_found(deferred, [1000]) |
2344 | + |
2345 | + def test_one_other(self): |
2346 | + self.expect_nova_get("servers/1000", |
2347 | + response={"server": self.make_server(1000, name="testing")}) |
2348 | + self.mocker.replay() |
2349 | + deferred = self.get_provider().get_machines([1000]) |
2350 | + return self.assert_not_found(deferred, [1000]) |
2351 | + |
2352 | + def test_one_missing(self): |
2353 | + self.expect_nova_get("servers/1000", |
2354 | + code=404, response={"itemNotFound": |
2355 | + {"message": "The resource could not be found.", "code": 404}}) |
2356 | + self.mocker.replay() |
2357 | + deferred = self.get_provider().get_machines([1000]) |
2358 | + return self.assert_not_found(deferred, [1000]) |
2359 | + # XXX: Need to do error handling properly in client still |
2360 | + test_one_missing.skip = True |
2361 | + |
2362 | + # XXX: EC2 also has some tests for wrapping unexpected errors from backend |
2363 | |
2364 | === added file 'juju/providers/openstack/tests/test_launch.py' |
2365 | --- juju/providers/openstack/tests/test_launch.py 1970-01-01 00:00:00 +0000 |
2366 | +++ juju/providers/openstack/tests/test_launch.py 2012-07-18 19:47:20 +0000 |
2367 | @@ -0,0 +1,113 @@ |
2368 | +"""Tests for launching a new server customised for juju using Nova""" |
2369 | + |
2370 | +import logging |
2371 | + |
2372 | +from twisted.internet import ( |
2373 | + defer, |
2374 | + ) |
2375 | + |
2376 | +from juju import errors |
2377 | +from juju.lib import ( |
2378 | + mocker, |
2379 | + testing, |
2380 | + ) |
2381 | +from juju.machine import ( |
2382 | + constraints, |
2383 | + ) |
2384 | + |
2385 | +from juju.providers.openstack import ( |
2386 | + launch, |
2387 | + tests, |
2388 | + ) |
2389 | + |
2390 | +class MockedLaunchProvider(tests.MockedProvider): |
2391 | + |
2392 | + def launch(self, machine_id, master=False): |
2393 | + constraint_set = constraints.ConstraintSet(self.provider_type) |
2394 | + details = { |
2395 | + 'machine-id': machine_id, |
2396 | + 'constraints': constraint_set.load({}), |
2397 | + } |
2398 | + return launch.NovaLaunchMachine.launch(self, details, master) |
2399 | + |
2400 | + def expect_launch_setup(self, machine_id): |
2401 | + self.port_manager.ensure_groups(machine_id) |
2402 | + self.mocker.result(defer.succeed(["juju-x", "juju-y"])) |
2403 | + self.nova.list_flavors() |
2404 | + self.mocker.result(defer.succeed([ |
2405 | + {'id': 1, 'name': "standard.xsmall"}, |
2406 | + ])) |
2407 | + |
2408 | + def expect_run_server(self, machine_id, response): |
2409 | + self.nova.run_server( |
2410 | + name="juju testing instance " + machine_id, |
2411 | + image_id=42, |
2412 | + flavor_id=1, |
2413 | + security_group_names=["juju-x", "juju-y"], |
2414 | + user_data=mocker.ANY, |
2415 | + ) |
2416 | + self.mocker.result(defer.succeed(response)) |
2417 | + |
2418 | + def expect_available_floating_ip(self, server_id): |
2419 | + self.nova.list_floating_ips() |
2420 | + self.mocker.result(defer.succeed([ |
2421 | + {'instance_id': None, 'ip': "198.162.1.0"}, |
2422 | + ])) |
2423 | + self.nova.add_floating_ip(server_id, "198.162.1.0") |
2424 | + self.mocker.result(defer.succeed(None)) |
2425 | + |
2426 | + |
2427 | +class NovaLaunchMachineTests(testing.TestCase): |
2428 | + |
2429 | + def test_launch_requires_default_image_id(self): |
2430 | + config = dict(MockedLaunchProvider.default_config) |
2431 | + del config['default-image-id'] |
2432 | + provider = MockedLaunchProvider(self.mocker, config) |
2433 | + provider.expect_zookeeper_machines(1000) |
2434 | + self.mocker.replay() |
2435 | + deferred = provider.launch("1") |
2436 | + return self.assertFailure(deferred, errors.ProviderError) |
2437 | + |
2438 | + def test_start_machine(self): |
2439 | + provider = MockedLaunchProvider(self.mocker) |
2440 | + provider.expect_zookeeper_machines(1000) |
2441 | + provider.expect_launch_setup("1") |
2442 | + provider.expect_run_server("1", response={ |
2443 | + 'id': 1001, |
2444 | + 'addresses': {'public': []}, |
2445 | + }) |
2446 | + self.mocker.replay() |
2447 | + return provider.launch("1") |
2448 | + |
2449 | + def test_start_machine_delay(self): |
2450 | + provider = MockedLaunchProvider(self.mocker) |
2451 | + provider.config["use-floating-ip"] = True |
2452 | + provider.expect_zookeeper_machines(1000) |
2453 | + provider.expect_launch_setup("1") |
2454 | + provider.expect_run_server("1", response={ |
2455 | + 'id': 1001, |
2456 | + }) |
2457 | + provider.nova.get_server(1001) |
2458 | + self.mocker.result(defer.succeed({ |
2459 | + 'id': 1001, |
2460 | + 'addresses': {'public': []}, |
2461 | + })) |
2462 | + provider.expect_available_floating_ip(1001) |
2463 | + self.mocker.result(defer.succeed(None)) |
2464 | + self.mocker.replay() |
2465 | + self.patch(launch.NovaLaunchMachine, "_DELAY_FOR_ADDRESSES", 0) |
2466 | + return provider.launch("1") |
2467 | + |
2468 | + def test_start_machine_master(self): |
2469 | + provider = MockedLaunchProvider(self.mocker) |
2470 | + provider.expect_swift_public_object_url("juju_master_id") |
2471 | + provider.expect_launch_setup("0") |
2472 | + provider.expect_run_server("0", response={ |
2473 | + 'id': 1000, |
2474 | + 'addresses': {'public': []}, |
2475 | + }) |
2476 | + provider.expect_swift_put("juju_master_id", "1000") |
2477 | + provider.provider_actions.save_state({'zookeeper-instances': [1000]}) |
2478 | + self.mocker.result(defer.succeed(None)) |
2479 | + self.mocker.replay() |
2480 | + return provider.launch("0", master=True) |
2481 | |
2482 | === added file 'juju/providers/openstack/tests/test_machine.py' |
2483 | --- juju/providers/openstack/tests/test_machine.py 1970-01-01 00:00:00 +0000 |
2484 | +++ juju/providers/openstack/tests/test_machine.py 2012-07-18 19:47:20 +0000 |
2485 | @@ -0,0 +1,110 @@ |
2486 | +"""Tests for server wrapper and helper functions""" |
2487 | + |
2488 | +from juju.providers.openstack.machine import ( |
2489 | + get_server_addresses, |
2490 | + get_server_status, |
2491 | + ) |
2492 | + |
2493 | +from juju.lib.testing import TestCase |
2494 | + |
2495 | + |
2496 | +class GetServerStatusTest(TestCase): |
2497 | + """Tests for mapping of Nova status names to EC2 style names""" |
2498 | + |
2499 | + def test_build_schedualing(self): |
2500 | + self.assertEqual("pending", |
2501 | + get_server_status({u'status': u'BUILD(scheduling)'})) |
2502 | + |
2503 | + def test_build_spawning(self): |
2504 | + self.assertEqual("pending", |
2505 | + get_server_status({u'status': u'BUILD(spawning)'})) |
2506 | + |
2507 | + def test_active(self): |
2508 | + self.assertEqual("running", |
2509 | + get_server_status({u'status': u'ACTIVE'})) |
2510 | + |
2511 | + def test_no_status(self): |
2512 | + self.assertEqual("pending", |
2513 | + get_server_status({})) |
2514 | + |
2515 | + def test_mystery_status(self): |
2516 | + self.assertEqual("unknown", |
2517 | + get_server_status({u'status': u'NEVER_BEFORE_SEEN_MYSTERY'})) |
2518 | + |
2519 | + |
2520 | +class GetServerAddressesTest(TestCase): |
2521 | + """Tests for deriving a public and private address from Nova dict""" |
2522 | + |
2523 | + def test_missing(self): |
2524 | + self.assertEqual((None, None), get_server_addresses({})) |
2525 | + |
2526 | + def test_empty(self): |
2527 | + self.assertEqual((None, None), |
2528 | + get_server_addresses({u'addresses': {}})) |
2529 | + |
2530 | + def test_private_only(self): |
2531 | + self.assertEqual(("127.0.0.4", None), |
2532 | + get_server_addresses({u'addresses': { |
2533 | + "private": [{"addr": "127.0.0.4", "version": 4}], |
2534 | + }})) |
2535 | + |
2536 | + def test_private_plus(self): |
2537 | + self.assertEqual(("127.0.0.4", "8.8.4.4"), |
2538 | + get_server_addresses({u'addresses': { |
2539 | + "private": [ |
2540 | + {"addr": "127.0.0.4", "version": 4}, |
2541 | + {"addr": "8.8.4.4", "version": 4}, |
2542 | + ], |
2543 | + }})) |
2544 | + |
2545 | + def test_public_only(self): |
2546 | + self.assertEqual((None, "8.8.8.8"), |
2547 | + get_server_addresses({u'addresses': { |
2548 | + "public": [{"addr": "8.8.8.8", "version": 4}], |
2549 | + }})) |
2550 | + |
2551 | + def test_public_and_private(self): |
2552 | + self.assertEqual(("127.0.0.4", "8.8.8.8"), |
2553 | + get_server_addresses({u'addresses': { |
2554 | + "public": [{"addr": "8.8.8.8", "version": 4}], |
2555 | + "private": [{"addr": "127.0.0.4", "version": 4}], |
2556 | + }})) |
2557 | + |
2558 | + def test_public_and_private_plus(self): |
2559 | + self.assertEqual(("127.0.0.4", "8.8.8.8"), |
2560 | + get_server_addresses({u'addresses': { |
2561 | + "public": [{"addr": "8.8.8.8", "version": 4}], |
2562 | + "private": [ |
2563 | + {"addr": "127.0.0.4", "version": 4}, |
2564 | + {"addr": "8.8.4.4", "version": 4}, |
2565 | + ], |
2566 | + }})) |
2567 | + |
2568 | + def test_custom_only(self): |
2569 | + self.assertEqual(("127.0.0.2", None), |
2570 | + get_server_addresses({u'addresses': { |
2571 | + "special": [{"addr": "127.0.0.2", "version": 4}], |
2572 | + }})) |
2573 | + |
2574 | + def test_custom_plus(self): |
2575 | + self.assertEqual(("127.0.0.2", "8.8.4.4"), |
2576 | + get_server_addresses({u'addresses': { |
2577 | + "special": [ |
2578 | + {"addr": "127.0.0.2", "version": 4}, |
2579 | + {"addr": "8.8.4.4", "version": 4}, |
2580 | + ], |
2581 | + }})) |
2582 | + |
2583 | + def test_custom_and_private(self): |
2584 | + self.assertEqual(("127.0.0.4", None), |
2585 | + get_server_addresses({u'addresses': { |
2586 | + "special": [{"addr": "127.0.0.2", "version": 4}], |
2587 | + "private": [{"addr": "127.0.0.4", "version": 4}], |
2588 | + }})) |
2589 | + |
2590 | + def test_custom_and_public(self): |
2591 | + self.assertEqual(("127.0.0.2", "8.8.8.8"), |
2592 | + get_server_addresses({u'addresses': { |
2593 | + "special": [{"addr": "127.0.0.2", "version": 4}], |
2594 | + "public": [{"addr": "8.8.8.8", "version": 4}], |
2595 | + }})) |
2596 | |
2597 | === added file 'juju/providers/openstack/tests/test_ports.py' |
2598 | --- juju/providers/openstack/tests/test_ports.py 1970-01-01 00:00:00 +0000 |
2599 | +++ juju/providers/openstack/tests/test_ports.py 2012-07-18 19:47:20 +0000 |
2600 | @@ -0,0 +1,396 @@ |
2601 | +"""Tests for emulating port management with security groups""" |
2602 | + |
2603 | +import logging |
2604 | + |
2605 | +from juju import errors |
2606 | +from juju.lib.testing import TestCase |
2607 | +from juju.providers.openstack.machine import NovaProviderMachine |
2608 | +from juju.providers.openstack.ports import NovaPortManager |
2609 | +from juju.providers.openstack.tests import OpenStackTestMixin |
2610 | + |
2611 | + |
2612 | +class ProviderPortMgmtTests(OpenStackTestMixin, TestCase): |
2613 | + """Tests for provider exposed port management methods""" |
2614 | + |
2615 | + def expect_create_rule(self, group_id, proto, port): |
2616 | + self.expect_nova_post("os-security-group-rules", |
2617 | + {'security_group_rule': { |
2618 | + 'parent_group_id': group_id, |
2619 | + 'ip_protocol': proto, |
2620 | + 'from_port': port, |
2621 | + 'to_port': port, |
2622 | + }}, |
2623 | + response={'security_group_rule': { |
2624 | + 'id': 144, 'parent_group_id': group_id, |
2625 | + }}) |
2626 | + |
2627 | + def expect_existing_rule(self, rule_id, proto, port): |
2628 | + self.expect_nova_get("servers/1000/os-security-groups", |
2629 | + response={'security_groups': [ |
2630 | + {'name': "juju-testing-1", 'id': 1, 'rules': [{ |
2631 | + 'id': rule_id, |
2632 | + 'parent_group_id': 1, |
2633 | + 'ip_protocol': proto, |
2634 | + 'from_port': port, |
2635 | + 'to_port': port, |
2636 | + }] |
2637 | + }, |
2638 | + ]}) |
2639 | + |
2640 | + def test_open_port(self): |
2641 | + """Opening a port adds the rule to the appropriate security group""" |
2642 | + self.expect_nova_get("servers/1000/os-security-groups", |
2643 | + response={'security_groups': [ |
2644 | + {'name': "juju-testing-1", 'id': 1}, |
2645 | + ]}) |
2646 | + self.expect_create_rule(1, "tcp", 80) |
2647 | + self.mocker.replay() |
2648 | + |
2649 | + log = self.capture_logging("juju.openstack", level=logging.DEBUG) |
2650 | + machine = NovaProviderMachine('1000', "server1000.testing.invalid") |
2651 | + deferred = self.get_provider().open_port(machine, "1", 80) |
2652 | + def _check_log(_): |
2653 | + self.assertIn("Opened 80/tcp on machine '1000'", |
2654 | + log.getvalue()) |
2655 | + return deferred.addCallback(_check_log) |
2656 | + |
2657 | + def test_open_port_missing_group(self): |
2658 | + """Missing security group raises an error on deleting port""" |
2659 | + self.expect_nova_get("servers/1000/os-security-groups", |
2660 | + response={'security_groups': []}) |
2661 | + self.mocker.replay() |
2662 | + |
2663 | + machine = NovaProviderMachine('1000', "server1000.testing.invalid") |
2664 | + deferred = self.get_provider().open_port(machine, "1", 80) |
2665 | + return self.assertFailure(deferred, errors.ProviderInteractionError) |
2666 | + |
2667 | + def test_close_port(self): |
2668 | + """Closing a port removes the matching rule from the security group""" |
2669 | + self.expect_existing_rule(12, "tcp", 80) |
2670 | + self.expect_nova_delete("os-security-group-rules/12") |
2671 | + self.mocker.replay() |
2672 | + |
2673 | + log = self.capture_logging("juju.openstack", level=logging.DEBUG) |
2674 | + machine = NovaProviderMachine('1000', "server1000.testing.invalid") |
2675 | + deferred = self.get_provider().close_port(machine, "1", 80) |
2676 | + def _check_log(_): |
2677 | + self.assertIn("Closed 80/tcp on machine '1000'", |
2678 | + log.getvalue()) |
2679 | + return deferred.addCallback(_check_log) |
2680 | + |
2681 | + def test_close_port_missing_group(self): |
2682 | + """Missing security group raises an error on closing port""" |
2683 | + self.expect_nova_get("servers/1000/os-security-groups", |
2684 | + response={'security_groups': []}) |
2685 | + self.mocker.replay() |
2686 | + |
2687 | + machine = NovaProviderMachine('1000', "server1000.testing.invalid") |
2688 | + deferred = self.get_provider().close_port(machine, "1", 80) |
2689 | + return self.assertFailure(deferred, errors.ProviderInteractionError) |
2690 | + |
2691 | + def test_close_port_missing_rule(self): |
2692 | + """Missing security group rule raises an error on closing port""" |
2693 | + self.expect_nova_get("servers/1000/os-security-groups", |
2694 | + response={'security_groups': [{ |
2695 | + 'name': "juju-testing-1", 'id': 1, "rules": [], |
2696 | + }]}) |
2697 | + self.mocker.replay() |
2698 | + |
2699 | + machine = NovaProviderMachine('1000', "server1000.testing.invalid") |
2700 | + deferred = self.get_provider().close_port(machine, "1", 80) |
2701 | + return self.assertFailure(deferred, errors.ProviderInteractionError) |
2702 | + |
2703 | + def test_close_port_mismatching_rule(self): |
2704 | + """Rule with different port raises an error on closing port""" |
2705 | + self.expect_existing_rule(12, "tcp", 8080) |
2706 | + self.mocker.replay() |
2707 | + |
2708 | + machine = NovaProviderMachine('1000', "server1000.testing.invalid") |
2709 | + deferred = self.get_provider().close_port(machine, "1", 80) |
2710 | + return self.assertFailure(deferred, errors.ProviderInteractionError) |
2711 | + |
2712 | + def test_get_opened_ports_none(self): |
2713 | + """No opened ports are listed when there are no rules""" |
2714 | + self.expect_nova_get("servers/1000/os-security-groups", |
2715 | + response={'security_groups': [{ |
2716 | + 'name': "juju-testing-1", 'id': 1, "rules": [], |
2717 | + }]}) |
2718 | + self.mocker.replay() |
2719 | + |
2720 | + machine = NovaProviderMachine('1000', "server1000.testing.invalid") |
2721 | + deferred = self.get_provider().get_opened_ports(machine, "1") |
2722 | + return deferred.addCallback(self.assertEqual, set()) |
2723 | + |
2724 | + def test_get_opened_ports_one(self): |
2725 | + """Opened port is listed when there is a matching rule""" |
2726 | + self.expect_existing_rule(12, "tcp", 80) |
2727 | + self.mocker.replay() |
2728 | + |
2729 | + machine = NovaProviderMachine('1000', "server1000.testing.invalid") |
2730 | + deferred = self.get_provider().get_opened_ports(machine, "1") |
2731 | + return deferred.addCallback(self.assertEqual, set([(80, "tcp")])) |
2732 | + |
2733 | + def test_get_opened_ports_group_ignored(self): |
2734 | + """Opened ports exclude rules delegating to other security groups""" |
2735 | + self.expect_nova_get("servers/1000/os-security-groups", |
2736 | + response={'security_groups': [{ |
2737 | + 'name': "juju-testing-1", 'id': 1, "rules": [{ |
2738 | + 'id': 12, |
2739 | + 'parent_group_id': 1, |
2740 | + 'ip_protocol': None, |
2741 | + 'from_port': None, |
2742 | + 'to_port': None, |
2743 | + 'group': {'name': "juju-testing"}, |
2744 | + }], |
2745 | + }]}) |
2746 | + self.mocker.replay() |
2747 | + |
2748 | + machine = NovaProviderMachine('1000', "server1000.testing.invalid") |
2749 | + deferred = self.get_provider().get_opened_ports(machine, "1") |
2750 | + return deferred.addCallback(self.assertEqual, set()) |
2751 | + |
2752 | + def test_get_opened_ports_multiport_ignored(self): |
2753 | + """Opened ports exclude rules spanning multiple ports""" |
2754 | + self.expect_nova_get("servers/1000/os-security-groups", |
2755 | + response={'security_groups': [{ |
2756 | + 'name': "juju-testing-1", 'id': 1, "rules": [{ |
2757 | + 'id': 12, |
2758 | + 'parent_group_id': 1, |
2759 | + 'ip_protocol': "tcp", |
2760 | + 'from_port': 8080, |
2761 | + 'to_port': 8081, |
2762 | + }], |
2763 | + }]}) |
2764 | + self.mocker.replay() |
2765 | + |
2766 | + machine = NovaProviderMachine('1000', "server1000.testing.invalid") |
2767 | + deferred = self.get_provider().get_opened_ports(machine, "1") |
2768 | + return deferred.addCallback(self.assertEqual, set()) |
2769 | + |
2770 | + |
2771 | +class PortManagerTestMixin(OpenStackTestMixin): |
2772 | + |
2773 | + def get_port_manager(self): |
2774 | + provider = self.get_provider() |
2775 | + return NovaPortManager(provider.nova, provider.environment_name) |
2776 | + |
2777 | + |
2778 | +class EnsureGroupsTests(PortManagerTestMixin, TestCase): |
2779 | + """Tests for ensure_groups method used when launching machines""" |
2780 | + |
2781 | + def expect_create_juju_group(self): |
2782 | + self.expect_nova_post("os-security-groups", |
2783 | + {'security_group': { |
2784 | + 'name': 'juju-testing', |
2785 | + 'description': 'juju group for testing', |
2786 | + }}, |
2787 | + response={'security_group': { |
2788 | + 'id': 1, |
2789 | + }}) |
2790 | + self.expect_nova_post("os-security-group-rules", |
2791 | + {'security_group_rule': { |
2792 | + 'parent_group_id': 1, |
2793 | + 'ip_protocol': "tcp", |
2794 | + 'from_port': 22, |
2795 | + 'to_port': 22, |
2796 | + }}, |
2797 | + response={'security_group_rule': { |
2798 | + 'id': 144, 'parent_group_id': 1, |
2799 | + }}) |
2800 | + self.expect_nova_post("os-security-group-rules", |
2801 | + {'security_group_rule': { |
2802 | + 'parent_group_id': 1, |
2803 | + 'group_id': 1, |
2804 | + 'ip_protocol': "tcp", |
2805 | + 'from_port': 1, |
2806 | + 'to_port': 65535, |
2807 | + }}, |
2808 | + response={'security_group_rule': { |
2809 | + 'id': 145, 'parent_group_id': 1, |
2810 | + }}) |
2811 | + |
2812 | + def expect_create_machine_group(self, machine_id): |
2813 | + machine = str(machine_id) |
2814 | + self.expect_nova_post("os-security-groups", |
2815 | + {'security_group': { |
2816 | + 'name': 'juju-testing-' + machine, |
2817 | + 'description': 'juju group for testing machine ' + machine, |
2818 | + }}, |
2819 | + response={'security_group': { |
2820 | + 'id': 2, |
2821 | + }}) |
2822 | + |
2823 | + def check_group_names(self, result, machine_id): |
2824 | + self.assertEqual(["juju-testing", "juju-testing-" + str(machine_id)], |
2825 | + result) |
2826 | + |
2827 | + def test_none_existing(self): |
2828 | + """When no groups exist juju and machine security groups are created""" |
2829 | + self.expect_nova_get("os-security-groups", |
2830 | + response={'security_groups': []}) |
2831 | + self.expect_create_juju_group() |
2832 | + self.expect_create_machine_group(0) |
2833 | + self.mocker.replay() |
2834 | + deferred = self.get_port_manager().ensure_groups(0) |
2835 | + return deferred.addCallback(self.check_group_names, 0) |
2836 | + |
2837 | + def test_other_existing(self): |
2838 | + """Existing groups in a different environment are not affected""" |
2839 | + self.expect_nova_get("os-security-groups", |
2840 | + response={'security_groups': [ |
2841 | + {'name': "juju-testingish", 'id': 7}, |
2842 | + {'name': "juju-testingish-0", 'id': 8}, |
2843 | + ]}) |
2844 | + self.expect_create_juju_group() |
2845 | + self.expect_create_machine_group(0) |
2846 | + self.mocker.replay() |
2847 | + deferred = self.get_port_manager().ensure_groups(0) |
2848 | + return deferred.addCallback(self.check_group_names, 0) |
2849 | + |
2850 | + def test_existing_juju_group(self): |
2851 | + """An exisiting juju security group is reused""" |
2852 | + self.expect_nova_get("os-security-groups", |
2853 | + response={'security_groups': [ |
2854 | + {'name': "juju-testing", 'id': 1}, |
2855 | + ]}) |
2856 | + self.expect_create_machine_group(0) |
2857 | + self.mocker.replay() |
2858 | + deferred = self.get_port_manager().ensure_groups(0) |
2859 | + return deferred.addCallback(self.check_group_names, 0) |
2860 | + |
2861 | + def test_existing_machine_group(self): |
2862 | + """An existing machine security group is deleted and remade""" |
2863 | + self.expect_nova_get("os-security-groups", |
2864 | + response={'security_groups': [ |
2865 | + {'name': "juju-testing-6", 'id': 3}, |
2866 | + ]}) |
2867 | + self.expect_create_juju_group() |
2868 | + self.expect_nova_delete("os-security-groups/3") |
2869 | + self.expect_create_machine_group(6) |
2870 | + self.mocker.replay() |
2871 | + deferred = self.get_port_manager().ensure_groups(6) |
2872 | + return deferred.addCallback(self.check_group_names, 6) |
2873 | + |
2874 | + |
2875 | +class GetMachineGroupsTests(PortManagerTestMixin, TestCase): |
2876 | + """Tests for get_machine_groups method needed for machine shutdown""" |
2877 | + |
2878 | + def test_normal(self): |
2879 | + """A standard juju machine returns the machine group name and id""" |
2880 | + self.expect_nova_get("servers/1000/os-security-groups", |
2881 | + response={'security_groups': [ |
2882 | + {'id': 7, 'name': "juju-testing"}, |
2883 | + {'id': 8, 'name': "juju-testing-0"}, |
2884 | + ]}) |
2885 | + self.mocker.replay() |
2886 | + machine = NovaProviderMachine(1000) |
2887 | + deferred = self.get_port_manager().get_machine_groups(machine) |
2888 | + return deferred.addCallback(self.assertEqual, {"juju-testing-0": 8}) |
2889 | + |
2890 | + def test_normal_include_juju(self): |
2891 | + """If param with_juju_group=True the juju group is also returned""" |
2892 | + self.expect_nova_get("servers/1000/os-security-groups", |
2893 | + response={'security_groups': [ |
2894 | + {'id': 7, 'name': "juju-testing"}, |
2895 | + {'id': 8, 'name': "juju-testing-0"}, |
2896 | + ]}) |
2897 | + self.mocker.replay() |
2898 | + machine = NovaProviderMachine(1000) |
2899 | + deferred = self.get_port_manager().get_machine_groups(machine, True) |
2900 | + return deferred.addCallback(self.assertEqual, |
2901 | + {"juju-testing": 7, "juju-testing-0": 8}) |
2902 | + |
2903 | + def test_extra_group(self): |
2904 | + """Additional groups not in the juju namespace are ignored""" |
2905 | + self.expect_nova_get("servers/1000/os-security-groups", |
2906 | + response={'security_groups': [ |
2907 | + {'id': 1, 'name': "default"}, |
2908 | + {'id': 7, 'name': "juju-testing"}, |
2909 | + {'id': 8, 'name': "juju-testing-0"}, |
2910 | + ]}) |
2911 | + self.mocker.replay() |
2912 | + machine = NovaProviderMachine(1000) |
2913 | + deferred = self.get_port_manager().get_machine_groups(machine) |
2914 | + return deferred.addCallback(self.assertEqual, {"juju-testing-0": 8}) |
2915 | + |
2916 | + def test_other_group(self): |
2917 | + """A server not managed by juju returns nothing""" |
2918 | + self.expect_nova_get("servers/1000/os-security-groups", |
2919 | + response={'security_groups': [ |
2920 | + {'id': 1, 'name': "default"}, |
2921 | + ]}) |
2922 | + self.mocker.replay() |
2923 | + machine = NovaProviderMachine(1000) |
2924 | + deferred = self.get_port_manager().get_machine_groups(machine) |
2925 | + return deferred.addCallback(self.assertEqual, None) |
2926 | + |
2927 | + def test_missing_groups(self): |
2928 | + """A server with no groups returns nothing""" |
2929 | + self.expect_nova_get("servers/1000/os-security-groups", |
2930 | + response={'security_groups': []}) |
2931 | + self.mocker.replay() |
2932 | + machine = NovaProviderMachine(1000) |
2933 | + deferred = self.get_port_manager().get_machine_groups(machine) |
2934 | + return deferred.addCallback(self.assertEqual, None) |
2935 | + |
2936 | + def test_error_missing_server(self): |
2937 | + """A server that doesn't exist or has been deleted returns nothing""" |
2938 | + self.expect_nova_get("servers/1000/os-security-groups", |
2939 | + code=404, response={"itemNotFound": { |
2940 | + "message": "Instance 1000 could not be found.", |
2941 | + "code": 404, |
2942 | + }}) |
2943 | + self.mocker.replay() |
2944 | + machine = NovaProviderMachine(1000) |
2945 | + deferred = self.get_port_manager().get_machine_groups(machine) |
2946 | + return deferred.addCallback(self.assertEqual, None) |
2947 | + # XXX: Broken by workaround for HP not supporting this api |
2948 | + test_error_missing_server.skip = True |
2949 | + |
2950 | + def test_error_missing_page(self): |
2951 | + """Unexpected errors from the client are propogated""" |
2952 | + self.expect_nova_get("servers/1000/os-security-groups", |
2953 | + code=404, response="404 Not Found\n\n" |
2954 | + "The resource could not be found.\n\n ") |
2955 | + self.mocker.replay() |
2956 | + machine = NovaProviderMachine(1000) |
2957 | + deferred = self.get_port_manager().get_machine_groups(machine) |
2958 | + return self.assertFailure(deferred, errors.ProviderInteractionError) |
2959 | + # XXX: Need implemention of fancy error to exception mapping |
2960 | + test_error_missing_page.skip = True |
2961 | + |
2962 | + def test_error_missing_server_fault(self): |
2963 | + "A bogus compute fault due to lp:1010486 returns nothing""" |
2964 | + self.expect_nova_get("servers/1000/os-security-groups", |
2965 | + code=500, response={"computeFault": { |
2966 | + "message": "The server has either erred or is incapable of" |
2967 | + " performing the requested operation.", |
2968 | + "code": 500, |
2969 | + }}) |
2970 | + self.expect_nova_get("servers/1000", |
2971 | + code=404, response={"itemNotFound": { |
2972 | + "message": "The resource could not be found.", |
2973 | + "code": 404, |
2974 | + }}) |
2975 | + self.mocker.replay() |
2976 | + machine = NovaProviderMachine(1000) |
2977 | + deferred = self.get_port_manager().get_machine_groups(machine) |
2978 | + return deferred.addCallback(self.assertEqual, None) |
2979 | + # XXX: Need implemention of fancy error to exception mapping |
2980 | + test_error_missing_server_fault.skip = True |
2981 | + |
2982 | + def test_error_really_fault(self): |
2983 | + """A real compute fault is propogated""" |
2984 | + self.expect_nova_get("servers/1000/os-security-groups", |
2985 | + code=500, response={"computeFault": { |
2986 | + "message": "The server has either erred or is incapable of" |
2987 | + " performing the requested operation.", |
2988 | + "code": 500, |
2989 | + }}) |
2990 | + self.expect_nova_get("servers/1000", response={"server": {"id": 1000}}) |
2991 | + self.mocker.replay() |
2992 | + machine = NovaProviderMachine(1000) |
2993 | + deferred = self.get_port_manager().get_machine_groups(machine) |
2994 | + return self.assertFailure(deferred, errors.ProviderInteractionError) |
2995 | + # XXX: Need implemention of fancy error to exception mapping |
2996 | + test_error_really_fault.skip = True |
2997 | |
2998 | === added file 'juju/providers/openstack/tests/test_provider.py' |
2999 | --- juju/providers/openstack/tests/test_provider.py 1970-01-01 00:00:00 +0000 |
3000 | +++ juju/providers/openstack/tests/test_provider.py 2012-07-18 19:47:20 +0000 |
3001 | @@ -0,0 +1,116 @@ |
3002 | +"""Testing for the OpenStack provider interface""" |
3003 | + |
3004 | +from juju.lib.testing import TestCase |
3005 | +from juju.environment.errors import EnvironmentsConfigError |
3006 | + |
3007 | +from juju.providers.openstack.files import FileStorage |
3008 | +from juju.providers.openstack.provider import MachineProvider |
3009 | + |
3010 | + |
3011 | +class ProviderTestCase(TestCase): |
3012 | + |
3013 | + environment_name = "testing" |
3014 | + |
3015 | + _test_environ = { |
3016 | + "NOVA_URL": "http://environ.invalid", |
3017 | + "NOVA_API_KEY": "env-key", |
3018 | + "EC2_SECRET_KEY": "env-xxxx", |
3019 | + "NOVA_PROJECT_ID": "env-project", |
3020 | + } |
3021 | + |
3022 | + def get_config(self): |
3023 | + return { |
3024 | + "type": "openstack", |
3025 | + "auth-mode": "keypair", |
3026 | + "access-key": "key", |
3027 | + "secret-key": "xxxxxxxx", |
3028 | + "auth-url": "http://testing.invalid", |
3029 | + "project-name": "project", |
3030 | + "control-bucket": self.environment_name, |
3031 | + } |
3032 | + |
3033 | + def test_empty_config_raises(self): |
3034 | + """Passing no config raises an exception about lacking credentials""" |
3035 | + self.change_environment() |
3036 | + # XXX: Should this raise EnvironmentsConfigError instead? |
3037 | + self.assertRaises(ValueError, |
3038 | + MachineProvider, self.environment_name, {}) |
3039 | + |
3040 | + def test_client_params(self): |
3041 | + """Config details get passed through to OpenStack client correctly""" |
3042 | + config = self.get_config() |
3043 | + provider = MachineProvider(self.environment_name, config) |
3044 | + creds = provider.credentials |
3045 | + self.assertEquals("key", creds.access_key) |
3046 | + self.assertEquals("xxxxxxxx", creds.secret_key) |
3047 | + self.assertEquals("http://testing.invalid", creds.url) |
3048 | + self.assertEquals("project", creds.project_name) |
3049 | + self.assertIs(creds, provider.nova._client.credentials) |
3050 | + self.assertIs(creds, provider.swift._client.credentials) |
3051 | + |
3052 | + def test_provider_attributes(self): |
3053 | + """ |
3054 | + The provider environment name and config should be available as |
3055 | + parameters in the provider. |
3056 | + """ |
3057 | + provider = MachineProvider(self.environment_name, self.get_config()) |
3058 | + self.assertEqual(provider.environment_name, self.environment_name) |
3059 | + self.assertEqual(provider.config.get("type"), "openstack") |
3060 | + self.assertEqual(provider.provider_type, "openstack") |
3061 | + |
3062 | + def test_get_file_storage(self): |
3063 | + """The file storage is accessible via the machine provider.""" |
3064 | + provider = MachineProvider(self.environment_name, self.get_config()) |
3065 | + storage = provider.get_file_storage() |
3066 | + self.assertTrue(isinstance(storage, FileStorage)) |
3067 | + |
3068 | + def test_config_serialization(self): |
3069 | + """ |
3070 | + The provider configuration can be serialized to yaml. |
3071 | + """ |
3072 | + self.change_environment() |
3073 | + config = self.get_config() |
3074 | + expected = config.copy() |
3075 | + config["authorized-keys-path"] = self.makeFile("key contents") |
3076 | + expected["authorized-keys"] = "key contents" |
3077 | + provider = MachineProvider(self.environment_name, config) |
3078 | + self.assertEqual(expected, provider.get_serialization_data()) |
3079 | + |
3080 | + def test_config_environment_extraction(self): |
3081 | + """ |
3082 | + The provider serialization loads keys as needed from the environment. |
3083 | + |
3084 | + Variables from the configuration take precendence over those from |
3085 | + the environment, when serializing. |
3086 | + """ |
3087 | + self.change_environment(**self._test_environ) |
3088 | + provider = MachineProvider(self.environment_name, { |
3089 | + "auth-mode": "keypair", |
3090 | + "project-name": "other-project", |
3091 | + "authorized-keys": "key-data", |
3092 | + }) |
3093 | + serialized = provider.get_serialization_data() |
3094 | + expected = { |
3095 | + "auth-mode": "keypair", |
3096 | + "access-key": "env-key", |
3097 | + "secret-key": "env-xxxx", |
3098 | + "auth-url": "http://environ.invalid", |
3099 | + "project-name": "other-project", |
3100 | + "authorized-keys": "key-data", |
3101 | + } |
3102 | + self.assertEqual(expected, serialized) |
3103 | + |
3104 | + def test_conflicting_authorized_keys_options(self): |
3105 | + """ |
3106 | + We can't handle two different authorized keys options, so deny |
3107 | + constructing an environment that way. |
3108 | + """ |
3109 | + config = self.get_config() |
3110 | + config["authorized-keys"] = "File content" |
3111 | + config["authorized-keys-path"] = "File path" |
3112 | + error = self.assertRaises(EnvironmentsConfigError, |
3113 | + MachineProvider, self.environment_name, config) |
3114 | + self.assertEquals( |
3115 | + str(error), |
3116 | + "Environment config cannot define both authorized-keys and " |
3117 | + "authorized-keys-path. Pick one!") |
3118 | |
3119 | === added file 'juju/providers/openstack/tests/test_shutdown.py' |
3120 | --- juju/providers/openstack/tests/test_shutdown.py 1970-01-01 00:00:00 +0000 |
3121 | +++ juju/providers/openstack/tests/test_shutdown.py 2012-07-18 19:47:20 +0000 |
3122 | @@ -0,0 +1,130 @@ |
3123 | +"""Tests for terminating machines and cleaning up the environment""" |
3124 | + |
3125 | +from juju import errors |
3126 | +from juju.lib import testing |
3127 | +from juju.machine import ProviderMachine |
3128 | +from juju.providers.openstack.machine import NovaProviderMachine |
3129 | +from juju.providers.openstack.tests import OpenStackTestMixin |
3130 | + |
3131 | + |
3132 | +class ShutdownMachineTests(OpenStackTestMixin, testing.TestCase): |
3133 | + |
3134 | + def test_shutdown_single(self): |
3135 | + self.expect_nova_get("servers/1000", |
3136 | + response={"server": { |
3137 | + 'name': "juju testing instance 0", |
3138 | + }}) |
3139 | + self.expect_nova_delete("servers/1000", code=204) |
3140 | + self.mocker.replay() |
3141 | + machine = NovaProviderMachine(1000) |
3142 | + deferred = self.get_provider().shutdown_machine(machine) |
3143 | + deferred.addCallback(self.assertIs, machine) |
3144 | + |
3145 | + def test_shutdown_single_other(self): |
3146 | + self.expect_nova_get("servers/1000", |
3147 | + response={"server": { |
3148 | + 'name': "some other instance", |
3149 | + }}) |
3150 | + self.mocker.replay() |
3151 | + machine = NovaProviderMachine(1000) |
3152 | + deferred = self.get_provider().shutdown_machine(machine) |
3153 | + return self.assert_not_found(deferred, [1000]) |
3154 | + |
3155 | + def test_shutdown_single_wrong_machine(self): |
3156 | + self.mocker.reset() |
3157 | + machine = ProviderMachine("i-000003E8") |
3158 | + e = self.assertRaises(errors.ProviderError, |
3159 | + self.get_provider().shutdown_machine, machine) |
3160 | + self.assertIn("Need a NovaProviderMachine to shutdown", str(e)) |
3161 | + |
3162 | + def test_shutdown_multi_none(self): |
3163 | + self.mocker.reset() |
3164 | + deferred = self.get_provider().shutdown_machines([]) |
3165 | + return deferred.addCallback(self.assertEqual, []) |
3166 | + |
3167 | + def test_shutdown_multi_some_invalid(self): |
3168 | + """No machines are shutdown if some are invalid""" |
3169 | + self.mocker.unorder() |
3170 | + self.expect_nova_get("servers/1001", |
3171 | + response={"server": { |
3172 | + 'name': "juju testing instance 1", |
3173 | + }}) |
3174 | + self.expect_nova_get("servers/1002", |
3175 | + response={"server": { |
3176 | + 'name': "some other instance", |
3177 | + }}) |
3178 | + self.mocker.replay() |
3179 | + machines = [NovaProviderMachine(1001), NovaProviderMachine(1002)] |
3180 | + deferred = self.get_provider().shutdown_machines(machines) |
3181 | + return self.assert_not_found(deferred, [1002]) |
3182 | + # XXX: dumb requirement to keep all running if some invalid, drop this |
3183 | + test_shutdown_multi_some_invalid.skip = True |
3184 | + |
3185 | + # GZ 2012-06-11: Corner case difference, EC2 rechecks machine statuses and |
3186 | + # group membership on shutdown. |
3187 | + |
3188 | + def test_shutdown_multi_success(self): |
3189 | + """Machines are shutdown and groups except juju group are deleted""" |
3190 | + self.mocker.unorder() |
3191 | + self.expect_nova_get("servers/1001", |
3192 | + response={"server": { |
3193 | + 'name': "juju testing instance 1", |
3194 | + }}) |
3195 | + self.expect_nova_get("servers/1002", |
3196 | + response={"server": { |
3197 | + 'name': "juju testing instance 2", |
3198 | + }}) |
3199 | + self.expect_nova_delete("servers/1001", code=204) |
3200 | + self.expect_nova_delete("servers/1002", code=204) |
3201 | + self.mocker.replay() |
3202 | + machines = [NovaProviderMachine(1001), NovaProviderMachine(1002)] |
3203 | + deferred = self.get_provider().shutdown_machines(machines) |
3204 | + return deferred |
3205 | + |
3206 | + |
3207 | +class DestroyEnvironmentTests(OpenStackTestMixin, testing.TestCase): |
3208 | + |
3209 | + def check_machine_ids(self, machines, server_ids): |
3210 | + self.assertEqual(set(m.instance_id for m in machines), set(server_ids)) |
3211 | + |
3212 | + def test_destroy_environment(self): |
3213 | + self.mocker.unorder() |
3214 | + self.expect_swift_put("testing/provider-state", "{}\n") |
3215 | + self.expect_nova_get("servers", response={"servers": [ |
3216 | + self.make_server(1001), |
3217 | + self.make_server(1002), |
3218 | + ]}) |
3219 | + self.expect_nova_get("servers/1001", |
3220 | + response={"server": { |
3221 | + 'name': "juju testing instance 1", |
3222 | + }}) |
3223 | + self.expect_nova_get("servers/1002", |
3224 | + response={"server": { |
3225 | + 'name': "juju testing instance 2", |
3226 | + }}) |
3227 | + self.expect_nova_delete("servers/1001", code=204) |
3228 | + self.expect_nova_delete("servers/1002", code=204) |
3229 | + self.mocker.replay() |
3230 | + deferred = self.get_provider().destroy_environment() |
3231 | + return deferred.addCallback(self.check_machine_ids, [1001, 1002]) |
3232 | + |
3233 | + def test_s3_failure(self): |
3234 | + self.mocker.unorder() |
3235 | + self.expect_swift_put("testing/provider-state", "{}\n", |
3236 | + code=500, response="Server unavailable or something") |
3237 | + # XXX: normal server shutdown should be expected here |
3238 | + self.mocker.replay() |
3239 | + deferred = self.get_provider().destroy_environment() |
3240 | + return deferred.addCallback(self.assertEqual, []) |
3241 | + # XXX: Need to bolster swift robustness in response to api errors |
3242 | + test_s3_failure.skip = True |
3243 | + |
3244 | + # GZ 2012-06-15: Always tries removing juju group unlike EC2 currently |
3245 | + def test_shutdown_no_instances(self): |
3246 | + """With no instances no shutdowns are attempted""" |
3247 | + self.mocker.unorder() |
3248 | + self.expect_swift_put("testing/provider-state", "{}\n") |
3249 | + self.expect_nova_get("servers", response={"servers": []}) |
3250 | + self.mocker.replay() |
3251 | + deferred = self.get_provider().destroy_environment() |
3252 | + return deferred.addCallback(self.assertEqual, []) |
3253 | |
3254 | === added file 'juju/providers/openstack/tests/test_state.py' |
3255 | --- juju/providers/openstack/tests/test_state.py 1970-01-01 00:00:00 +0000 |
3256 | +++ juju/providers/openstack/tests/test_state.py 2012-07-18 19:47:20 +0000 |
3257 | @@ -0,0 +1,58 @@ |
3258 | +"""Tests for the common state interface over the the Openstack provider |
3259 | + |
3260 | +The testcases here largely duplicate those in openstack.tests.test_files as |
3261 | +state handling is a pretty thin layer over the file storage. |
3262 | +""" |
3263 | + |
3264 | +import yaml |
3265 | + |
3266 | +from juju.lib.testing import TestCase |
3267 | +from juju.providers.openstack.tests import OpenStackTestMixin |
3268 | + |
3269 | + |
3270 | +class OpenStackStateTest(OpenStackTestMixin, TestCase): |
3271 | + |
3272 | + def test_save(self): |
3273 | + """Saving a dict puts yaml serialized bytes in provider-state""" |
3274 | + state = {"zookeeper-instances": [ |
3275 | + [1000, "x1.example.com"], |
3276 | + ]} |
3277 | + self.expect_swift_put("testing/provider-state", yaml.dump(state)) |
3278 | + self.mocker.replay() |
3279 | + return self.get_provider().save_state(state) |
3280 | + |
3281 | + def test_save_missing_container(self): |
3282 | + """Saving will create the container when it does not exist already""" |
3283 | + state = {"zookeeper-instances": [ |
3284 | + [1000, "x1.example.com"], |
3285 | + ]} |
3286 | + state_bytes = yaml.dump(state) |
3287 | + self.expect_swift_put("testing/provider-state", state_bytes, code=404) |
3288 | + self.expect_swift_put_container("testing") |
3289 | + self.expect_swift_put("testing/provider-state", state_bytes) |
3290 | + self.mocker.replay() |
3291 | + return self.get_provider().save_state(state) |
3292 | + |
3293 | + def test_load(self): |
3294 | + """Loading deserializes yaml from provider-state to a python dict""" |
3295 | + state = {"zookeeper-instances": []} |
3296 | + self.expect_swift_get("testing/provider-state", |
3297 | + response=yaml.dump(state)) |
3298 | + self.mocker.replay() |
3299 | + deferred = self.get_provider().load_state() |
3300 | + return deferred.addCallback(self.assertEqual, state) |
3301 | + |
3302 | + def test_load_missing(self): |
3303 | + """Loading returns False if provider-state does not exist""" |
3304 | + self.expect_swift_get("testing/provider-state", code=404, |
3305 | + response={}) |
3306 | + self.mocker.replay() |
3307 | + deferred = self.get_provider().load_state() |
3308 | + return deferred.addCallback(self.assertIs, False) |
3309 | + |
3310 | + def test_load_no_content(self): |
3311 | + """Loading returns False if provider-state is empty""" |
3312 | + self.expect_swift_get("testing/provider-state", response="") |
3313 | + self.mocker.replay() |
3314 | + deferred = self.get_provider().load_state() |
3315 | + return deferred.addCallback(self.assertIs, False) |
3316 | |
3317 | === added directory 'juju/providers/openstack_s3' |
3318 | === added file 'juju/providers/openstack_s3/__init__.py' |
3319 | --- juju/providers/openstack_s3/__init__.py 1970-01-01 00:00:00 +0000 |
3320 | +++ juju/providers/openstack_s3/__init__.py 2012-07-18 19:47:20 +0000 |
3321 | @@ -0,0 +1,48 @@ |
3322 | +"""Provider interface implementation for Openstack with S3 storage""" |
3323 | + |
3324 | +import os |
3325 | + |
3326 | +from txaws.service import AWSServiceRegion |
3327 | + |
3328 | +from juju.providers.openstack import provider as os_provider |
3329 | +from juju.providers.openstack import credentials |
3330 | +from juju.providers.ec2 import files as s3_files |
3331 | + |
3332 | + |
3333 | +class HybridCredentials(credentials.OpenStackCredentials): |
3334 | + """Encapsulation of credentials with S3 required values included""" |
3335 | + |
3336 | + _config_vars = { |
3337 | + 'combined-key': ("EC2_ACCESS_KEY", "AWS_ACCESS_KEY_ID"), |
3338 | + 's3-uri': ("S3_URL",), |
3339 | + } |
3340 | + _config_vars.update(credentials.OpenStackCredentials._config_vars) |
3341 | + |
3342 | + |
3343 | +class MachineProvider(os_provider.MachineProvider): |
3344 | + """MachineProvider for use in Openstack environment but with S3 API""" |
3345 | + |
3346 | + Credentials = HybridCredentials |
3347 | + |
3348 | + def __init__(self, environment_name, config): |
3349 | + super(MachineProvider, self).__init__(environment_name, config) |
3350 | + |
3351 | + del self.swift |
3352 | + |
3353 | + # If access or secret keys are still blank, inside txaws environment |
3354 | + # a ValueError will be raised after rechecking the environment. |
3355 | + self._aws_service = AWSServiceRegion( |
3356 | + access_key=self.credentials.combined_key, |
3357 | + secret_key=self.credentials.secret_key, |
3358 | + ec2_uri="", # The EC2 client will not be used |
3359 | + s3_uri=self.credentials.s3_uri) |
3360 | + |
3361 | + self.s3 = self._aws_service.get_s3_client() |
3362 | + |
3363 | + @property |
3364 | + def provider_type(self): |
3365 | + return "openstack_s3" |
3366 | + |
3367 | + def get_file_storage(self): |
3368 | + """Retrieve a S3 API compatible backend FileStorage class""" |
3369 | + return s3_files.FileStorage(self.s3, self.config["control-bucket"]) |
3370 | |
3371 | === added directory 'juju/providers/openstack_s3/tests' |
3372 | === added file 'juju/providers/openstack_s3/tests/__init__.py' |
3373 | === added file 'juju/providers/openstack_s3/tests/test_provider.py' |
3374 | --- juju/providers/openstack_s3/tests/test_provider.py 1970-01-01 00:00:00 +0000 |
3375 | +++ juju/providers/openstack_s3/tests/test_provider.py 2012-07-18 19:47:20 +0000 |
3376 | @@ -0,0 +1,122 @@ |
3377 | +"""Testing for the OpenStack provider interface""" |
3378 | + |
3379 | +from juju.lib.testing import TestCase |
3380 | +from juju.environment.errors import EnvironmentsConfigError |
3381 | + |
3382 | +from juju.providers.ec2.files import FileStorage |
3383 | +from juju.providers.openstack_s3 import MachineProvider |
3384 | + |
3385 | + |
3386 | +class ProviderTestCase(TestCase): |
3387 | + |
3388 | + environment_name = "testing" |
3389 | + |
3390 | + _test_environ = { |
3391 | + "NOVA_URL": "http://environ.invalid", |
3392 | + "NOVA_API_KEY": "env-key", |
3393 | + "EC2_ACCESS_KEY": "env-key:env-project", |
3394 | + "EC2_SECRET_KEY": "env-xxxx", |
3395 | + "NOVA_PROJECT_ID": "env-project", |
3396 | + "S3_URL": "http://environ.invalid:3333", |
3397 | + } |
3398 | + |
3399 | + def get_config(self): |
3400 | + return { |
3401 | + "type": "openstack_s3", |
3402 | + "auth-mode": "keypair", |
3403 | + "access-key": "key", |
3404 | + "secret-key": "xxxxxxxx", |
3405 | + "auth-url": "http://testing.invalid", |
3406 | + "project-name": "project", |
3407 | + "control-bucket": self.environment_name, |
3408 | + "combined-key": "key:project", |
3409 | + "s3-uri": "http://testing.invalid:3333", |
3410 | + } |
3411 | + |
3412 | + def test_client_params(self): |
3413 | + """Config details get passed through to OpenStack client correctly""" |
3414 | + config = self.get_config() |
3415 | + provider = MachineProvider(self.environment_name, config) |
3416 | + creds = provider.credentials |
3417 | + self.assertEquals("key", creds.access_key) |
3418 | + self.assertEquals("xxxxxxxx", creds.secret_key) |
3419 | + self.assertEquals("http://testing.invalid", creds.url) |
3420 | + self.assertEquals("project", creds.project_name) |
3421 | + self.assertIs(creds, provider.nova._client.credentials) |
3422 | + |
3423 | + def test_s3_params(self): |
3424 | + """Config details get passed through to txaws S3 client correctly""" |
3425 | + config = self.get_config() |
3426 | + s3 = MachineProvider(self.environment_name, config).s3 |
3427 | + self.assertEquals("http://testing.invalid:3333/", s3.endpoint.get_uri()) |
3428 | + self.assertEquals("key:project", s3.creds.access_key) |
3429 | + self.assertEquals("xxxxxxxx", s3.creds.secret_key) |
3430 | + |
3431 | + def test_provider_attributes(self): |
3432 | + """ |
3433 | + The provider environment name and config should be available as |
3434 | + parameters in the provider. |
3435 | + """ |
3436 | + provider = MachineProvider(self.environment_name, self.get_config()) |
3437 | + self.assertEqual(provider.environment_name, self.environment_name) |
3438 | + self.assertEqual(provider.config.get("type"), "openstack_s3") |
3439 | + self.assertEqual(provider.provider_type, "openstack_s3") |
3440 | + |
3441 | + def test_get_file_storage(self): |
3442 | + """The file storage is accessible via the machine provider.""" |
3443 | + provider = MachineProvider(self.environment_name, self.get_config()) |
3444 | + storage = provider.get_file_storage() |
3445 | + self.assertTrue(isinstance(storage, FileStorage)) |
3446 | + |
3447 | + def test_config_serialization(self): |
3448 | + """ |
3449 | + The provider configuration can be serialized to yaml. |
3450 | + """ |
3451 | + self.change_environment() |
3452 | + config = self.get_config() |
3453 | + expected = config.copy() |
3454 | + config["authorized-keys-path"] = self.makeFile("key contents") |
3455 | + expected["authorized-keys"] = "key contents" |
3456 | + provider = MachineProvider(self.environment_name, config) |
3457 | + self.assertEqual(expected, provider.get_serialization_data()) |
3458 | + |
3459 | + def test_config_environment_extraction(self): |
3460 | + """ |
3461 | + The provider serialization loads keys as needed from the environment. |
3462 | + |
3463 | + Variables from the configuration take precendence over those from |
3464 | + the environment, when serializing. |
3465 | + """ |
3466 | + self.change_environment(**self._test_environ) |
3467 | + provider = MachineProvider(self.environment_name, { |
3468 | + "auth-mode": "keypair", |
3469 | + "project-name": "other-project", |
3470 | + "authorized-keys": "key-data", |
3471 | + }) |
3472 | + serialized = provider.get_serialization_data() |
3473 | + expected = { |
3474 | + "auth-mode": "keypair", |
3475 | + "access-key": "env-key", |
3476 | + "secret-key": "env-xxxx", |
3477 | + "auth-url": "http://environ.invalid", |
3478 | + "project-name": "other-project", |
3479 | + "authorized-keys": "key-data", |
3480 | + "combined-key": "env-key:env-project", |
3481 | + "s3-uri": "http://environ.invalid:3333", |
3482 | + } |
3483 | + self.assertEqual(expected, serialized) |
3484 | + |
3485 | + def test_conflicting_authorized_keys_options(self): |
3486 | + """ |
3487 | + We can't handle two different authorized keys options, so deny |
3488 | + constructing an environment that way. |
3489 | + """ |
3490 | + config = self.get_config() |
3491 | + config["authorized-keys"] = "File content" |
3492 | + config["authorized-keys-path"] = "File path" |
3493 | + error = self.assertRaises(EnvironmentsConfigError, |
3494 | + MachineProvider, self.environment_name, config) |
3495 | + self.assertEquals( |
3496 | + str(error), |
3497 | + "Environment config cannot define both authorized-keys and " |
3498 | + "authorized-keys-path. Pick one!") |
3499 | |
3500 | === modified file 'juju/state/initialize.py' |
3501 | --- juju/state/initialize.py 2012-03-29 01:37:57 +0000 |
3502 | +++ juju/state/initialize.py 2012-07-18 19:47:20 +0000 |
3503 | @@ -30,7 +30,10 @@ |
3504 | """ |
3505 | self.client = client |
3506 | self.admin_identity = admin_identity |
3507 | + if instance_id.isdigit(): |
3508 | + instance_id = int(instance_id) |
3509 | self.instance_id = instance_id |
3510 | + |
3511 | self.constraints_data = constraints_data |
3512 | self.provider_type = provider_type |
3513 | |
3514 | @@ -57,6 +60,7 @@ |
3515 | # Poke constraints data into a machine state to represent this machine. |
3516 | manager = MachineStateManager(self.client) |
3517 | machine_state = yield manager.add_machine_state(constraints) |
3518 | + |
3519 | yield machine_state.set_instance_id(self.instance_id) |
3520 | |
3521 | # Set up environment constraints similarly. |
3522 | |
3523 | === modified file 'juju/unit/address.py' |
3524 | --- juju/unit/address.py 2012-03-22 09:08:12 +0000 |
3525 | +++ juju/unit/address.py 2012-07-18 19:47:20 +0000 |
3526 | @@ -16,6 +16,8 @@ |
3527 | provider_type = yield settings.get_provider_type() |
3528 | if provider_type == "ec2": |
3529 | returnValue(EC2UnitAddress()) |
3530 | + if provider_type in ("openstack", "openstack_s3"): |
3531 | + returnValue(OpenStackUnitAddress()) |
3532 | elif provider_type == "local": |
3533 | returnValue(LocalUnitAddress()) |
3534 | elif provider_type == "orchestra": |
3535 | @@ -24,7 +26,6 @@ |
3536 | returnValue(DummyUnitAddress()) |
3537 | elif provider_type == "maas": |
3538 | returnValue(MAASUnitAddress()) |
3539 | - |
3540 | raise JujuError( |
3541 | "Unknown provider type: %r, unit addresses unknown." % provider_type) |
3542 | |
3543 | @@ -32,10 +33,10 @@ |
3544 | class UnitAddress(object): |
3545 | |
3546 | def get_private_address(self): |
3547 | - raise NotImplemented() |
3548 | + raise NotImplementedError(self.get_private_address) |
3549 | |
3550 | def get_public_address(self): |
3551 | - raise NotImplemented() |
3552 | + raise NotImplementedError(self.get_public_address) |
3553 | |
3554 | |
3555 | class DummyUnitAddress(UnitAddress): |
3556 | @@ -62,6 +63,27 @@ |
3557 | returnValue(content.strip()) |
3558 | |
3559 | |
3560 | +class OpenStackUnitAddress(UnitAddress): |
3561 | + """Address determination of a service unit on an OpenStack server |
3562 | + |
3563 | + Unlike EC2 there are no promises that an instance will have a resolvable |
3564 | + hostname, or for that matter a public ip address. |
3565 | + """ |
3566 | + |
3567 | + def _get_metadata_string(self, key): |
3568 | + return client.getPage("http://169.254.169.254/1.0/meta-data/" + key) |
3569 | + |
3570 | + def get_private_address(self): |
3571 | + return self._get_metadata_string("local-ipv4") |
3572 | + |
3573 | + @inlineCallbacks |
3574 | + def get_public_address(self): |
3575 | + address = yield self._get_metadata_string("public-ipv4") |
3576 | + if not address: |
3577 | + address = yield self.get_private_address() |
3578 | + returnValue(address) |
3579 | + |
3580 | + |
3581 | class LocalUnitAddress(UnitAddress): |
3582 | |
3583 | def get_private_address(self): |
3584 | @@ -87,6 +109,7 @@ |
3585 | output = subprocess.check_output(["hostname", "-f"]) |
3586 | return output.strip() |
3587 | |
3588 | + |
3589 | class MAASUnitAddress(UnitAddress): |
3590 | |
3591 | def get_private_address(self): |
3592 | |
3593 | === modified file 'juju/unit/tests/test_address.py' |
3594 | --- juju/unit/tests/test_address.py 2012-03-22 09:08:12 +0000 |
3595 | +++ juju/unit/tests/test_address.py 2012-07-18 19:47:20 +0000 |
3596 | @@ -8,7 +8,7 @@ |
3597 | from juju.lib.testing import TestCase |
3598 | from juju.unit.address import ( |
3599 | EC2UnitAddress, LocalUnitAddress, OrchestraUnitAddress, DummyUnitAddress, |
3600 | - MAASUnitAddress, get_unit_address) |
3601 | + MAASUnitAddress, OpenStackUnitAddress, UnitAddress, get_unit_address) |
3602 | from juju.state.environment import GlobalSettingsStateManager |
3603 | |
3604 | |
3605 | @@ -32,6 +32,16 @@ |
3606 | self.assertTrue(isinstance(address, EC2UnitAddress)) |
3607 | |
3608 | @inlineCallbacks |
3609 | + def test_get_openstack_address(self): |
3610 | + address = yield self.get_address_for("openstack") |
3611 | + self.assertTrue(isinstance(address, OpenStackUnitAddress)) |
3612 | + |
3613 | + @inlineCallbacks |
3614 | + def test_get_openstack_s3_address(self): |
3615 | + address = yield self.get_address_for("openstack_s3") |
3616 | + self.assertTrue(isinstance(address, OpenStackUnitAddress)) |
3617 | + |
3618 | + @inlineCallbacks |
3619 | def test_get_local_address(self): |
3620 | address = yield self.get_address_for("local") |
3621 | self.assertTrue(isinstance(address, LocalUnitAddress)) |
3622 | @@ -55,6 +65,22 @@ |
3623 | return self.assertFailure(self.get_address_for("foobar"), JujuError) |
3624 | |
3625 | |
3626 | +class SubclassAddressTest(TestCase): |
3627 | + |
3628 | + class TestingAddress(UnitAddress): |
3629 | + """An address class that neglects to implement the required methods""" |
3630 | + |
3631 | + def test_get_public_address(self): |
3632 | + err = self.assertRaises(NotImplementedError, |
3633 | + self.TestingAddress().get_public_address) |
3634 | + self.assertIn("TestingAddress.get_public_address", str(err)) |
3635 | + |
3636 | + def test_get_private_address(self): |
3637 | + err = self.assertRaises(NotImplementedError, |
3638 | + self.TestingAddress().get_private_address) |
3639 | + self.assertIn("TestingAddress.get_private_address", str(err)) |
3640 | + |
3641 | + |
3642 | class DummyAddressTest(TestCase): |
3643 | |
3644 | def setUp(self): |
3645 | @@ -93,6 +119,35 @@ |
3646 | (yield self.address.get_public_address()), "foobar") |
3647 | |
3648 | |
3649 | +class OpenStackAddressTest(TestCase): |
3650 | + |
3651 | + def setUp(self): |
3652 | + self.address = OpenStackUnitAddress() |
3653 | + self.patch(client, "getPage", self._fetch_metadata) |
3654 | + |
3655 | + def _fetch_metadata(self, url): |
3656 | + head, tail = url.rsplit("/", 1) |
3657 | + self.assertEqual("http://169.254.169.254/1.0/meta-data", head) |
3658 | + return succeed(self.meta.pop(tail)) |
3659 | + |
3660 | + @inlineCallbacks |
3661 | + def test_get_private_address(self): |
3662 | + self.meta = {"local-ipv4": "192.168.0.2"} |
3663 | + self.assertEqual("192.168.0.2", |
3664 | + (yield self.address.get_private_address())) |
3665 | + |
3666 | + @inlineCallbacks |
3667 | + def test_get_public_address_present(self): |
3668 | + self.meta = {"public-ipv4": "8.8.8.8"} |
3669 | + self.assertEqual("8.8.8.8", (yield self.address.get_public_address())) |
3670 | + |
3671 | + @inlineCallbacks |
3672 | + def test_get_public_address_missing(self): |
3673 | + self.meta = {"public-ipv4": "", "local-ipv4": "192.168.0.2"} |
3674 | + self.assertEqual("192.168.0.2", |
3675 | + (yield self.address.get_public_address())) |
3676 | + |
3677 | + |
3678 | class LocalAddressTest(TestCase): |
3679 | |
3680 | def setUp(self): |
Hi martin, would you mind resubmitting this with the lbox tool that's in the juju ppa, its integrates with reitveld for inline code reviews. it should just be a matter of installing it and running lbox propose within the branch.