Merge lp:~gz/pyjuju/openstack_provider into lp:pyjuju

Proposed by Martin Packman
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
Reviewer Review Type Date Requested Status
Kapil Thangavelu (community) Approve
Review via email: mp+110860@code.launchpad.net

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.

https://codereview.appspot.com/6312050/

To post a comment you must log in.
Revision history for this message
Kapil Thangavelu (hazmat) wrote :

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.

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

Oh.. and btw this is awesome! :-)

Revision history for this message
Martin Packman (gz) wrote :

Please take a look.

Revision history for this message
Martin Packman (gz) wrote :

Reviewers: mp+110860_code.launchpad.net,

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://code.launchpad.net/~gz/juju/openstack_provider/+merge/110860

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/6312050/

Affected files:
   A [revision details]
   M juju/environment/config.py
   M juju/errors.py
   M juju/providers/common/findzookeepers.py
   M juju/providers/ec2/tests/test_launch.py
   A juju/providers/openstack/__init__.py
   A juju/providers/openstack/client.py
   A juju/providers/openstack/files.py
   A juju/providers/openstack/launch.py
   A juju/providers/openstack/machine.py
   A juju/providers/openstack/ports.py
   A juju/providers/openstack/provider.py
   A juju/providers/openstack/tests/__init__.py
   A juju/providers/openstack/tests/test_bootstrap.py
   A juju/providers/openstack/tests/test_client.py
   A juju/providers/openstack/tests/test_files.py
   A juju/providers/openstack/tests/test_getmachines.py
   A juju/providers/openstack/tests/test_machine.py
   A juju/providers/openstack/tests/test_ports.py
   A juju/providers/openstack/tests/test_provider.py
   A juju/providers/openstack/tests/test_shutdown.py
   A juju/providers/openstack/tests/test_state.py
   A juju/providers/openstack_s3/__init__.py
   A juju/providers/openstack_s3/tests/__init__.py
   A juju/providers/openstack_s3/tests/test_provider.py
   M juju/unit/address.py
   M juju/unit/tests/test_address.py

Revision history for this message
Kapil Thangavelu (hazmat) wrote :
Download full text (4.2 KiB)

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://codereview.appspot.com/6312050/diff/1/juju/environment/config.py
File juju/environment/config.py (right):

https://codereview.appspot.com/6312050/diff/1/juju/environment/config.py#newcode57
juju/environment/config.py:57: "default-instance-type": String(),
given constraint support default-instance-type shouldn't be needed as a
config option.

https://codereview.appspot.com/6312050/diff/1/juju/errors.py
File juju/errors.py (right):

https://codereview.appspot.com/6312050/diff/1/juju/errors.py#newcode162
juju/errors.py:162: ", ".join(map(str, self.instance_ids)))
looks fine, but curious why needed, instance ids should always be
integers.

https://codereview.appspot.com/6312050/diff/1/juju/providers/openstack/client.py
File juju/providers/openstack/client.py (right):

https://codereview.appspot.com/6312050/diff/1/juju/providers/openstack/client.py#newcode158
juju/providers/openstack/client.py:158: response, body = yield deferred
style minors, deferred transient isn't needed.

https://codereview.appspot.com/6312050/diff/1/juju/providers/openstack/client.py#newcode166
juju/providers/openstack/client.py:166: [self.token] =
response.headers.getRawHeaders("X-Auth-Token")
haven't seen this style before, preferable to just index 0 off the
result.

https://codereview.appspot.com/6312050/diff/1/juju/providers/openstack/client.py#newcode295
juju/providers/openstack/client.py:295: flavor_id = yield
self._lookup_flavor(flavor_name)
please document the kw params supported (sec groups and ipv4).

https://codereview.appspot.com/6312050/diff/1/juju/providers/openstack/client.py#newcode309
juju/providers/openstack/client.py:309: server["accessIPv4"] = ip
what's this for?

https://codereview.appspot.com/6312050/diff/1/juju/providers/openstack/files.py
File juju/providers/openstack/files.py (right):

https://codereview.appspot.com/6312050/diff/1/juju/providers/openstack/files.py#newcode78
juju/providers/openstack/files.py:78: if response.code != 201:
its likely the original error is more interesting at this point.

https://codereview.appspot.com/6312050/diff/1/juju/providers/openstack/launch.py
File juju/providers/openstack/launch.py (right):

https://codereview.appspot.com/6312050/diff/1/juju/providers/openstack/launch.py#newcode61
juju/providers/openstack/launch.py:61: if self._master:
is the md service not available?

https://codereview.appspot.com/6312050/diff/1/juju/providers/openstack/launch.py#newcode89
juju/providers/openstack/launch.py:89: yield filestorage.put(id_name,
StringIO(server['id']))
have you run into this in practice?, it seems hard to achieve, but good
to note nonetheless.

https://codereview.appspot.com/6312050/diff/1/juju/providers/openstack/launch.py#newcode99
juju/providers/openstack/launch.py:99: reactor.callLater(5,
deferred.callback, None)
ouch!.. you have to wait for the servers to boot to attach a public...

Read more...

Revision history for this message
Martin Packman (gz) wrote :
Download full text (3.2 KiB)

Thanks for the comments! Have replied to some below, will act on the
others.

https://codereview.appspot.com/6312050/diff/1/juju/errors.py
File juju/errors.py (right):

https://codereview.appspot.com/6312050/diff/1/juju/errors.py#newcode162
juju/errors.py:162: ", ".join(map(str, self.instance_ids)))
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://codereview.appspot.com/6312050/diff/1/juju/providers/openstack/client.py
File juju/providers/openstack/client.py (right):

https://codereview.appspot.com/6312050/diff/1/juju/providers/openstack/client.py#newcode309
juju/providers/openstack/client.py:309: server["accessIPv4"] = ip
Serves no purpose now, was added when I was experimenting with ways to
solve the problem of juju needing a public ip.

https://codereview.appspot.com/6312050/diff/1/juju/providers/openstack/files.py
File juju/providers/openstack/files.py (right):

https://codereview.appspot.com/6312050/diff/1/juju/providers/openstack/files.py#newcode78
juju/providers/openstack/files.py:78: if response.code != 201:
Right, the swift backend should probably raise exceptions then this
block could reraise the original one.

https://codereview.appspot.com/6312050/diff/1/juju/providers/openstack/launch.py
File juju/providers/openstack/launch.py (right):

https://codereview.appspot.com/6312050/diff/1/juju/providers/openstack/launch.py#newcode61
juju/providers/openstack/launch.py:61: if self._master:
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://codereview.appspot.com/6312050/diff/1/juju/providers/openstack/launch.py#newcode89
juju/providers/openstack/launch.py:89: yield filestorage.put(id_name,
StringIO(server['id']))
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://codereview.appspot.com/6312050/diff/1/juju/providers/openstack/launch.py#newcode99
juju/providers/openstack/launch.py:99: reactor.callLater(5,
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://codereview.appspot.com/6312050/diff/1/juju/providers/openstack/provider.py
File juju/providers/openstack/provider.py (right):

https://codereview.appspot.com/6312050/diff/1/juju/providers/openstack/provider.py#newcode198
juju/providers/openstack/provider.py:198: return defer.gatherResults(
I saw that helper but it wasn't obvious what it was adding. I think the
correct behaviour here requires usi...

Read more...

Revision history for this message
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-environment, and for some reason machine 0 doesn't get hostname details.

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.

Revision history for this message
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.

Revision history for this message
Martin Packman (gz) wrote :
Revision history for this message
Kapil Thangavelu (hazmat) wrote :
Download full text (5.6 KiB)

review round 2

https://codereview.appspot.com/6312050/diff/9001/juju/environment/config.py
File juju/environment/config.py (right):

https://codereview.appspot.com/6312050/diff/9001/juju/environment/config.py#newcode26
juju/environment/config.py:26: _OPENSTACK_AUTH_MODE = OneOf(
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://codereview.appspot.com/6312050/diff/9001/juju/providers/common/findzookeepers.py
File juju/providers/common/findzookeepers.py (right):

https://codereview.appspot.com/6312050/diff/9001/juju/providers/common/findzookeepers.py#newcode40
juju/providers/common/findzookeepers.py:40: % missing_instance_ids)
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://codereview.appspot.com/6312050/diff/9001/juju/providers/openstack/client.py
File juju/providers/openstack/client.py (right):

https://codereview.appspot.com/6312050/diff/9001/juju/providers/openstack/client.py#newcode115
juju/providers/openstack/client.py:115: body = yield
reader.onConnectionLost
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://codereview.appspot.com/6312050/diff/9001/juju/providers/openstack/client.py#newcode131
juju/providers/openstack/client.py:131: def make_url(self, service,
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://codereview.appspot.com/6312050/diff/9001/juju/providers/openstack/credentials.py
File juju/providers/openstack/credentials.py (right):

https://codereview.appspot.com/6312050/diff/9001/juju/providers/openstack/credentials.py#newcode8
juju/providers/openstack/credentials.py:8: TODO: rename 'nova-uri' to
'auth-url'
re TODO b/c?

https://codereview.appspot.com/6312050/diff/9001/juju/providers/openstack/files.py
File juju/providers/openstack/files.py (right):

https://codereview.appspot.com/6312050/diff/9001/juju/providers/openstack/files.py#newcode41
juju/providers/openstack/files.py:41: # XXX: what exactly is the
expectation here?
already documented in the module doc string.

https://codereview.appspot.com/6312050/diff/9001/juju/providers/openstack/launch.py
File juju/providers/openstack/launch.py (right):

https://codereview.appspot.com/6312050/diff/9001/juju/providers/openstack/launch.py#...

Read more...

Revision history for this message
Martin Packman (gz) wrote :
Download full text (6.8 KiB)

Thanks Kapil, is really useful input.

https://codereview.appspot.com/6312050/diff/9001/juju/environment/config.py
File juju/environment/config.py (right):

https://codereview.appspot.com/6312050/diff/9001/juju/environment/config.py#newcode26
juju/environment/config.py:26: _OPENSTACK_AUTH_MODE = OneOf(
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://codereview.appspot.com/6312050/diff/9001/juju/providers/common/findzookeepers.py
File juju/providers/common/findzookeepers.py (right):

https://codereview.appspot.com/6312050/diff/9001/juju/providers/common/findzookeepers.py#newcode40
juju/providers/common/findzookeepers.py:40: % missing_instance_ids)
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://codereview.appspot.com/6312050/diff/9001/juju/providers/openstack/client.py
File juju/providers/openstack/client.py (right):

https://codereview.appspot.com/6312050/diff/9001/juju/providers/openstack/client.py#newcode115
juju/providers/openstack/client.py:115: body = yield
reader.onConnectionLost
Right, still some rearrangements needed here for sanity.

https://codereview.appspot.com/6312050/diff/9001/juju/providers/openstack/client.py#newcode131
juju/providers/openstack/client.py:131: def make_url(self, service,
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/public_object_url needs it but that's a
somewhat special case.

https://codereview.appspot.com/6312050/diff/9001/juju/providers/openstack/credentials.py
File juju/providers/openstack/credentials.py (right):

https://codereview.appspot.com/6312050/diff/9001/juju/providers/openstack/credentials.py#newcode8
juju/providers/openstack/credentials.py:8: TODO: rename 'nova-uri' to
'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://codereview.appspot.com/6312050/diff/9001/juju/providers/openstack/files.py
File juju/providers/openstack/files.py (right):

https://codereview.appspot.com/6312050/diff/9001/juju/providers/openstack/files.py#newcode41
juju/providers/openstack/files.py:41: # XXX: what exactly is the
expectation here?
On 2012/07/03 17:25:44, hazmat wrote:
> already documented in the module doc string.

Right, will remove this.

https://codereview.appspot.com/6312050/diff/9001/juju/pr...

Read more...

Revision history for this message
Martin Packman (gz) wrote :
lp:~gz/pyjuju/openstack_provider updated
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

Revision history for this message
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/preference for essex style. Shutdown is more robust and simpler now with yanking the sec groups manipulation, trunk already supports group reuse, which obsoletes maintenance to a large extent (quotas not withstanding, but unused cleanup can be a jitsu thing). hpcloud's int instance ids are better supported (really only the bootstrap node was an issue). also includes rackspace auth support, i've left off on going much further with that, as its going to require a large refactoring of the client api interaction to be capabilities driven via service catalogs and extensions discovery.

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!

review: Approve
lp:~gz/pyjuju/openstack_provider updated
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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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):

Subscribers

People subscribed via source and target branches

to status/vote changes: