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
=== modified file 'juju/agents/provision.py'
--- juju/agents/provision.py 2012-03-20 10:13:22 +0000
+++ juju/agents/provision.py 2012-07-18 19:47:20 +0000
@@ -195,6 +195,7 @@
195 for instance_id in unused:195 for instance_id in unused:
196 log.info("Shutting down machine id:%s ...", instance_id)196 log.info("Shutting down machine id:%s ...", instance_id)
197 machine = provider_machines[instance_id]197 machine = provider_machines[instance_id]
198
198 try:199 try:
199 yield self.provider.shutdown_machine(machine)200 yield self.provider.shutdown_machine(machine)
200 except ProviderError:201 except ProviderError:
201202
=== modified file 'juju/control/status.py'
--- juju/control/status.py 2012-05-18 21:41:15 +0000
+++ juju/control/status.py 2012-07-18 19:47:20 +0000
@@ -543,7 +543,7 @@
543 m["agent-state"] = "not-started"543 m["agent-state"] = "not-started"
544 except ProviderError:544 except ProviderError:
545 # The provider doesn't have machine information545 # The provider doesn't have machine information
546 self.log.error(546 self.log.exception(
547 "Machine provider information missing: machine %s" % (547 "Machine provider information missing: machine %s" % (
548 machine_state.id))548 machine_state.id))
549549
550550
=== modified file 'juju/environment/config.py'
--- juju/environment/config.py 2012-04-11 01:17:53 +0000
+++ juju/environment/config.py 2012-07-18 19:47:20 +0000
@@ -6,8 +6,8 @@
6from juju.environment.errors import EnvironmentsConfigError6from juju.environment.errors import EnvironmentsConfigError
7from juju.errors import FileAlreadyExists, FileNotFound7from juju.errors import FileAlreadyExists, FileNotFound
8from juju.lib.schema import (8from juju.lib.schema import (
9 Constant, Dict, KeyDict, OAuthString, OneOf, SchemaError, SelectDict,9 Constant, Dict, Int, KeyDict, OAuthString, OneOf, SchemaError, SelectDict,
10 String)10 String, Bool)
1111
12DEFAULT_CONFIG_PATH = "~/.juju/environments.yaml"12DEFAULT_CONFIG_PATH = "~/.juju/environments.yaml"
1313
@@ -23,6 +23,14 @@
2323
24_EITHER_PLACEMENT = OneOf(Constant("unassigned"), Constant("local"))24_EITHER_PLACEMENT = OneOf(Constant("unassigned"), Constant("local"))
2525
26# See juju.providers.openstack.credentials for definition and more details
27_OPENSTACK_AUTH_MODE = OneOf(
28 Constant("userpass"),
29 Constant("keypair"),
30 Constant("legacy"),
31 Constant("rax"),
32 )
33
26SCHEMA = KeyDict({34SCHEMA = KeyDict({
27 "default": String(),35 "default": String(),
28 "environments": Dict(String(), SelectDict("type", {36 "environments": Dict(String(), SelectDict("type", {
@@ -49,6 +57,44 @@
49 optional=[57 optional=[
50 "access-key", "secret-key", "region", "ec2-uri", "s3-uri",58 "access-key", "secret-key", "region", "ec2-uri", "s3-uri",
51 "placement", "ssl-hostname-verification"]),59 "placement", "ssl-hostname-verification"]),
60 "openstack": KeyDict({
61 "control-bucket": String(),
62 "admin-secret": String(),
63 "access-key": String(),
64 "secret-key": String(),
65 "default-instance-type": String(),
66 "default-image-id": OneOf(String(), Int()),
67 "auth-url": String(),
68 "project-name": String(),
69 "use-floating-ip": Bool(),
70 "placement": _EITHER_PLACEMENT,
71 "auth-mode": _OPENSTACK_AUTH_MODE,
72 "region": String(),
73 "default-series": String(),
74 },
75 optional=[
76 "access-key", "secret-key", "auth-url", "project-name",
77 "placement", "auth-mode", "region", "use-floating-ip"]),
78 "openstack_s3": KeyDict({
79 "control-bucket": String(),
80 "admin-secret": String(),
81 "access-key": String(),
82 "secret-key": String(),
83 "default-instance-type": String(),
84 "default-image-id": OneOf(String(), Int()),
85 "auth-url": String(),
86 "placement": _EITHER_PLACEMENT,
87 "combined-key": String(),
88 "s3-uri": String(),
89 "use-floating-ip": Bool(),
90 "auth-mode": _OPENSTACK_AUTH_MODE,
91 "region": String(),
92 "default-series": String(),
93 },
94 optional=[
95 "access-key", "secret-key", "combined-key", "auth-url",
96 "s3-uri", "project-name", "placement", "auth-mode", "region",
97 "use-floating-ip"]),
52 "orchestra": KeyDict({98 "orchestra": KeyDict({
53 "orchestra-server": String(),99 "orchestra-server": String(),
54 "orchestra-user": String(),100 "orchestra-user": String(),
55101
=== modified file 'juju/errors.py'
--- juju/errors.py 2012-03-28 07:33:22 +0000
+++ juju/errors.py 2012-07-18 19:47:20 +0000
@@ -159,7 +159,7 @@
159 def __str__(self):159 def __str__(self):
160 return "Cannot find machine%s: %s" % (160 return "Cannot find machine%s: %s" % (
161 "" if len(self.instance_ids) == 1 else "s",161 "" if len(self.instance_ids) == 1 else "s",
162 ", ".join(self.instance_ids))162 ", ".join(map(str, self.instance_ids)))
163163
164164
165class ProviderInteractionError(ProviderError):165class ProviderInteractionError(ProviderError):
166166
=== modified file 'juju/providers/common/findzookeepers.py'
--- juju/providers/common/findzookeepers.py 2011-09-15 18:50:23 +0000
+++ juju/providers/common/findzookeepers.py 2012-07-18 19:47:20 +0000
@@ -36,5 +36,6 @@
3636
37 if machines:37 if machines:
38 returnValue(machines)38 returnValue(machines)
39
39 raise EnvironmentNotFound("machines are not running (%s)"40 raise EnvironmentNotFound("machines are not running (%s)"
40 % ", ".join(missing_instance_ids))41 % ", ".join(map(str, missing_instance_ids)))
4142
=== modified file 'juju/providers/ec2/tests/test_launch.py'
--- juju/providers/ec2/tests/test_launch.py 2012-07-05 21:49:12 +0000
+++ juju/providers/ec2/tests/test_launch.py 2012-07-18 19:47:20 +0000
@@ -67,7 +67,7 @@
67 def test_provider_launch(self):67 def test_provider_launch(self):
68 """68 """
69 The provider can be used to launch a machine with a minimal set of69 The provider can be used to launch a machine with a minimal set of
70 required packages, repositories, and and security groups.70 required packages, repositories, and security groups.
71 """71 """
72 self.ec2.describe_security_groups()72 self.ec2.describe_security_groups()
73 self.mocker.result(succeed([]))73 self.mocker.result(succeed([]))
7474
=== added directory 'juju/providers/openstack'
=== added file 'juju/providers/openstack/__init__.py'
--- juju/providers/openstack/__init__.py 1970-01-01 00:00:00 +0000
+++ juju/providers/openstack/__init__.py 2012-07-18 19:47:20 +0000
@@ -0,0 +1,3 @@
1"""Support for using OpenStack as a cloud provider for juju"""
2
3from .provider import MachineProvider
04
=== added file 'juju/providers/openstack/client.py'
--- juju/providers/openstack/client.py 1970-01-01 00:00:00 +0000
+++ juju/providers/openstack/client.py 2012-07-18 19:47:20 +0000
@@ -0,0 +1,470 @@
1"""Client for talking to OpenStack APIs using twisted
2
3This is not a complete implemention of all interfaces, just what juju needs.
4
5There is a fair bit of code cleanup and feature implementation to do here
6still.
7
8* Must check https certificates, can use code in txaws to do this.
9* Must support user/password authentication with keystone as well as keypair.
10* Want a ProviderInteractionError subclass that can include the extra details
11 returned in json form when something goes wrong and is raised by clients.
12* Request flow and json handling in general needs polish.
13* Need to prevent concurrent authentication attempts.
14* Need to limit concurrent http api requests to 4 or something reasonable,
15 can use DeferredSemaphore for this.
16* Should really have authentication retry logic in case the token expires.
17* Would be nice to use Agent keep alive support that twisted 12.1.0 added.
18"""
19
20import base64
21import json
22import logging
23import operator
24import urllib
25
26import twisted
27from twisted.internet import (
28 defer,
29 interfaces,
30 protocol,
31 reactor,
32 )
33from twisted.web import (
34 client,
35 http_headers,
36 )
37from zope.interface import implements
38
39from juju import errors
40
41
42log = logging.getLogger("juju.openstack")
43
44
45# Need the right juju version number, not exposed within python package.
46_USER_AGENT = "juju/%s twisted/%s" % ("0.5", twisted.__version__)
47
48
49class BytestringProducer(object):
50 """Wrap basic bytestring as a needlessly fancy twisted producer."""
51
52 implements(interfaces.IProducer)
53
54 def __init__(self, bytestring):
55 self.content = bytestring
56 self.length = len(bytestring)
57
58 def pauseProducing(self):
59 """Nothing to do if production is paused"""
60
61 def startProducing(self, consumer):
62 """Write entire contents when production starts"""
63 consumer.write(self.content)
64 return defer.succeed(None)
65
66 def stopProducing(self):
67 """Nothing to do when production halts"""
68
69
70class ResponseReader(protocol.Protocol):
71 """Protocol object suitable for use with Response.deliverBody
72
73 The 'onConnectionLost' deferred will be called back once the connection
74 is shut down with all the bytes from the body collected at that point.
75 """
76
77 def __init__(self):
78 self.onConnectionLost = defer.Deferred()
79
80 def connectionMade(self):
81 self.data = []
82
83 def dataReceived(self, data):
84 self.data.append(data)
85
86 def connectionLost(self, reason):
87 """Called on connection shut down
88
89 Here 'reason' can be one of ResponseDone, PotentialDataLost, or
90 ResponseFailed, but currently there is no fancy handling of these.
91 """
92 self.onConnectionLost.callback("".join(self.data))
93
94
95@defer.inlineCallbacks
96def request(method, url, extra_headers=(), body=None):
97 headers = http_headers.Headers({
98 # GZ 2012-07-03: Previously passed Accept: application/json header
99 # here, but not always the right thing. Bad for swift?
100 "User-Agent": [_USER_AGENT],
101 })
102 for header, value in extra_headers:
103 headers.setRawHeaders(header, [value])
104 if body is not None:
105 if isinstance(body, dict):
106 content_type = "application/json"
107 body = json.dumps(body)
108 elif isinstance(body, str):
109 content_type = "application/octet-stream"
110 headers.setRawHeaders("Content-Type", [content_type])
111 body = BytestringProducer(body)
112 response = yield client.Agent(reactor).request(method, url, headers, body)
113 if response.length == 0:
114 defer.returnValue((response, ""))
115 reader = ResponseReader()
116 response.deliverBody(reader)
117 body = yield reader.onConnectionLost
118 defer.returnValue((response, body))
119
120
121class _OpenStackClient(object):
122
123 def __init__(self, credentials):
124 self.credentials = credentials
125 log.debug("openstack: using auth-mode %r with %s", credentials.mode,
126 credentials.url)
127 if credentials.mode == "keypair":
128 self.authenticate = self.authenticate_v2_keypair
129 elif credentials.mode == "legacy":
130 self.authenticate = self.authenticate_v1
131 elif credentials.mode == "rax":
132 self.authenticate = self.authenticate_rax_auth
133 else:
134 self.authenticate = self.authenticate_v2_userpass
135 self.token = None
136
137 def _make_url(self, service, parts):
138 """Form full url from path components to service endpoint url"""
139 # GZ 2012-07-03: Need to ensure either services is populated or catch
140 # error here and propogate as one useful for users.
141 endpoint = self.services[service]
142 if not endpoint[-1] == "/":
143 endpoint += "/"
144 if isinstance(parts, str):
145 return endpoint + parts
146 quoted_parts = []
147 for part in parts:
148 if not isinstance(part, str):
149 part = urllib.quote(unicode(part).encode("utf-8"), "/~")
150 quoted_parts.append(part)
151 url = endpoint + "/".join(quoted_parts)
152 log.debug('access %s @ %s', service, url)
153 return url
154
155 @defer.inlineCallbacks
156 def authenticate_v1(self):
157 deferred = request(
158 "GET",
159 self.credentials.url,
160 extra_headers=[
161 ("X-Auth-User", self.credentials.username),
162 ("X-Auth-Key", self.credentials.access_key),
163 ],
164 )
165 response, body = yield deferred
166 if response.code != 204:
167 raise errors.ProviderInteractionError("Failed to authenticate")
168 # TODO: check response has right headers
169 [nova_url] = response.headers.getRawHeaders("X-Server-Management-Url")
170 self.services = {"compute": self.nova_url}
171 # No swift_url set as that is not supported
172 [self.token] = response.headers.getRawHeaders("X-Auth-Token")
173
174 def authenticate_v2_keypair(self):
175 deferred = request(
176 "POST",
177 self.credentials.url + "tokens",
178 body={"auth": {
179 "apiAccessKeyCredentials": {
180 "accessKey": self.credentials.access_key,
181 "secretKey": self.credentials.secret_key,
182 },
183 "tenantName": self.credentials.project_name,
184 }}
185 )
186 return deferred.addCallback(self._handle_v2_auth)
187
188 def authenticate_v2_userpass(self):
189 deferred = request(
190 "POST",
191 self.credentials.url + "tokens",
192 body={"auth": {
193 "passwordCredentials": {
194 "username": self.credentials.username,
195 "password": self.credentials.password,
196 },
197 "tenantName": self.credentials.project_name,
198 }}
199 )
200 return deferred.addCallback(self._handle_v2_auth)
201
202 def authenticate_rax_auth(self):
203 # openstack is not a product, but a kit for making snowflakes.
204 deferred = request(
205 "POST",
206 self.credentials.url + "tokens",
207 body={"auth": {
208 "RAX-KSKEY:apiKeyCredentials": {
209 "username": self.credentials.username,
210 "apiKey": self.credentials.password,
211 "tenantName": self.credentials.project_name}}}
212 )
213 return deferred.addCallback(self._handle_v2_auth)
214
215 def _handle_v2_auth(self, result):
216 access_details = self._json(result, 200, 'access')
217 token_details = access_details["token"]
218 self.token = token_details["id"]
219
220 # TODO: care about token_details["expires"]
221 # Don't need to we're not preserving tokens.
222 services = []
223 log.debug("openstack: authenticated til %r", token_details['expires'])
224 region = self.credentials.region
225 # HP cloud uses both az-1.region-a.geo-1 and region-a.geo-1 forms, not
226 # clear what should be in config or what the correct logic is.
227 if region is not None:
228 base_region = region.split('.', 1)[-1]
229 # GZ: 2012-07-03: Should split extraction of endpoints, add logging,
230 # and make more robust.
231 for catalog in access_details["serviceCatalog"]:
232 for endpoint in catalog["endpoints"]:
233 if region is not None and region != endpoint["region"]:
234 if base_region != endpoint["region"]:
235 continue
236 services.append((catalog["type"], str(endpoint["publicURL"])))
237 break
238
239 if not services:
240 raise errors.ProviderInteractionError("No suitable endpoints")
241
242 self.services = dict(services)
243
244 def is_authenticated(self):
245 return self.token is not None
246
247 @defer.inlineCallbacks
248 def authed_request(self, method, url, headers=None, body=None):
249 log.debug("openstack: %s %r", method, url)
250 request_headers = [("X-Auth-Token", self.token)]
251 if headers:
252 request_headers += headers
253 response, body = yield request(method, url, request_headers, body)
254 log.debug("openstack: %d %r", response.code, body)
255 defer.returnValue((response, body))
256
257 def _empty(self, result, code):
258 response, body = result
259 if response.code != code:
260 # XXX: This is a deeply unhelpful error, need context from request
261 raise errors.ProviderInteractionError("Unexpected %d: %r" % (
262 response.code, body))
263
264 def _json(self, result, code, root=None):
265 response, body = result
266 if response.code != code:
267 raise errors.ProviderInteractionError("Unexpected %d: %r" % (
268 response.code, body))
269 type_headers = response.headers.getRawHeaders("Content-Type")
270
271 found = False
272 for h in type_headers:
273 if 'application/json' in h:
274 found = True
275 if not found:
276 raise errors.ProviderInteractionError(
277 "Expected json response got %s" % type_headers)
278
279 data = json.loads(body)
280 if root is not None:
281 return data[root]
282 return data
283
284
285class _NovaClient(object):
286
287 def __init__(self, client):
288 self._client = client
289
290 @defer.inlineCallbacks
291 def request(self, method, parts, headers=None, body=None):
292 if not self._client.is_authenticated():
293 yield self._client.authenticate()
294 url = self._client._make_url("compute", parts)
295 result = yield self._client.authed_request(method, url, headers, body)
296 defer.returnValue(result)
297
298 def delete(self, parts, code=202):
299 deferred = self.request("DELETE", parts)
300 return deferred.addCallback(self._client._empty, code)
301
302 def get(self, parts, root, code=200):
303 deferred = self.request("GET", parts)
304 return deferred.addCallback(self._client._json, code, root)
305
306 def post(self, parts, jsonobj, root, code=200):
307 deferred = self.request("POST", parts, None, jsonobj) # XXX
308 return deferred.addCallback(self._client._json, code, root)
309
310 def post_no_data(self, parts, root, code=200):
311 deferred = self.request("POST", parts, None, "") # XXX
312 return deferred.addCallback(self._client._json, code, root)
313
314 def post_no_result(self, parts, jsonobj, code=202):
315 deferred = self.request("POST", parts, None, jsonobj) # XXX
316 return deferred.addCallback(self._client._empty, code)
317
318 def list_flavors(self):
319 return self.get("flavors", "flavors")
320
321 def get_server(self, server_id):
322 return self.get(["servers", server_id], "server")
323
324 # GZ 2012-05-31: Appending detail isn't a known path for some reason.
325 def get_server_detail(self, server_id):
326 return self.get(["servers", server_id, "detail"], "server")
327
328 def list_servers(self):
329 return self.get(["servers"], "servers")
330
331 def list_servers_detail(self):
332 return self.get(["servers", "detail"], "servers")
333
334 def delete_server(self, server_id):
335 return self.delete(["servers", server_id], code=204)
336
337 def run_server(self, image_id, flavor_id, name, security_group_names=None,
338 user_data=None):
339 server = {
340 'name': name,
341 'flavorRef': flavor_id,
342 'imageRef': image_id,
343 }
344 if user_data is not None:
345 server["user_data"] = base64.b64encode(user_data)
346 if security_group_names is not None:
347 server["security_groups"] = [{'name': n}
348 for n in security_group_names]
349 return self.post(["servers"], {'server': server},
350 root="server", code=202)
351
352 def get_server_security_groups(self, server_id):
353 d = self.get(
354 ["servers", server_id, "os-security-groups"],
355 root="security_groups")
356 # 2012-07-12: Workaround lack of this api in HP cloud
357 d.addErrback(
358 lambda f: self.get_server(server_id).addCallback(
359 operator.itemgetter("security_groups")))
360 return d
361
362 def list_security_groups(self):
363 return self.get(["os-security-groups"], "security_groups")
364
365 def create_security_group(self, name, description):
366 return self.post("os-security-groups", {
367 'security_group': {
368 'name': name,
369 'description': description,
370 }
371 },
372 root="security_group")
373
374 def delete_security_group(self, group_id):
375 return self.delete(["os-security-groups", group_id])
376
377 def add_security_group_rule(self, parent_group_id, **kwargs):
378 rule = {'parent_group_id': parent_group_id}
379 using_group = "group_id" in kwargs
380 if using_group:
381 rule['group_id'] = kwargs['group_id']
382 elif "cidr" in kwargs:
383 rule['cidr'] = kwargs['cidr']
384 if not using_group or "ip_protocol" in kwargs:
385 rule['ip_protocol'] = kwargs['ip_protocol']
386 rule['from_port'] = kwargs['from_port']
387 rule['to_port'] = kwargs['to_port']
388 return self.post("os-security-group-rules",
389 {'security_group_rule': rule},
390 root="security_group_rule")
391
392 def delete_security_group_rule(self, rule_id):
393 return self.delete(["os-security-group-rules", rule_id])
394
395 def add_server_security_group(self, server_id, group_name):
396 return self.post_no_result(["servers", server_id, "action"], {
397 "addSecurityGroup": {
398 "name": group_name,
399 }})
400
401 def remove_server_security_group(self, server_id, group_name):
402 return self.post_no_result(["servers", server_id, "action"], {
403 "removeSecurityGroup": {
404 "name": group_name,
405 }})
406
407 def list_floating_ips(self):
408 return self.get(["os-floating-ips"], "floating_ips")
409
410 def get_floating_ip(self, ip_id):
411 return self.get(["os-floating-ips", ip_id], "floating_ip")
412
413 def allocate_floating_ip(self):
414 return self.post_no_data(["os-floating-ips"], "floating_ip")
415
416 def delete_floating_ip(self, ip_id):
417 return self.delete(["os-floating-ips", ip_id])
418
419 def add_floating_ip(self, server_id, addr):
420 return self.post_no_result(["servers", server_id, "action"], {
421 'addFloatingIp': {
422 'address': addr,
423 }})
424
425 def remove_floating_ip(self, server_id, addr):
426 return self.post_no_result(["servers", server_id, "action"], {
427 'removeFloatingIp': {
428 'address': addr,
429 }})
430
431
432class _SwiftClient(object):
433
434 def __init__(self, client):
435 self._client = client
436
437 @defer.inlineCallbacks
438 def request(self, method, parts, headers=None, body=None):
439 if not self._client.is_authenticated():
440 yield self._client.authenticate()
441 url = self._client._make_url("object-store", parts)
442 result = yield self._client.authed_request(method, url, headers, body)
443 defer.returnValue(result)
444
445 def public_object_url(self, container, object_name):
446 if not self._client.is_authenticated():
447 raise ValueError("Need to have authenticated to get object url")
448 return self._client._make_url("object-store", [container, object_name])
449
450 def put_container(self, container_name):
451 # Juju expects there to be a (semi) public url for some objects. This
452 # could probably be more restrictive or placed in a seperate container
453 # with some refactoring, but for now just make everything public.
454 read_acl_header = ("X-Container-Read", ".r:*")
455 return self.request("PUT", [container_name], [read_acl_header], "")
456
457 def delete_container(self, container_name):
458 return self.request("DELETE", [container_name])
459
460 def head_object(self, container, object_name):
461 return self.request("HEAD", [container, object_name])
462
463 def get_object(self, container, object_name):
464 return self.request("GET", [container, object_name])
465
466 def delete_object(self, container, object_name):
467 return self.request("DELETE", [container, object_name])
468
469 def put_object(self, container, object_name, bytestring):
470 return self.request("PUT", [container, object_name], None, bytestring)
0471
=== added file 'juju/providers/openstack/credentials.py'
--- juju/providers/openstack/credentials.py 1970-01-01 00:00:00 +0000
+++ juju/providers/openstack/credentials.py 2012-07-18 19:47:20 +0000
@@ -0,0 +1,101 @@
1"""Handling of the credentials needed to authenticate with the OpenStack api
2
3Supports several different sets of credentials different auth modes need:
4* 'legacy' is built into nova and deprecated in favour of using keystone
5* 'keypair' works with the HP public cloud implemention of keystone
6* 'userpass' is the way keystone seems to want to do authentication generally
7"""
8
9import os
10
11
12class OpenStackCredentials(object):
13 """Encapsulation of credentials used to authenticate with OpenStack"""
14
15 _config_vars = {
16 'auth-url': ("OS_AUTH_URL", "NOVA_URL"),
17 'username': ("OS_USERNAME", "NOVA_USERNAME"),
18 'password': ("OS_PASSWORD", "NOVA_PASSWORD"),
19 # HP exposes both a numeric id and a name for tenants, passed back
20 # as tenantId and tenantName. Use the name only for simplicity.
21 'project-name': ("OS_TENANT_NAME", "NOVA_PROJECT_NAME",
22 "NOVA_PROJECT_ID"),
23 'region': ("OS_REGION_NAME", "NOVA_REGION_NAME", "NOVA_REGION"),
24 # The key variables don't seem to have modern OS_ prefixed aliases
25 'access-key': ("NOVA_API_KEY",),
26 'secret-key': ("EC2_SECRET_KEY", "AWS_SECRET_ACCESS_KEY"),
27 # A usable mode can normally be guessed, but may be configured
28 'auth-mode': (),
29 }
30
31 # Really, legacy auth could pass in the project id and keystone doesn't
32 # require it, but this is what the client expects for now.
33 _modes = {
34 'userpass': ('username', 'password', 'project-name'),
35 'rax': ('username', 'password', 'project-name'),
36 'keypair': ('access-key', 'secret-key', 'project-name'),
37 'legacy': ('username', 'access-key'),
38 }
39
40 _version_to_mode = {
41 "v2.0": 'userpass',
42 "v1.1": 'legacy',
43 "v1.0": 'legacy',
44 }
45
46 def __init__(self, creds_dict):
47 url = creds_dict.get("auth-url")
48 if not url:
49 raise ValueError("Missing config 'auth-url' for OpenStack api")
50 mode = creds_dict.get("auth-mode")
51 if mode is None:
52 mode = self._guess_auth_mode(url)
53 elif mode not in self._modes:
54 # The juju.environment.config layer should raise a pretty error
55 raise ValueError("Unknown 'auth-mode' value %r" % (self.mode,))
56 missing_keys = [key for key in self._modes[mode]
57 if not creds_dict.get(key)]
58 if missing_keys:
59 raise ValueError("Missing config %s required for %s auth" % (
60 ", ".join(map(repr, missing_keys)), mode))
61 self.url = url
62 self.mode = mode
63 for key in self._config_vars:
64 if key not in ("auth-url", "auth-mode"):
65 setattr(self, key.replace("-", "_"), creds_dict.get(key))
66
67 @classmethod
68 def _guess_auth_mode(cls, url):
69 """Pick a mode based on the version at the end of `url` given"""
70 final_part = url.rstrip("/").rsplit("/", 1)[-1]
71 try:
72 return cls._version_to_mode[final_part]
73 except KeyError:
74 raise ValueError(
75 "Missing config 'auth-mode' as unknown version"
76 " in 'auth-url' given: " + url)
77
78 @classmethod
79 def _get(cls, config, key):
80 """Retrieve `key` from `config` if present or in matching envvars"""
81 val = config.get(key)
82 if val is None:
83 for env_key in cls._config_vars[key]:
84
85 val = os.environ.get(env_key)
86 if val:
87 return val
88 return val
89
90 @classmethod
91 def from_environment(cls, config):
92 """Create credentials from `config` falling back to environment"""
93 return cls(dict((k, cls._get(config, k)) for k in cls._config_vars))
94
95 def set_config_defaults(self, data):
96 """Populate `data` with these credentials where not already set"""
97 for key in self._config_vars:
98 if key not in data:
99 val = getattr(self, key.replace("auth-", "").replace("-", "_"))
100 if val is not None:
101 data[key] = val
0102
=== added file 'juju/providers/openstack/files.py'
--- juju/providers/openstack/files.py 1970-01-01 00:00:00 +0000
+++ juju/providers/openstack/files.py 2012-07-18 19:47:20 +0000
@@ -0,0 +1,84 @@
1"""OpenStack provider file storage on Swift
2
3Basically a limited wrapper around the underlying api calls, with a few added
4quirks. There's some specific handling for 404 responses, raises FileNotFound
5on GET, and on PUT attempts to create the container then retries.
6
7Expects file-like objects for data. This isn't terribly useful as it doesn't
8fit well with the twisted model for chunking http data and most objects are
9small anyway.
10
11The main complication is the get_url method, which requires the generation of
12link that can be used by any http client to fetch a particular object without
13authentication. This is not possible in the general case in swift, however
14there are a few ways around the problem:
15
16* The stub nova-objectstore service doesn't check access anyway, see lp:947374
17* The tempurl swift middleware if enabled can do this with some advance setup
18* A container can have a more permissive ACL applying to all objects within
19
20All of these require touching the network to at least get the swift endpoint
21from the identity service, which as get_url doesn't return a deferred is
22problematic. In practice it's only used after putting an object however, so
23just raising if client has not yet been authenticated is good enough.
24"""
25
26from cStringIO import StringIO
27
28from twisted.internet import defer
29
30from juju import errors
31
32
33class FileStorage(object):
34 """Swift-backed :class:`FileStorage` abstraction"""
35
36 def __init__(self, swift, container):
37 self._swift = swift
38 self._container = container
39
40 def get_url(self, name):
41 return self._swift.public_object_url(self._container, name)
42
43 @defer.inlineCallbacks
44 def get(self, name):
45 """Get a file object from Swift.
46
47 :param unicode name: S3 key for the desired file
48
49 :return: an open file object
50 :rtype: :class:`twisted.internet.defer.Deferred`
51
52 :raises: :exc:`juju.errors.FileNotFound` if the file doesn't exist
53 """
54 response, body = yield self._swift.get_object(self._container, name)
55 if response.code == 404:
56 raise errors.FileNotFound(name)
57 if response.code != 200:
58 raise errors.ProviderInteractionError(
59 "Couldn't fetch object %r %r" % (response.code, body))
60 defer.returnValue(StringIO(body))
61
62 @defer.inlineCallbacks
63 def put(self, remote_path, file_object):
64 """Upload a file to Swift.
65
66 :param unicode remote_path: key on which to store the content
67
68 :param file_object: open file object containing the content
69
70 :rtype: :class:`twisted.internet.defer.Deferred`
71 """
72 data = file_object.read()
73 response, body = yield self._swift.put_object(self._container,
74 remote_path, data)
75 if response.code == 404:
76 response, body = yield self._swift.put_container(self._container)
77 if response.code != 201:
78 raise errors.ProviderInteractionError(
79 "Couldn't create container %r" % (self._container,))
80 response, body = yield self._swift.put_object(self._container,
81 remote_path, data)
82 if response.code != 201:
83 raise errors.ProviderInteractionError(
84 "Couldn't create object %r %r" % (response.code, remote_path))
085
=== added file 'juju/providers/openstack/launch.py'
--- juju/providers/openstack/launch.py 1970-01-01 00:00:00 +0000
+++ juju/providers/openstack/launch.py 2012-07-18 19:47:20 +0000
@@ -0,0 +1,131 @@
1"""Helpers for creating servers catered to Juju needs with OpenStack
2
3Specific notes:
4* Expects a public address for each machine, as that's what EC2 promises,
5 would be good to weaken this requirement.
6* Creates a per-machine security group in case need to poke ports open later,
7 as EC2 doesn't support changing groups later, but OpenStack does.
8* Needs to tell cloud-init how to get server id, currently in essex metadata
9 service gives i-08x style only, so cheat and use filestorage.
10* Config must specify a flavor, as these vary between Openstack deployments.
11 Working out constraints should resolve this.
12* Config must specify an image id, as there's no standard way of looking up
13 from distro series across clouds yet in essex.
14* Would be really nice to put the service name in the server name, but it's
15 not passed down into LaunchMachine currently.
16
17There are some race issues with the current setup:
18* Storing of server id needs to complete before cloud-init does the lookup.
19* A floating ip may be assigned to another server before the current one
20 finishes launching and can use the available ip itself.
21"""
22
23from cStringIO import StringIO
24
25from twisted.internet import (
26 defer,
27 reactor,
28 )
29
30from juju.errors import ProviderError, ProviderInteractionError
31from juju.lib import twistutils
32from juju.providers.common.launch import LaunchMachine
33
34from .machine import machine_from_instance, get_server_status
35
36from .client import log
37
38
39class NovaLaunchMachine(LaunchMachine):
40 """OpenStack Nova operation for creating a server"""
41
42 _DELAY_FOR_ADDRESSES = 5 # seconds
43
44 @defer.inlineCallbacks
45 def start_machine(self, machine_id, zookeepers):
46 """Actually launch an instance on Nova.
47
48 :param str machine_id: the juju machine ID to assign
49
50 :param zookeepers: the machines currently running zookeeper, to which
51 the new machine will need to connect
52 :type zookeepers: list of
53 :class:`juju.providers.openstack.machine.NovaProviderMachine`
54
55 :return: a singe-entry list containing a
56 :class:`juju.providers.openstack.machine.NovaProviderMachine`
57 representing the newly-launched machine
58 :rtype: :class:`twisted.internet.defer.Deferred`
59 """
60 cloud_init = self._create_cloud_init(machine_id, zookeepers)
61 cloud_init.set_provider_type(self._provider.provider_type)
62 filestorage = self._provider.get_file_storage()
63 # Only the master is required to get its own instance id like this.
64 if self._master:
65 id_name = "juju_master_id"
66 cloud_init.set_instance_id_accessor("$(curl %s)" % (
67 filestorage.get_url(id_name),))
68 user_data = cloud_init.render()
69
70 # For openstack deployments, really need image id configured as there
71 # are no standards to provide a fallback value.
72 image_id = self._provider.config.get("default-image-id")
73 if image_id is None:
74 raise ProviderError("Need to specify a default-image-id")
75
76 security_groups = (
77 yield self._provider.port_manager.ensure_groups(machine_id))
78
79 # Until constraints are implemented, need to a configured instance
80 # type and resolve it to a flavor id here.
81 flavor_name = self._provider.config.get("default-instance-type")
82 if flavor_name is None:
83 raise ProviderError("Need to specify a default-instance-type")
84 flavors = yield self._provider.nova.list_flavors()
85 flavor_map = dict((f['name'], f['id']) for f in flavors)
86 if not flavor_name in flavor_map:
87 ProviderError("Unknown instance type given: %r" % (flavor_name,))
88
89 server = yield self._provider.nova.run_server(
90 name="juju %s instance %s" %
91 (self._provider.environment_name, machine_id,),
92 image_id=image_id,
93 flavor_id=flavor_map[flavor_name],
94 security_group_names=security_groups,
95 user_data=user_data)
96
97 if self._master:
98 yield filestorage.put(id_name, StringIO(str(server['id'])))
99
100 # For private clouds allow an option of attaching public
101 # floating ips to all the machines. None of the extant public
102 # clouds need this.
103 if self._provider.config.get('use-floating-ip'):
104 # Not possible to attach a floating ip to a newly booted
105 # server, must wait for networking to be ready when some
106 # kind of address exists.
107 while not server.get('addresses'):
108 status = get_server_status(server)
109 if status != "pending":
110 raise ProviderInteractionError(
111 "Server out of pending status "
112 "without addresses set: %r" % server)
113 # Bad, bad place to be doing a wait loop directly
114 yield twistutils.sleep(self._DELAY_FOR_ADDRESSES)
115 log.debug("Waited for %d seconds for networking on server %r",
116 self._DELAY_FOR_ADDRESSES, server['id'])
117 server = yield self._provider.nova.get_server(server['id'])
118 yield _assign_floating_ip(self._provider, server['id'])
119
120 defer.returnValue([machine_from_instance(server)])
121
122
123@defer.inlineCallbacks
124def _assign_floating_ip(provider, server_id):
125 floating_ips = yield provider.nova.list_floating_ips()
126 for floating_ip in floating_ips:
127 if floating_ip['instance_id'] is None:
128 break
129 else:
130 floating_ip = yield provider.nova.allocate_floating_ip()
131 yield provider.nova.add_floating_ip(server_id, floating_ip['ip'])
0132
=== added file 'juju/providers/openstack/machine.py'
--- juju/providers/openstack/machine.py 1970-01-01 00:00:00 +0000
+++ juju/providers/openstack/machine.py 2012-07-18 19:47:20 +0000
@@ -0,0 +1,68 @@
1"""Helpers for mapping Nova api results to the juju machine abstraction"""
2
3from juju.machine import ProviderMachine
4
5
6_SERVER_STATE_MAP = {
7 None: 'pending',
8 'ACTIVE': 'running',
9 'BUILD': 'pending',
10 'BUILDING': 'pending',
11 'REBUILDING': 'pending',
12 'DELETED': 'terminated',
13 'STOPPED': 'stopped',
14 }
15
16
17class NovaProviderMachine(ProviderMachine):
18 """Nova-specific ProviderMachine implementation"""
19
20
21def get_server_status(server):
22 status = server.get('status')
23 if status is not None and "(" in status:
24 status = status.split("(", 1)[0]
25 return _SERVER_STATE_MAP.get(status, 'unknown')
26
27
28def get_server_addresses(server):
29 private_addr = public_addr = None
30 addresses = server.get("addresses")
31 if addresses is not None:
32 # Issue with some setups, have custom network only, use as private
33 network = ()
34 for name in sorted(addresses):
35 if name not in ("private", "public"):
36 network = addresses[name]
37 if network:
38 break
39 network = addresses.get("private", network)
40 for address in network:
41 if address.get("version", 0) == 4:
42 private_addr = address['addr']
43 break
44 # Issue with HP cloud, public address is second in private network
45 network = addresses.get("public", network[1:])
46 for address in network:
47 if address.get("version", 0) == 4:
48 public_addr = address['addr']
49 return private_addr, public_addr
50
51
52def machine_from_instance(server):
53 """Create an :class:`NovaProviderMachine` from a server details dict
54
55 :param server: a dictionary of server info as given by the Nova api
56
57 :return: a matching :class:`NovaProviderMachine`
58 """
59 private_addr, public_addr = get_server_addresses(server)
60 # Juju assumes it always needs a public address and loops waiting for one.
61 # In fact a private address is generally fine provided it can be sshed to.
62 if public_addr is None and private_addr is not None:
63 public_addr = private_addr
64 return NovaProviderMachine(
65 server['id'],
66 public_addr,
67 private_addr,
68 get_server_status(server))
069
=== added file 'juju/providers/openstack/ports.py'
--- juju/providers/openstack/ports.py 1970-01-01 00:00:00 +0000
+++ juju/providers/openstack/ports.py 2012-07-18 19:47:20 +0000
@@ -0,0 +1,179 @@
1"""Manage port access to machines using Nova security group extension
2
3The mechanism is based on the existing scheme used by the EC2 provider.
4
5Each machine is launched with two security groups, a juju group that is shared
6across all machines and allows access to 22/tcp for ssh, and a machine group
7just for that server so ports can be opened and closed on an individual level.
8
9There is some mismatch between the port hole poking and security group models:
10* A new security group is created for every machine
11* Rules are not shared between service units but set up again each launch
12* Support for port ranges is not exposed
13
14The Nova security group module follows the EC2 example quite closely, but as
15of Essex it's still under contrib and has a number of quirks:
16* To run a server with, or add or remove groups from a server, 'name' is used
17* To get details, delete, or add or remove rules from a group, 'id' is needed
18
19The only way of getting 'id' if 'name' is known is by listing all groups then
20looking at the details of the one with the matching name.
21"""
22
23from twisted.internet import (
24 defer,
25 )
26
27from juju import errors
28
29from .client import log
30
31
32class NovaPortManager(object):
33 """Mapping of port-based juju interface to Nova security group actions
34
35 There is the potential to record some state on the instance to reduce api
36 round-trips when, for instance, launching multiple machines at once, but
37 for now
38 """
39
40 def __init__(self, nova, environment_name):
41 self.nova = nova
42 self.tag = environment_name
43
44 def _juju_group_name(self):
45 return "juju-%s" % (self.tag,)
46
47 def _machine_group_name(self, machine_id):
48 return "juju-%s-%s" % (self.tag, machine_id)
49
50 @defer.inlineCallbacks
51 def _get_machine_group(self, machine, machine_id):
52 """Get details of the machine specific security group
53
54 As only the name of the group can be derived, this means listing every
55 security group for that server and seeing which has a matching name.
56 """
57 group_name = self._machine_group_name(machine_id)
58 server_id = machine.instance_id
59 groups = yield self.nova.get_server_security_groups(server_id)
60 for group in groups:
61 if group['name'] == group_name:
62 defer.returnValue(group)
63 raise errors.ProviderInteractionError(
64 "Missing security group %r for machine %r" %
65 (group_name, server_id))
66
67 @defer.inlineCallbacks
68 def open_port(self, machine, machine_id, port, protocol="tcp"):
69 """Allow access to a port for the given machine only"""
70 group = yield self._get_machine_group(machine, machine_id)
71 yield self.nova.add_security_group_rule(group['id'],
72 ip_protocol=protocol, from_port=port, to_port=port)
73 log.debug("Opened %s/%s on machine %r",
74 port, protocol, machine.instance_id)
75
76 @defer.inlineCallbacks
77 def close_port(self, machine, machine_id, port, protocol="tcp"):
78 """Revoke access to a port for the given machine only"""
79 group = yield self._get_machine_group(machine, machine_id)
80 for rule in group["rules"]:
81 if (port == rule["from_port"] == rule["to_port"] and
82 rule["ip_protocol"] == protocol):
83 yield self.nova.delete_security_group_rule(rule["id"])
84 log.debug("Closed %s/%s on machine %r",
85 port, protocol, machine.instance_id)
86 return
87 raise errors.ProviderInteractionError(
88 "Couldn't close unopened %s/%s on machine %r",
89 port, protocol, machine.instance_id)
90
91 @defer.inlineCallbacks
92 def get_opened_ports(self, machine, machine_id):
93 """Get a set of opened port/protocol pairs for a machine"""
94 group = yield self._get_machine_group(machine, machine_id)
95 opened_ports = set()
96 for rule in group.get("rules", []):
97 if not rule.get("group"):
98 protocol = rule["ip_protocol"]
99 from_port = rule["from_port"]
100 to_port = rule["to_port"]
101 if from_port == to_port:
102 opened_ports.add((from_port, protocol))
103 defer.returnValue(opened_ports)
104
105 @defer.inlineCallbacks
106 def ensure_groups(self, machine_id):
107 """Get names of the security groups for a machine, creating if needed
108
109 If the juju group already exists, it is assumed to be correctly set up.
110 If the machine group already exists, it is deleted then recreated.
111 """
112 security_groups = yield self.nova.list_security_groups()
113 groups_by_name = dict((sg['name'], sg['id']) for sg in security_groups)
114
115 juju_group = self._juju_group_name()
116 if not juju_group in groups_by_name:
117 log.debug("Creating juju security group %s", juju_group)
118 sg = yield self.nova.create_security_group(juju_group,
119 "juju group for %s" % (self.tag,))
120 # Add external ssh access
121 yield self.nova.add_security_group_rule(sg['id'],
122 ip_protocol="tcp", from_port=22, to_port=22)
123 # Add internal group access
124 yield self.nova.add_security_group_rule(
125 parent_group_id=sg['id'], group_id=sg['id'],
126 ip_protocol="tcp", from_port=1, to_port=65535)
127
128 machine_group = self._machine_group_name(machine_id)
129 if machine_group in groups_by_name:
130 yield self.nova.delete_security_group(
131 groups_by_name[machine_group])
132 log.debug("Creating machine security group %s", machine_group)
133 yield self.nova.create_security_group(machine_group,
134 "juju group for %s machine %s" % (self.tag, machine_id))
135
136 defer.returnValue([juju_group, machine_group])
137
138 @defer.inlineCallbacks
139 def get_machine_groups(self, machine, with_juju_group=False):
140 try:
141 ret = yield self.get_machine_groups_pure(machine, with_juju_group)
142 except errors.ProviderInteractionError, e:
143 # XXX: Need to wire up treatment of 500s properly in client
144 if getattr(e, "kind", None) == "computeError":
145 try:
146 yield self.nova.get_server(machine.instance_id)
147 except errors.ProviderInteractionError, e:
148 pass # just rebinding e
149 if True or getattr(e, "kind", None) == "itemNotFound":
150 defer.returnValue(None)
151 raise
152 defer.returnValue(ret)
153
154 @defer.inlineCallbacks
155 def get_machine_groups_pure(self, machine, with_juju_group=False):
156 server_id = machine.instance_id
157 groups = yield self.nova.get_server_security_groups(server_id)
158 juju_group = self._juju_group_name()
159 groups_by_name = dict((g['name'], g['id']) for g in groups
160 if g['name'].startswith(juju_group))
161 if juju_group not in groups_by_name:
162 # Not a juju machine, shouldn't touch
163 defer.returnValue(None)
164 if not with_juju_group:
165 groups_by_name.pop(juju_group)
166 # else assumption: only one remaining group, is the machine group
167 defer.returnValue(groups_by_name)
168
169 @defer.inlineCallbacks
170 def delete_juju_group(self):
171 security_groups = yield self.nova.list_security_groups()
172 juju_group = self._juju_group_name()
173 for group in security_groups:
174 if group['name'] == juju_group:
175 break
176 else:
177 log.debug("Can't delete missing juju group")
178 return
179 yield self.nova.delete_security_group(group['id'])
0180
=== added file 'juju/providers/openstack/provider.py'
--- juju/providers/openstack/provider.py 1970-01-01 00:00:00 +0000
+++ juju/providers/openstack/provider.py 2012-07-18 19:47:20 +0000
@@ -0,0 +1,189 @@
1"""Provider interface implementation for OpenStack backend
2
3Much of the logic is implemented in sibling modules, but the overall model is
4exposed here.
5
6Still in need of work here:
7* Implement constraints using the Nova flavors api. This will always mean an
8 api call rather than hard coding values as is done with EC2. Things like
9 memory and cpu count are broadly equivalent, but there's no guarentee what
10 details are exposed and ranking by price will generally not be an option.
11"""
12
13import logging
14
15from twisted.internet import defer
16
17from juju import errors
18from juju.providers.common.base import MachineProviderBase
19
20from .client import _OpenStackClient, _NovaClient, _SwiftClient
21from . import credentials
22from .files import FileStorage
23from .launch import NovaLaunchMachine
24from .machine import (
25 NovaProviderMachine, get_server_status, machine_from_instance
26 )
27from .ports import NovaPortManager
28
29
30log = logging.getLogger("juju.openstack")
31
32
33class MachineProvider(MachineProviderBase):
34 """MachineProvider for use in an OpenStack environment"""
35
36 Credentials = credentials.OpenStackCredentials
37
38 def __init__(self, environment_name, config):
39 super(MachineProvider, self).__init__(environment_name, config)
40 self.credentials = self.Credentials.from_environment(config)
41 client = _OpenStackClient(self.credentials)
42 self.nova = _NovaClient(client)
43 self.swift = _SwiftClient(client)
44 self.port_manager = NovaPortManager(self.nova, environment_name)
45
46 @property
47 def provider_type(self):
48 return "openstack"
49
50 def get_serialization_data(self):
51 """Get provider configuration suitable for serialization.
52
53 Also fills in credential information that may have earlier been
54 extracted from the environment.
55 """
56 data = super(MachineProvider, self).get_serialization_data()
57 self.credentials.set_config_defaults(data)
58 return data
59
60 def get_file_storage(self):
61 """Retrieve a Swift-backed :class:`FileStorage`."""
62 return FileStorage(self.swift, self.config["control-bucket"])
63
64 def start_machine(self, machine_data, master=False):
65 """Start an OpenStack machine.
66
67 :param dict machine_data: desired characteristics of the new machine;
68 it must include a "machine-id" key, and may include a "constraints"
69 key to specify the underlying OS and hardware.
70
71 :param bool master: if True, machine will initialize the juju admin
72 and run a provisioning agent, in addition to running a machine
73 agent.
74 """
75 return NovaLaunchMachine.launch(self, machine_data, master)
76
77 @defer.inlineCallbacks
78 def get_machines(self, instance_ids=()):
79 """List machines running in the provider.
80
81 :param list instance_ids: ids of instances you want to get. Leave empty
82 to list every
83 :class:`juju.providers.openstack.machine.NovaProviderMachine` owned
84 by this provider.
85
86 :return: a list of
87 :class:`juju.providers.openstack.machine.NovaProviderMachine`
88 instances
89 :rtype: :class:`twisted.internet.defer.Deferred`
90
91 :raises: :exc:`juju.errors.MachinesNotFound`
92 """
93 if len(instance_ids) == 1:
94 try:
95 instances = [(yield self.nova.get_server(instance_ids[0]))]
96 except errors.ProviderInteractionError, e:
97 # XXX: Need to wire up treatment of 404s properly in client
98 if True or getattr(e, "kind", None) == "itemNotFound":
99 raise errors.MachinesNotFound(set(instance_ids))
100 raise
101 instance_ids = frozenset(instance_ids)
102 else:
103 instances = yield self.nova.list_servers()
104 if instance_ids:
105 instance_ids = frozenset(instance_ids)
106 instances = [instance for instance in instances
107 if instance['id'] in instance_ids]
108
109 # Only want to deal with servers that were created by juju, checking
110 # the name begins with the prefix launch uses is good enough.
111 name_prefix = "juju %s instance " % (self.environment_name,)
112 machines = []
113 for instance in instances:
114 if (instance['name'].startswith(name_prefix) and
115 get_server_status(instance) in ("running", "pending")):
116 machines.append(machine_from_instance(instance))
117
118 if instance_ids:
119 # We were asked for a specific list of machines, and if we can't
120 # completely fulfil that request we should blow up.
121 missing = instance_ids.difference(m.instance_id for m in machines)
122 if missing:
123 raise errors.MachinesNotFound(missing)
124
125 defer.returnValue(machines)
126
127 @defer.inlineCallbacks
128 def _delete_machine(self, machine, full=False):
129 server_id = machine.instance_id
130 server = yield self.nova.get_server(server_id)
131 if not server['name'].startswith(
132 "juju %s instance" % self.environment_name):
133 raise errors.MachinesNotFound(set([machine.instance_id]))
134 yield self.nova.delete_server(server_id)
135 defer.returnValue(machine)
136
137 def shutdown_machine(self, machine):
138 if not isinstance(machine, NovaProviderMachine):
139 raise errors.ProviderError(
140 "Need a NovaProviderMachine to shutdown not: %r" % (machine,))
141 # EC2 provider re-gets the machine to see if it's still in existance
142 # and can be shutdown, instead just handle an error? 404-ish?
143 return self._delete_machine(machine)
144
145 @defer.inlineCallbacks
146 def destroy_environment(self):
147 """Terminate all associated machines and security groups.
148
149 The super defintion of this method terminates each machine in
150 the environment; this needs to be augmented here by also
151 removing the security group for the environment.
152
153 :rtype: :class:`twisted.internet.defer.Deferred`
154 """
155 machines = yield self.get_machines()
156 deleted_machines = yield defer.gatherResults(
157 [self._delete_machine(m, True) for m in machines])
158 yield self.save_state({})
159 defer.returnValue(deleted_machines)
160
161 def shutdown_machines(self, machines):
162 """Terminate machines associated with this provider.
163
164 :param machines: machines to shut down
165 :type machines: list of
166 :class:`juju.providers.openstack.machine.NovaProviderMachine`
167
168 :return: list of terminated
169 :class:`juju.providers.openstack.machine.NovaProviderMachine`
170 instances
171 :rtype: :class:`twisted.internet.defer.Deferred`
172 """
173 # XXX: need to actually handle errors as non-terminated machines
174 # and not include them in the resulting list
175 return defer.gatherResults(
176 [self.shutdown_machine(m) for m in machines], consumeErrors=True)
177
178 def open_port(self, machine, machine_id, port, protocol="tcp"):
179 """Authorizes `port` using `protocol` on EC2 for `machine`."""
180 return self.port_manager.open_port(machine, machine_id, port, protocol)
181
182 def close_port(self, machine, machine_id, port, protocol="tcp"):
183 """Revokes `port` using `protocol` on EC2 for `machine`."""
184 return self.port_manager.close_port(
185 machine, machine_id, port, protocol)
186
187 def get_opened_ports(self, machine, machine_id):
188 """Returns a set of open (port, proto) pairs for `machine`."""
189 return self.port_manager.get_opened_ports(machine, machine_id)
0190
=== added directory 'juju/providers/openstack/tests'
=== added file 'juju/providers/openstack/tests/__init__.py'
--- juju/providers/openstack/tests/__init__.py 1970-01-01 00:00:00 +0000
+++ juju/providers/openstack/tests/__init__.py 2012-07-18 19:47:20 +0000
@@ -0,0 +1,206 @@
1"""Infrastructure shared by all tests for OpenStack provider"""
2
3import json
4
5from twisted.internet import (
6 defer,
7 )
8from twisted.web import (
9 http,
10 http_headers,
11 )
12
13from juju import errors
14from juju.lib import (
15 mocker,
16 )
17
18from juju.providers.openstack import (
19 client,
20 files,
21 machine,
22 ports,
23 provider,
24 )
25
26
27class FakeResponse(object):
28
29 def __init__(self, code):
30 if code not in http.RESPONSES:
31 raise ValueError("Unknown http code: %r" % (code,))
32 self.code = code
33 self.headers = http_headers.Headers(
34 {"Content-Type": ["application/json"]})
35
36
37class OpenStackTestMixin(object):
38 """Helpers for test classes that want to exercise the OpenStack APIs
39
40 This goes all the way down to the http layer, which is really a little too
41 low for most of what the test cases want to assert. With some cleanups to
42 the client code, tests could instead mock out client methods.
43 """
44
45 environment_name = "testing"
46 api_url = "http://test.invalid/v2.0/"
47
48 def setUp(self):
49 self._mock_request = self.mocker.replace(
50 "juju.providers.openstack.client.request",
51 passthrough=False)
52 self.mocker.order()
53 self.nova_url = self.api_url + "nova"
54 self.swift_url = self.api_url + "swift"
55 self._mock_request("POST", self.api_url + "tokens", body=mocker.ANY)
56 self.mocker.result(defer.succeed((FakeResponse(200),
57 json.dumps({"access": {
58 "token": {"id": "tok", "expires": "2030-01-01T00:00:00"},
59 "serviceCatalog": [
60 {
61 "type": "compute",
62 "endpoints": [{"publicURL": self.nova_url}]
63 },
64 {
65 "type": "object-store",
66 "endpoints": [{"publicURL": self.swift_url}]
67 }
68 ]
69 }}))))
70 # Clear the environment so provider won't pick up config from envvars
71 self.change_environment()
72
73 def get_config(self):
74 return {
75 "type": "openstack",
76 "admin-secret": "password",
77 "access-key": "90abcdef",
78 "secret-key": "xxxxxxxx",
79 "auth-mode": "keypair",
80 "auth-url": self.api_url,
81 "project-name": "test_project",
82 "control-bucket": self.environment_name,
83 "default-instance-type": "standard.xsmall",
84 "default-image-id": 42,
85 "use-floating-ip": True,
86 }
87
88 def get_provider(self):
89 """Return the openstack machine provider.
90
91 This should only be invoked after mocker is in replay mode.
92 """
93 return provider.MachineProvider(self.environment_name,
94 self.get_config())
95
96 def make_server(self, server_id, name=None, status="ACTIVE"):
97 if name is None:
98 # Would be machine id rather than server id really but will do.
99 name = "juju testing instance " + str(server_id)
100 return {
101 "id": server_id,
102 "name": name,
103 "status": status,
104 "addresses": {},
105 }
106
107 def assert_not_found(self, deferred, server_ids):
108 self.assertFailure(deferred, errors.MachinesNotFound)
109 return deferred.addCallback(self._check_not_found, server_ids)
110
111 def _check_not_found(self, error, server_ids):
112 self.assertEqual(error.instance_ids, server_ids)
113
114 def _mock_nova(self, method, path, body):
115 self._mock_request(method,
116 "%s/%s" % (self.nova_url, path),
117 [("X-Auth-Token", "tok")],
118 body)
119
120 def _mock_swift(self, method, path, body, extra_headers=None):
121 headers = [("X-Auth-Token", "tok")]
122 if extra_headers is not None:
123 headers += extra_headers
124 url = "%s/%s" % (self.swift_url, path)
125 self._mock_request(method, url, headers, body)
126
127 def expect_nova_get(self, path, code=200, response=""):
128 self._mock_nova("GET", path, None)
129 if not isinstance(response, str):
130 response = json.dumps(response)
131 self.mocker.result(defer.succeed((FakeResponse(code), response)))
132
133 def expect_nova_post(self, path, body, code=200, response=""):
134 self._mock_nova("POST", path, body)
135 if not isinstance(response, str):
136 response = json.dumps(response)
137 self.mocker.result(defer.succeed((FakeResponse(code), response)))
138
139 def expect_nova_delete(self, path, code=202, response=""):
140 self._mock_nova("DELETE", path, None)
141 self.mocker.result(defer.succeed((FakeResponse(code), response)))
142
143 def expect_swift_get(self, path, code=200, response=""):
144 self._mock_swift("GET", path, None)
145 self.mocker.result(defer.succeed((FakeResponse(code), response)))
146
147 def expect_swift_put(self, path, body, code=201, response=""):
148 self._mock_swift("PUT", path, body)
149 self.mocker.result(defer.succeed((FakeResponse(code), response)))
150
151 def expect_swift_put_container(self, container, code=201, response=""):
152 self._mock_swift("PUT", container, "", [('X-Container-Read', '.r:*')])
153 self.mocker.result(defer.succeed((FakeResponse(code), response)))
154
155
156class MockedProvider(object):
157
158 provider_type = "openstack"
159 environment_name = "testing"
160 api_url = "http://testing.invalid/2.0/"
161
162 default_config = {
163 "type": "openstack",
164 "admin-secret": "asecret",
165 "username": "auser",
166 "password": "xxxxxxxx",
167 "auth-url": api_url,
168 "project-name": "aproject",
169 "control-bucket": environment_name,
170 "default-instance-type": "standard.xsmall",
171 "default-image-id": 42,
172 }
173
174 def __init__(self, mocker, config=None):
175 if config is None:
176 config = dict(self.default_config)
177 self.config = config
178 self.nova = mocker.proxy(client._NovaClient(None), passthrough=False)
179 self.swift = mocker.proxy(client._SwiftClient(None), passthrough=False)
180 self.port_manager = mocker.proxy(
181 ports.NovaPortManager(self.nova, self.environment_name))
182 self.provider_actions = mocker.mock()
183 self.mocker = mocker
184
185 def get_file_storage(self):
186 return files.FileStorage(self.swift, self.config['control-bucket'])
187
188 def __getattr__(self, attr):
189 return getattr(self.provider_actions, attr)
190
191 def expect_swift_public_object_url(self, object_name):
192 container_name = self.config['control-bucket']
193 event = self.swift.public_object_url(container_name, object_name)
194 self.mocker.result("%sswift/%s" % (self.api_url, object_name))
195 return event
196
197 def expect_swift_put(self, object_name, body, code=201, response=""):
198 container_name = self.config['control-bucket']
199 event = self.swift.put_object(container_name, object_name, body)
200 self.mocker.result(defer.succeed((FakeResponse(code), response)))
201 return event
202
203 def expect_zookeeper_machines(self, iid):
204 event = self.provider_actions.get_zookeeper_machines()
205 self.mocker.result(defer.succeed([machine.NovaProviderMachine(iid)]))
206 return event
0207
=== added file 'juju/providers/openstack/tests/test_bootstrap.py'
--- juju/providers/openstack/tests/test_bootstrap.py 1970-01-01 00:00:00 +0000
+++ juju/providers/openstack/tests/test_bootstrap.py 2012-07-18 19:47:20 +0000
@@ -0,0 +1,213 @@
1"""Tests for bootstrapping juju on openstack
2
3Bootstrap touches a lot of the other parts of the provider, including machine
4launching, security groups and so on. Testing this in an end-to-end fashion as
5is done currently duplicates many checks from other more focussed tests.
6"""
7
8import logging
9import yaml
10
11from juju.lib import (
12 mocker,
13 testing,
14 )
15from juju.machine import constraints
16
17from juju.providers.openstack.machine import NovaProviderMachine
18
19from juju.providers.openstack.tests import OpenStackTestMixin
20
21
22class OpenStackBootstrapTest(OpenStackTestMixin, testing.TestCase):
23
24 def expect_verify(self):
25 self.expect_swift_put("testing/bootstrap-verify",
26 "storage is writable")
27
28 def expect_provider_state_fresh(self):
29 self.expect_swift_get("testing/provider-state")
30 self.expect_verify()
31
32 def expect_create_group(self):
33 self.expect_nova_post("os-security-groups",
34 {'security_group': {
35 'name': 'juju-testing',
36 'description': 'juju group for testing',
37 }},
38 response={'security_group': {
39 'id': 1,
40 }})
41 self.expect_nova_post("os-security-group-rules",
42 {'security_group_rule': {
43 'parent_group_id': 1,
44 'ip_protocol': "tcp",
45 'from_port': 22,
46 'to_port': 22,
47 }},
48 response={'security_group_rule': {
49 'id': 144, 'parent_group_id': 1,
50 }})
51 self.expect_nova_post("os-security-group-rules",
52 {'security_group_rule': {
53 'parent_group_id': 1,
54 'group_id': 1,
55 'ip_protocol': "tcp",
56 'from_port': 1,
57 'to_port': 65535,
58 }},
59 response={'security_group_rule': {
60 'id': 145, 'parent_group_id': 1,
61 }})
62
63 def expect_create_machine_group(self, machine_id):
64 machine = str(machine_id)
65 self.expect_nova_post("os-security-groups",
66 {'security_group': {
67 'name': 'juju-testing-' + machine,
68 'description': 'juju group for testing machine ' + machine,
69 }},
70 response={'security_group': {
71 'id': 2,
72 }})
73
74 def _match_server(self, data):
75 userdata = data['server'].pop('user_data').decode("base64")
76 self.assertEqual("#cloud-config", userdata.split("\n", 1)[0])
77 cloud_init = yaml.load(userdata)
78 # TODO: assertions on cloud-init content
79 self.assertEqual({'server': {
80 'flavorRef': 1,
81 'imageRef': 42,
82 'name': 'juju testing instance 0',
83 'security_groups': [
84 {'name': 'juju-testing'},
85 {'name': 'juju-testing-0'},
86 ],
87 }},
88 data)
89 return True
90
91 def expect_launch(self):
92 self.expect_nova_get("flavors",
93 response={'flavors': [{'id': 1, 'name': "standard.xsmall"}]})
94 self.expect_nova_post("servers", mocker.MATCH(self._match_server),
95 code=202, response={'server': {
96 'id': '1000',
97 'status': "PENDING",
98 'addresses': {'private': [{'version': 4, 'addr': "4.4.4.4"}]},
99 }})
100 self.expect_swift_put("testing/juju_master_id", "1000")
101 self.expect_nova_get("os-floating-ips",
102 response={'floating_ips': [
103 {'id': 80, 'instance_id': None, 'ip': "8.8.8.8"}
104 ]})
105 self.expect_nova_post("servers/1000/action",
106 {"addFloatingIp": {"address": "8.8.8.8"}}, code=202)
107 self.expect_swift_put("testing/provider-state",
108 yaml.dump({'zookeeper-instances': ['1000']}))
109
110 def _check_machine(self, machine_list):
111 [machine] = machine_list
112 self.assertTrue(isinstance(machine, NovaProviderMachine))
113 self.assertEqual(machine.instance_id, '1000')
114
115 def bootstrap(self):
116 provider = self.get_provider()
117 constraint_set = constraints.ConstraintSet(provider.provider_type)
118 return provider.bootstrap(constraint_set.load({}))
119
120 def test_bootstrap_clean(self):
121 """Bootstrap from a clean slate makes groups and zookeeper instance"""
122 self.expect_provider_state_fresh()
123 self.expect_nova_get("os-security-groups",
124 response={'security_groups': []})
125 self.expect_create_group()
126 self.expect_create_machine_group(0)
127 self.expect_launch()
128 self.mocker.replay()
129
130 log = self.capture_logging("juju.common", level=logging.DEBUG)
131 deferred = self.bootstrap()
132 deferred.addCallback(self._check_machine)
133 def check_log(_):
134 log_text = log.getvalue()
135 self.assertIn("Launching juju bootstrap instance", log_text)
136 self.assertNotIn("previously bootstrapped", log_text)
137 return deferred.addCallback(check_log)
138
139 def test_bootstrap_existing_group(self):
140 """Bootstrap reuses an exisiting provider security group"""
141 self.expect_provider_state_fresh()
142 self.expect_nova_get("os-security-groups",
143 response={'security_groups': [
144 {'name': "juju-testing", 'id': 1},
145 ]})
146 self.expect_create_machine_group(0)
147 self.expect_launch()
148 self.mocker.replay()
149 return self.bootstrap().addCallback(self._check_machine)
150
151 def test_bootstrap_existing_machine_group(self):
152 """Bootstrap deletes and remakes an existing machine security group"""
153 self.expect_provider_state_fresh()
154 self.expect_nova_get("os-security-groups",
155 response={'security_groups': [
156 {'name': "juju-testing-0", 'id': 3},
157 ]})
158 self.expect_create_group()
159 self.expect_nova_delete("os-security-groups/3")
160 self.expect_create_machine_group(0)
161 self.expect_launch()
162 self.mocker.replay()
163 return self.bootstrap().addCallback(self._check_machine)
164
165 def test_existing_machine(self):
166 """A preexisting zookeeper instance is returned if present"""
167 self.expect_swift_get("testing/provider-state",
168 response=yaml.dump({'zookeeper-instances': ['1000']}))
169 self.expect_nova_get("servers/1000",
170 response={'server': {
171 'id': '1000',
172 'name': 'juju testing instance 0',
173 'state': "RUNNING"
174 }})
175 self.mocker.replay()
176
177 log = self.capture_logging("juju.common")
178 self.capture_logging("juju.openstack") # Drop to avoid stderr kipple
179 deferred = self.bootstrap()
180 deferred.addCallback(self._check_machine)
181 def check_log(_):
182 self.assertEqual("juju environment previously bootstrapped.\n",
183 log.getvalue())
184 return deferred.addCallback(check_log)
185
186 def test_existing_machine_missing(self):
187 """Bootstrap overwrites existing zookeeper if instance is present"""
188 self.expect_swift_get("testing/provider-state",
189 response=yaml.dump({'zookeeper-instances': [3000]}))
190 self.expect_nova_get("servers/3000", code=404,
191 response={'itemNotFound':
192 {'message': "The resource could not be found.", 'code': 404}
193 })
194 self.expect_verify()
195 self.expect_nova_get("os-security-groups",
196 response={'security_groups': [
197 {'name': "juju-testing", 'id': 1},
198 {'name': "juju-testing-0", 'id': 3},
199 ]})
200 self.expect_nova_delete("os-security-groups/3")
201 self.expect_create_machine_group(0)
202 self.expect_launch()
203 self.mocker.replay()
204
205 log = self.capture_logging("juju.common", level=logging.DEBUG)
206 self.capture_logging("juju.openstack") # Drop to avoid stderr kipple
207 deferred = self.bootstrap()
208 deferred.addCallback(self._check_machine)
209 def check_log(_):
210 log_text = log.getvalue()
211 self.assertIn("Launching juju bootstrap instance", log_text)
212 self.assertNotIn("previously bootstrapped", log_text)
213 return deferred.addCallback(check_log)
0214
=== added file 'juju/providers/openstack/tests/test_client.py'
--- juju/providers/openstack/tests/test_client.py 1970-01-01 00:00:00 +0000
+++ juju/providers/openstack/tests/test_client.py 2012-07-18 19:47:20 +0000
@@ -0,0 +1,75 @@
1"""Tests for OpenStack API twisted client"""
2
3from juju.lib import testing
4
5from juju.providers.openstack import client
6
7
8class StubClient(client._OpenStackClient):
9
10 def __init__(self):
11 self.url = "http://testing.invalid"
12
13 make_url = client._OpenStackClient._make_url
14
15
16class TestMakeUrl(testing.TestCase):
17
18 def setUp(self):
19 self.client = StubClient()
20 self.client.services = {
21 "compute": self.client.url + "/nova",
22 "object-store": self.client.url + "/swift",
23 }
24
25 def test_list_str(self):
26 self.assertEqual("http://testing.invalid/nova/servers",
27 self.client.make_url("compute", ["servers"]))
28 self.assertEqual("http://testing.invalid/swift/container/object",
29 self.client.make_url("object-store", ["container", "object"]))
30
31 def test_list_int(self):
32 self.assertEqual("http://testing.invalid/nova/servers/1000",
33 self.client.make_url("compute", ["servers", 1000]))
34 self.assertEqual("http://testing.invalid/nova/servers/1000/detail",
35 self.client.make_url("compute", ["servers", 1000, "detail"]))
36
37 def test_list_unicode(self):
38 url = self.client.make_url("object-store", ["container", u"\xa7"])
39 self.assertIsInstance(url, str)
40 self.assertEqual("http://testing.invalid/swift/container/%C2%A7", url)
41
42 def test_str(self):
43 self.assertEqual("http://testing.invalid/nova/servers",
44 self.client.make_url("compute", "servers"))
45 self.assertEqual("http://testing.invalid/swift/container/object",
46 self.client.make_url("object-store", "container/object"))
47
48 def test_trailing_slash(self):
49 self.client.services["object-store"] += "/"
50 self.assertEqual("http://testing.invalid/nova/container",
51 self.client.make_url("compute", "container"))
52 self.assertEqual("http://testing.invalid/nova/container/object",
53 self.client.make_url("compute", ["container", "object"]))
54
55
56class TestPlan(testing.TestCase):
57 """Ideas for tests needed"""
58
59 # auth request without auth
60 # get bytes, content-length 0, return ""
61 # get bytes, not ResponseDone, raise wrapped in ProviderError (with any bytes?)
62 # get bytes, type json, return bytes
63 # get json, content length 0, raise ProviderError
64 # get json, bad header (several forms), raise ProviderError (with bytes)
65 # get json, not ResponseDone, raise wrapped in ProviderError (with any bytes?)
66 # get json, undecodable, raise wrapped in ProviderError with bytes
67 # get json, mismatching root, raise ProviderError with bytes or json?
68 # wrong code, no json header, raise ProviderError with bytes
69 # wrong code, not ResponseDone, raise ProviderError from code with any bytes
70 # wrong code, undecodable, raise ProviderError from code with bytes
71 # wrong code, has mystery root, raise ProviderError from code with bytes or json?
72 # wrong code, has good root, no message
73 # wrong code, has good root, no code
74 # wrong code, has good root, differing code
75 # wrong code, has good root, message, and matching code
076
=== added file 'juju/providers/openstack/tests/test_credentials.py'
--- juju/providers/openstack/tests/test_credentials.py 1970-01-01 00:00:00 +0000
+++ juju/providers/openstack/tests/test_credentials.py 2012-07-18 19:47:20 +0000
@@ -0,0 +1,216 @@
1"""Tests for handling of OpenStack credentials in config and environment"""
2
3from juju.lib import (
4 testing,
5 )
6from juju.providers.openstack import (
7 credentials,
8 )
9
10
11class OpenStackCredentialsTests(testing.TestCase):
12
13 def test_required_url(self):
14 e = self.assertRaises(ValueError, credentials.OpenStackCredentials, {})
15 self.assertIn("Missing config 'auth-url'", str(e))
16
17 def test_required_mode_if_unguessable(self):
18 e = self.assertRaises(ValueError, credentials.OpenStackCredentials, {
19 'auth-url': "http://example.com",
20 })
21 self.assertIn("Missing config 'auth-mode'", str(e))
22
23 def test_legacy(self):
24 creds = credentials.OpenStackCredentials({
25 'auth-url': "http://example.com",
26 'auth-mode': "legacy",
27 'username': "luser",
28 'access-key': "laccess",
29 })
30 self.assertEqual(creds.url, "http://example.com")
31 self.assertEqual(creds.mode, "legacy")
32 self.assertEqual(creds.username, "luser")
33 self.assertEqual(creds.access_key, "laccess")
34
35 def test_legacy_required_username(self):
36 e = self.assertRaises(ValueError, credentials.OpenStackCredentials, {
37 'auth-url': "http://example.com",
38 'auth-mode': "legacy",
39 'access-key': "laccess",
40 })
41 self.assertIn("Missing config 'username'", str(e))
42
43 def test_legacy_required_access_key(self):
44 e = self.assertRaises(ValueError, credentials.OpenStackCredentials, {
45 'auth-url': "http://example.com",
46 'auth-mode': "legacy",
47 'username': "luser",
48 })
49 self.assertIn("Missing config 'access-key'", str(e))
50
51 # v1.0 auth is gone from the upstream codebase so maybe remove support
52 def test_legacy_guess_v1_0(self):
53 creds = credentials.OpenStackCredentials({
54 'auth-url': "http://example.com/v1.0/",
55 'username': "luser",
56 'access-key': "laccess",
57 })
58 self.assertEqual(creds.mode, "legacy")
59
60 def test_legacy_guess_v1_1(self):
61 creds = credentials.OpenStackCredentials({
62 'auth-url': "http://example.com/v1.1/",
63 'username': "luser",
64 'access-key': "laccess",
65 })
66 self.assertEqual(creds.mode, "legacy")
67
68 def test_userpass(self):
69 creds = credentials.OpenStackCredentials({
70 'auth-url': "http://example.com",
71 'auth-mode': "userpass",
72 'username': "uuser",
73 'password': "upass",
74 'project-name': "uproject",
75 })
76 self.assertEqual(creds.url, "http://example.com")
77 self.assertEqual(creds.mode, "userpass")
78 self.assertEqual(creds.username, "uuser")
79 self.assertEqual(creds.password, "upass")
80 self.assertEqual(creds.project_name, "uproject")
81
82 def test_userpass_guess_v2_0_no_slash(self):
83 creds = credentials.OpenStackCredentials({
84 'auth-url': "http://example.com/v2.0",
85 'username': "uuser",
86 'password': "upass",
87 'project-name': "uproject",
88 })
89 self.assertEqual(creds.mode, "userpass")
90
91 def test_userpass_guess_v2_0_slash(self):
92 creds = credentials.OpenStackCredentials({
93 'auth-url': "http://example.com/v2.0/",
94 'username': "uuser",
95 'password': "upass",
96 'project-name': "uproject",
97 })
98 self.assertEqual(creds.mode, "userpass")
99
100 def test_keypair(self):
101 creds = credentials.OpenStackCredentials({
102 'auth-url': "http://example.com",
103 'auth-mode': "keypair",
104 'access-key': "kaccess",
105 'secret-key': "ksecret",
106 'project-name': "kproject",
107 })
108 self.assertEqual(creds.url, "http://example.com")
109 self.assertEqual(creds.mode, "keypair")
110 self.assertEqual(creds.access_key, "kaccess")
111 self.assertEqual(creds.secret_key, "ksecret")
112 self.assertEqual(creds.project_name, "kproject")
113
114
115class FromEnvironmentTests(testing.TestCase):
116
117 def test_required_url(self):
118 self.change_environment()
119 e = self.assertRaises(ValueError,
120 credentials.OpenStackCredentials.from_environment, {})
121 self.assertIn("Missing config 'auth-url'", str(e))
122
123 def test_required_mode_if_unguessable(self):
124 self.change_environment(**{"NOVA_URL": "http://example.com"})
125 e = self.assertRaises(ValueError,
126 credentials.OpenStackCredentials.from_environment, {})
127 self.assertIn("Missing config 'auth-mode'", str(e))
128
129 def test_legacy(self):
130 self.change_environment(**{
131 "NOVA_URL": "http://example.com/v1.1/",
132 "NOVA_USERNAME": "euser",
133 "NOVA_API_KEY": "ekey",
134 })
135 creds = credentials.OpenStackCredentials.from_environment({})
136 self.assertEqual(creds.mode, "legacy")
137 self.assertEqual(creds.username, "euser")
138 self.assertEqual(creds.access_key, "ekey")
139
140 def test_keypair(self):
141 self.change_environment(**{
142 "NOVA_URL": "http://example.com/v2.0/",
143 "NOVA_API_KEY": "eaccess",
144 "NOVA_PROJECT_NAME": "eproject",
145 "NOVA_PROJECT_ID": "349212",
146 "NOVA_REGION_NAME": "eregion",
147 "EC2_SECRET_KEY": "esecret",
148 })
149 creds = credentials.OpenStackCredentials.from_environment({
150 'auth-mode': "keypair",
151 })
152 self.assertEqual(creds.access_key, "eaccess")
153 self.assertEqual(creds.secret_key, "esecret")
154 self.assertEqual(creds.project_name, "eproject")
155 self.assertEqual(creds.region, "eregion")
156
157 def test_userpass(self):
158 self.change_environment(**{
159 "OS_AUTH_URL": "http://example.com/v2.0/",
160 "OS_USERNAME": "euser",
161 "OS_PASSWORD": "epass",
162 "OS_TENANT_NAME": "eproject",
163 "OS_REGION_NAME": "eregion",
164 })
165 creds = credentials.OpenStackCredentials.from_environment({})
166 self.assertEqual(creds.mode, "userpass")
167 self.assertEqual(creds.username, "euser")
168 self.assertEqual(creds.password, "epass")
169 self.assertEqual(creds.project_name, "eproject")
170 self.assertEqual(creds.region, "eregion")
171
172 def test_prefer_os_auth_url(self):
173 self.change_environment(**{
174 "NOVA_URL": "http://example.com/v1.1/",
175 "NOVA_API_KEY": "eaccess",
176 "OS_AUTH_URL": "http://example.com/v2.0/",
177 "OS_USERNAME": "euser",
178 "OS_PASSWORD": "epass",
179 "OS_TENANT_NAME": "eproject",
180 })
181 creds = credentials.OpenStackCredentials.from_environment({})
182 self.assertEqual(creds.url, "http://example.com/v2.0/")
183 self.assertEqual(creds.mode, "userpass")
184
185
186class SetConfigDefaultsTests(testing.TestCase):
187
188 def test_set_all(self):
189 config = {
190 'auth-url': "http://example.com",
191 'auth-mode': "legacy",
192 'username': "luser",
193 'access-key': "laccess",
194 }
195 creds = credentials.OpenStackCredentials(config)
196 new_config = {}
197 creds.set_config_defaults(new_config)
198 self.assertEqual(config, new_config)
199
200 def test_set_only_missing(self):
201 config = {
202 'auth-url': "http://example.com/v1.1/",
203 'username': "luser",
204 'access-key': "laccess",
205 }
206 creds = credentials.OpenStackCredentials(config)
207 new_config = {
208 'username': "nuser",
209 }
210 creds.set_config_defaults(new_config)
211 self.assertEqual({
212 'auth-url': "http://example.com/v1.1/",
213 'auth-mode': "legacy",
214 'username': "nuser",
215 'access-key': "laccess",
216 }, new_config)
0217
=== added file 'juju/providers/openstack/tests/test_files.py'
--- juju/providers/openstack/tests/test_files.py 1970-01-01 00:00:00 +0000
+++ juju/providers/openstack/tests/test_files.py 2012-07-18 19:47:20 +0000
@@ -0,0 +1,103 @@
1"""Tests for file storage backend based on Swift"""
2
3from cStringIO import StringIO
4
5from twisted.internet import (
6 defer,
7 )
8
9from juju import errors
10from juju.lib import (
11 testing,
12 )
13from juju.providers.openstack.tests import OpenStackTestMixin
14
15
16class FileStorageTestCase(OpenStackTestMixin, testing.TestCase):
17
18 def get_storage(self):
19 provider = self.get_provider()
20 storage = provider.get_file_storage()
21 return storage
22
23 def test_put_file(self):
24 """A file can be put in the storage"""
25 content = "some text"
26 self.expect_swift_put("testing/object", content)
27 self.mocker.replay()
28 return self.get_storage().put("object", StringIO(content))
29
30 def test_put_file_unicode(self):
31 """A file with a unicode name is put in UTF-8 url encoded form"""
32 content = "some text"
33 self.expect_swift_put("testing/%C2%A7", content)
34 self.mocker.replay()
35 return self.get_storage().put(u"\xa7", StringIO(content))
36
37 def test_put_file_create_container(self):
38 """The container will be created if it doesn't exist yet"""
39 content = "some text"
40 self.expect_swift_put("testing/object", content, code=404)
41 self.expect_swift_put_container("testing")
42 self.expect_swift_put("testing/object", content)
43 self.mocker.replay()
44 return self.get_storage().put("object", StringIO(content))
45
46 def test_put_file_unknown_error(self):
47 """Unexpected errors from client propogate"""
48 content = "some text"
49 self._mock_swift("PUT", "testing/object", content)
50 self.mocker.result(defer.fail(ValueError("Something unexpected")))
51 self.mocker.replay()
52 deferred = self.get_storage().put("object", StringIO(content))
53 return self.assertFailure(deferred, ValueError)
54
55 @defer.inlineCallbacks
56 def test_get_url(self):
57 """A url can be generated for any stored file."""
58 self.mocker.replay()
59 storage = self.get_storage()
60 yield storage._swift._client.authenticate()
61 url = storage.get_url("object")
62 self.assertEqual(self.swift_url + "/testing/object", url)
63
64 @defer.inlineCallbacks
65 def test_get_url_unicode(self):
66 """A url can be generated for *any* stored file."""
67 self.mocker.replay()
68 storage = self.get_storage()
69 yield storage._swift._client.authenticate()
70 url = storage.get_url(u"\xa7")
71 self.assertEqual(self.swift_url + "/testing/%C2%A7", url)
72
73 @defer.inlineCallbacks
74 def test_get_file(self):
75 """Retrieving a file returns a file-like object with the content"""
76 content = "some text"
77 self.expect_swift_get("testing/object", response=content)
78 self.mocker.replay()
79 result = yield self.get_storage().get("object")
80 self.assertEqual(result.read(), content)
81
82 @defer.inlineCallbacks
83 def test_get_file_unicode(self):
84 """Retrieving a file with a unicode key uses a UTF-8 url"""
85 content = "some text"
86 self.expect_swift_get(u"testing/%C2%A7", response=content)
87 self.mocker.replay()
88 result = yield self.get_storage().get(u"\xa7")
89 self.assertEqual(result.read(), content)
90
91 def test_get_file_nonexistant(self):
92 """Retrieving a nonexistant file raises a file not found error."""
93 self.expect_swift_get(u"testing/missing", code=404)
94 self.mocker.replay()
95 deferred = self.get_storage().get("missing")
96 return self.assertFailure(deferred, errors.FileNotFound)
97
98 def test_get_file_error(self):
99 """An error from the client results is attributed to the provider"""
100 self.expect_swift_get(u"testing/unavailable", code=500)
101 self.mocker.replay()
102 deferred = self.get_storage().get("unavailable")
103 return self.assertFailure(deferred, errors.ProviderInteractionError)
0104
=== added file 'juju/providers/openstack/tests/test_getmachines.py'
--- juju/providers/openstack/tests/test_getmachines.py 1970-01-01 00:00:00 +0000
+++ juju/providers/openstack/tests/test_getmachines.py 2012-07-18 19:47:20 +0000
@@ -0,0 +1,115 @@
1"""Tests for OpenStack provider method for listing live juju machines"""
2
3from juju.lib import testing
4from juju.providers.openstack.machine import NovaProviderMachine
5from juju.providers.openstack.tests import OpenStackTestMixin
6
7
8class GetMachinesTests(OpenStackTestMixin, testing.TestCase):
9
10 def check_machines(self, machines, expected):
11 machine_details = []
12 for m in machines:
13 self.assertTrue(isinstance(m, NovaProviderMachine))
14 machine_details.append((m.instance_id, m.state))
15 machine_details.sort()
16 expected.sort()
17 self.assertEqual(expected, machine_details)
18
19 def test_all_none(self):
20 self.expect_nova_get("servers", response={"servers": []})
21 self.mocker.replay()
22 return self.get_provider().get_machines().addCallback(
23 self.check_machines, [])
24
25 def test_all_single(self):
26 self.expect_nova_get("servers", response={"servers": [
27 self.make_server(1000),
28 ]})
29 self.mocker.replay()
30 return self.get_provider().get_machines().addCallback(
31 self.check_machines, [(1000, 'running')])
32
33 def test_all_multiple(self):
34 self.expect_nova_get("servers", response={"servers": [
35 self.make_server(1001),
36 self.make_server(1002, status="BUILDING"),
37 ]})
38 self.mocker.replay()
39 return self.get_provider().get_machines().addCallback(
40 self.check_machines, [(1001, 'running'), (1002, 'pending')])
41
42 def test_all_some_dead(self):
43 self.expect_nova_get("servers", response={"servers": [
44 self.make_server(1001, status="BUILDING"),
45 self.make_server(1002, status="DELETED"),
46 ]})
47 self.mocker.replay()
48 return self.get_provider().get_machines().addCallback(
49 self.check_machines, [(1001, 'pending')])
50
51 def test_all_some_other(self):
52 self.expect_nova_get("servers", response={"servers": [
53 self.make_server(1001, name="nova started server"),
54 self.make_server(1002),
55 ]})
56 self.mocker.replay()
57 return self.get_provider().get_machines().addCallback(
58 self.check_machines, [(1002, 'running')])
59
60 def test_two_none(self):
61 self.expect_nova_get("servers", response={"servers": []})
62 self.mocker.replay()
63 deferred = self.get_provider().get_machines([1001, 1002])
64 return self.assert_not_found(deferred, [1001, 1002])
65
66 def test_two_some_dead(self):
67 self.expect_nova_get("servers", response={"servers": [
68 self.make_server(1001, status="BUILDING"),
69 self.make_server(1002, status="DELETED"),
70 ]})
71 self.mocker.replay()
72 deferred = self.get_provider().get_machines([1001, 1002])
73 return self.assert_not_found(deferred, [1002])
74
75 def test_two_some_other(self):
76 self.expect_nova_get("servers", response={"servers": [
77 self.make_server(1001, name="nova started server"),
78 self.make_server(1002),
79 ]})
80 self.mocker.replay()
81 deferred = self.get_provider().get_machines([1001, 1002])
82 return self.assert_not_found(deferred, [1001])
83
84 def test_one_running(self):
85 self.expect_nova_get("servers/1000",
86 response={"server": self.make_server(1000)})
87 self.mocker.replay()
88 deferred = self.get_provider().get_machines([1000])
89 return deferred.addCallback(self.check_machines, [(1000, 'running')])
90
91 def test_one_dead(self):
92 self.expect_nova_get("servers/1000",
93 response={"server": self.make_server(1000, status="DELETED")})
94 self.mocker.replay()
95 deferred = self.get_provider().get_machines([1000])
96 return self.assert_not_found(deferred, [1000])
97
98 def test_one_other(self):
99 self.expect_nova_get("servers/1000",
100 response={"server": self.make_server(1000, name="testing")})
101 self.mocker.replay()
102 deferred = self.get_provider().get_machines([1000])
103 return self.assert_not_found(deferred, [1000])
104
105 def test_one_missing(self):
106 self.expect_nova_get("servers/1000",
107 code=404, response={"itemNotFound":
108 {"message": "The resource could not be found.", "code": 404}})
109 self.mocker.replay()
110 deferred = self.get_provider().get_machines([1000])
111 return self.assert_not_found(deferred, [1000])
112 # XXX: Need to do error handling properly in client still
113 test_one_missing.skip = True
114
115 # XXX: EC2 also has some tests for wrapping unexpected errors from backend
0116
=== added file 'juju/providers/openstack/tests/test_launch.py'
--- juju/providers/openstack/tests/test_launch.py 1970-01-01 00:00:00 +0000
+++ juju/providers/openstack/tests/test_launch.py 2012-07-18 19:47:20 +0000
@@ -0,0 +1,113 @@
1"""Tests for launching a new server customised for juju using Nova"""
2
3import logging
4
5from twisted.internet import (
6 defer,
7 )
8
9from juju import errors
10from juju.lib import (
11 mocker,
12 testing,
13 )
14from juju.machine import (
15 constraints,
16 )
17
18from juju.providers.openstack import (
19 launch,
20 tests,
21 )
22
23class MockedLaunchProvider(tests.MockedProvider):
24
25 def launch(self, machine_id, master=False):
26 constraint_set = constraints.ConstraintSet(self.provider_type)
27 details = {
28 'machine-id': machine_id,
29 'constraints': constraint_set.load({}),
30 }
31 return launch.NovaLaunchMachine.launch(self, details, master)
32
33 def expect_launch_setup(self, machine_id):
34 self.port_manager.ensure_groups(machine_id)
35 self.mocker.result(defer.succeed(["juju-x", "juju-y"]))
36 self.nova.list_flavors()
37 self.mocker.result(defer.succeed([
38 {'id': 1, 'name': "standard.xsmall"},
39 ]))
40
41 def expect_run_server(self, machine_id, response):
42 self.nova.run_server(
43 name="juju testing instance " + machine_id,
44 image_id=42,
45 flavor_id=1,
46 security_group_names=["juju-x", "juju-y"],
47 user_data=mocker.ANY,
48 )
49 self.mocker.result(defer.succeed(response))
50
51 def expect_available_floating_ip(self, server_id):
52 self.nova.list_floating_ips()
53 self.mocker.result(defer.succeed([
54 {'instance_id': None, 'ip': "198.162.1.0"},
55 ]))
56 self.nova.add_floating_ip(server_id, "198.162.1.0")
57 self.mocker.result(defer.succeed(None))
58
59
60class NovaLaunchMachineTests(testing.TestCase):
61
62 def test_launch_requires_default_image_id(self):
63 config = dict(MockedLaunchProvider.default_config)
64 del config['default-image-id']
65 provider = MockedLaunchProvider(self.mocker, config)
66 provider.expect_zookeeper_machines(1000)
67 self.mocker.replay()
68 deferred = provider.launch("1")
69 return self.assertFailure(deferred, errors.ProviderError)
70
71 def test_start_machine(self):
72 provider = MockedLaunchProvider(self.mocker)
73 provider.expect_zookeeper_machines(1000)
74 provider.expect_launch_setup("1")
75 provider.expect_run_server("1", response={
76 'id': 1001,
77 'addresses': {'public': []},
78 })
79 self.mocker.replay()
80 return provider.launch("1")
81
82 def test_start_machine_delay(self):
83 provider = MockedLaunchProvider(self.mocker)
84 provider.config["use-floating-ip"] = True
85 provider.expect_zookeeper_machines(1000)
86 provider.expect_launch_setup("1")
87 provider.expect_run_server("1", response={
88 'id': 1001,
89 })
90 provider.nova.get_server(1001)
91 self.mocker.result(defer.succeed({
92 'id': 1001,
93 'addresses': {'public': []},
94 }))
95 provider.expect_available_floating_ip(1001)
96 self.mocker.result(defer.succeed(None))
97 self.mocker.replay()
98 self.patch(launch.NovaLaunchMachine, "_DELAY_FOR_ADDRESSES", 0)
99 return provider.launch("1")
100
101 def test_start_machine_master(self):
102 provider = MockedLaunchProvider(self.mocker)
103 provider.expect_swift_public_object_url("juju_master_id")
104 provider.expect_launch_setup("0")
105 provider.expect_run_server("0", response={
106 'id': 1000,
107 'addresses': {'public': []},
108 })
109 provider.expect_swift_put("juju_master_id", "1000")
110 provider.provider_actions.save_state({'zookeeper-instances': [1000]})
111 self.mocker.result(defer.succeed(None))
112 self.mocker.replay()
113 return provider.launch("0", master=True)
0114
=== added file 'juju/providers/openstack/tests/test_machine.py'
--- juju/providers/openstack/tests/test_machine.py 1970-01-01 00:00:00 +0000
+++ juju/providers/openstack/tests/test_machine.py 2012-07-18 19:47:20 +0000
@@ -0,0 +1,110 @@
1"""Tests for server wrapper and helper functions"""
2
3from juju.providers.openstack.machine import (
4 get_server_addresses,
5 get_server_status,
6 )
7
8from juju.lib.testing import TestCase
9
10
11class GetServerStatusTest(TestCase):
12 """Tests for mapping of Nova status names to EC2 style names"""
13
14 def test_build_schedualing(self):
15 self.assertEqual("pending",
16 get_server_status({u'status': u'BUILD(scheduling)'}))
17
18 def test_build_spawning(self):
19 self.assertEqual("pending",
20 get_server_status({u'status': u'BUILD(spawning)'}))
21
22 def test_active(self):
23 self.assertEqual("running",
24 get_server_status({u'status': u'ACTIVE'}))
25
26 def test_no_status(self):
27 self.assertEqual("pending",
28 get_server_status({}))
29
30 def test_mystery_status(self):
31 self.assertEqual("unknown",
32 get_server_status({u'status': u'NEVER_BEFORE_SEEN_MYSTERY'}))
33
34
35class GetServerAddressesTest(TestCase):
36 """Tests for deriving a public and private address from Nova dict"""
37
38 def test_missing(self):
39 self.assertEqual((None, None), get_server_addresses({}))
40
41 def test_empty(self):
42 self.assertEqual((None, None),
43 get_server_addresses({u'addresses': {}}))
44
45 def test_private_only(self):
46 self.assertEqual(("127.0.0.4", None),
47 get_server_addresses({u'addresses': {
48 "private": [{"addr": "127.0.0.4", "version": 4}],
49 }}))
50
51 def test_private_plus(self):
52 self.assertEqual(("127.0.0.4", "8.8.4.4"),
53 get_server_addresses({u'addresses': {
54 "private": [
55 {"addr": "127.0.0.4", "version": 4},
56 {"addr": "8.8.4.4", "version": 4},
57 ],
58 }}))
59
60 def test_public_only(self):
61 self.assertEqual((None, "8.8.8.8"),
62 get_server_addresses({u'addresses': {
63 "public": [{"addr": "8.8.8.8", "version": 4}],
64 }}))
65
66 def test_public_and_private(self):
67 self.assertEqual(("127.0.0.4", "8.8.8.8"),
68 get_server_addresses({u'addresses': {
69 "public": [{"addr": "8.8.8.8", "version": 4}],
70 "private": [{"addr": "127.0.0.4", "version": 4}],
71 }}))
72
73 def test_public_and_private_plus(self):
74 self.assertEqual(("127.0.0.4", "8.8.8.8"),
75 get_server_addresses({u'addresses': {
76 "public": [{"addr": "8.8.8.8", "version": 4}],
77 "private": [
78 {"addr": "127.0.0.4", "version": 4},
79 {"addr": "8.8.4.4", "version": 4},
80 ],
81 }}))
82
83 def test_custom_only(self):
84 self.assertEqual(("127.0.0.2", None),
85 get_server_addresses({u'addresses': {
86 "special": [{"addr": "127.0.0.2", "version": 4}],
87 }}))
88
89 def test_custom_plus(self):
90 self.assertEqual(("127.0.0.2", "8.8.4.4"),
91 get_server_addresses({u'addresses': {
92 "special": [
93 {"addr": "127.0.0.2", "version": 4},
94 {"addr": "8.8.4.4", "version": 4},
95 ],
96 }}))
97
98 def test_custom_and_private(self):
99 self.assertEqual(("127.0.0.4", None),
100 get_server_addresses({u'addresses': {
101 "special": [{"addr": "127.0.0.2", "version": 4}],
102 "private": [{"addr": "127.0.0.4", "version": 4}],
103 }}))
104
105 def test_custom_and_public(self):
106 self.assertEqual(("127.0.0.2", "8.8.8.8"),
107 get_server_addresses({u'addresses': {
108 "special": [{"addr": "127.0.0.2", "version": 4}],
109 "public": [{"addr": "8.8.8.8", "version": 4}],
110 }}))
0111
=== added file 'juju/providers/openstack/tests/test_ports.py'
--- juju/providers/openstack/tests/test_ports.py 1970-01-01 00:00:00 +0000
+++ juju/providers/openstack/tests/test_ports.py 2012-07-18 19:47:20 +0000
@@ -0,0 +1,396 @@
1"""Tests for emulating port management with security groups"""
2
3import logging
4
5from juju import errors
6from juju.lib.testing import TestCase
7from juju.providers.openstack.machine import NovaProviderMachine
8from juju.providers.openstack.ports import NovaPortManager
9from juju.providers.openstack.tests import OpenStackTestMixin
10
11
12class ProviderPortMgmtTests(OpenStackTestMixin, TestCase):
13 """Tests for provider exposed port management methods"""
14
15 def expect_create_rule(self, group_id, proto, port):
16 self.expect_nova_post("os-security-group-rules",
17 {'security_group_rule': {
18 'parent_group_id': group_id,
19 'ip_protocol': proto,
20 'from_port': port,
21 'to_port': port,
22 }},
23 response={'security_group_rule': {
24 'id': 144, 'parent_group_id': group_id,
25 }})
26
27 def expect_existing_rule(self, rule_id, proto, port):
28 self.expect_nova_get("servers/1000/os-security-groups",
29 response={'security_groups': [
30 {'name': "juju-testing-1", 'id': 1, 'rules': [{
31 'id': rule_id,
32 'parent_group_id': 1,
33 'ip_protocol': proto,
34 'from_port': port,
35 'to_port': port,
36 }]
37 },
38 ]})
39
40 def test_open_port(self):
41 """Opening a port adds the rule to the appropriate security group"""
42 self.expect_nova_get("servers/1000/os-security-groups",
43 response={'security_groups': [
44 {'name': "juju-testing-1", 'id': 1},
45 ]})
46 self.expect_create_rule(1, "tcp", 80)
47 self.mocker.replay()
48
49 log = self.capture_logging("juju.openstack", level=logging.DEBUG)
50 machine = NovaProviderMachine('1000', "server1000.testing.invalid")
51 deferred = self.get_provider().open_port(machine, "1", 80)
52 def _check_log(_):
53 self.assertIn("Opened 80/tcp on machine '1000'",
54 log.getvalue())
55 return deferred.addCallback(_check_log)
56
57 def test_open_port_missing_group(self):
58 """Missing security group raises an error on deleting port"""
59 self.expect_nova_get("servers/1000/os-security-groups",
60 response={'security_groups': []})
61 self.mocker.replay()
62
63 machine = NovaProviderMachine('1000', "server1000.testing.invalid")
64 deferred = self.get_provider().open_port(machine, "1", 80)
65 return self.assertFailure(deferred, errors.ProviderInteractionError)
66
67 def test_close_port(self):
68 """Closing a port removes the matching rule from the security group"""
69 self.expect_existing_rule(12, "tcp", 80)
70 self.expect_nova_delete("os-security-group-rules/12")
71 self.mocker.replay()
72
73 log = self.capture_logging("juju.openstack", level=logging.DEBUG)
74 machine = NovaProviderMachine('1000', "server1000.testing.invalid")
75 deferred = self.get_provider().close_port(machine, "1", 80)
76 def _check_log(_):
77 self.assertIn("Closed 80/tcp on machine '1000'",
78 log.getvalue())
79 return deferred.addCallback(_check_log)
80
81 def test_close_port_missing_group(self):
82 """Missing security group raises an error on closing port"""
83 self.expect_nova_get("servers/1000/os-security-groups",
84 response={'security_groups': []})
85 self.mocker.replay()
86
87 machine = NovaProviderMachine('1000', "server1000.testing.invalid")
88 deferred = self.get_provider().close_port(machine, "1", 80)
89 return self.assertFailure(deferred, errors.ProviderInteractionError)
90
91 def test_close_port_missing_rule(self):
92 """Missing security group rule raises an error on closing port"""
93 self.expect_nova_get("servers/1000/os-security-groups",
94 response={'security_groups': [{
95 'name': "juju-testing-1", 'id': 1, "rules": [],
96 }]})
97 self.mocker.replay()
98
99 machine = NovaProviderMachine('1000', "server1000.testing.invalid")
100 deferred = self.get_provider().close_port(machine, "1", 80)
101 return self.assertFailure(deferred, errors.ProviderInteractionError)
102
103 def test_close_port_mismatching_rule(self):
104 """Rule with different port raises an error on closing port"""
105 self.expect_existing_rule(12, "tcp", 8080)
106 self.mocker.replay()
107
108 machine = NovaProviderMachine('1000', "server1000.testing.invalid")
109 deferred = self.get_provider().close_port(machine, "1", 80)
110 return self.assertFailure(deferred, errors.ProviderInteractionError)
111
112 def test_get_opened_ports_none(self):
113 """No opened ports are listed when there are no rules"""
114 self.expect_nova_get("servers/1000/os-security-groups",
115 response={'security_groups': [{
116 'name': "juju-testing-1", 'id': 1, "rules": [],
117 }]})
118 self.mocker.replay()
119
120 machine = NovaProviderMachine('1000', "server1000.testing.invalid")
121 deferred = self.get_provider().get_opened_ports(machine, "1")
122 return deferred.addCallback(self.assertEqual, set())
123
124 def test_get_opened_ports_one(self):
125 """Opened port is listed when there is a matching rule"""
126 self.expect_existing_rule(12, "tcp", 80)
127 self.mocker.replay()
128
129 machine = NovaProviderMachine('1000', "server1000.testing.invalid")
130 deferred = self.get_provider().get_opened_ports(machine, "1")
131 return deferred.addCallback(self.assertEqual, set([(80, "tcp")]))
132
133 def test_get_opened_ports_group_ignored(self):
134 """Opened ports exclude rules delegating to other security groups"""
135 self.expect_nova_get("servers/1000/os-security-groups",
136 response={'security_groups': [{
137 'name': "juju-testing-1", 'id': 1, "rules": [{
138 'id': 12,
139 'parent_group_id': 1,
140 'ip_protocol': None,
141 'from_port': None,
142 'to_port': None,
143 'group': {'name': "juju-testing"},
144 }],
145 }]})
146 self.mocker.replay()
147
148 machine = NovaProviderMachine('1000', "server1000.testing.invalid")
149 deferred = self.get_provider().get_opened_ports(machine, "1")
150 return deferred.addCallback(self.assertEqual, set())
151
152 def test_get_opened_ports_multiport_ignored(self):
153 """Opened ports exclude rules spanning multiple ports"""
154 self.expect_nova_get("servers/1000/os-security-groups",
155 response={'security_groups': [{
156 'name': "juju-testing-1", 'id': 1, "rules": [{
157 'id': 12,
158 'parent_group_id': 1,
159 'ip_protocol': "tcp",
160 'from_port': 8080,
161 'to_port': 8081,
162 }],
163 }]})
164 self.mocker.replay()
165
166 machine = NovaProviderMachine('1000', "server1000.testing.invalid")
167 deferred = self.get_provider().get_opened_ports(machine, "1")
168 return deferred.addCallback(self.assertEqual, set())
169
170
171class PortManagerTestMixin(OpenStackTestMixin):
172
173 def get_port_manager(self):
174 provider = self.get_provider()
175 return NovaPortManager(provider.nova, provider.environment_name)
176
177
178class EnsureGroupsTests(PortManagerTestMixin, TestCase):
179 """Tests for ensure_groups method used when launching machines"""
180
181 def expect_create_juju_group(self):
182 self.expect_nova_post("os-security-groups",
183 {'security_group': {
184 'name': 'juju-testing',
185 'description': 'juju group for testing',
186 }},
187 response={'security_group': {
188 'id': 1,
189 }})
190 self.expect_nova_post("os-security-group-rules",
191 {'security_group_rule': {
192 'parent_group_id': 1,
193 'ip_protocol': "tcp",
194 'from_port': 22,
195 'to_port': 22,
196 }},
197 response={'security_group_rule': {
198 'id': 144, 'parent_group_id': 1,
199 }})
200 self.expect_nova_post("os-security-group-rules",
201 {'security_group_rule': {
202 'parent_group_id': 1,
203 'group_id': 1,
204 'ip_protocol': "tcp",
205 'from_port': 1,
206 'to_port': 65535,
207 }},
208 response={'security_group_rule': {
209 'id': 145, 'parent_group_id': 1,
210 }})
211
212 def expect_create_machine_group(self, machine_id):
213 machine = str(machine_id)
214 self.expect_nova_post("os-security-groups",
215 {'security_group': {
216 'name': 'juju-testing-' + machine,
217 'description': 'juju group for testing machine ' + machine,
218 }},
219 response={'security_group': {
220 'id': 2,
221 }})
222
223 def check_group_names(self, result, machine_id):
224 self.assertEqual(["juju-testing", "juju-testing-" + str(machine_id)],
225 result)
226
227 def test_none_existing(self):
228 """When no groups exist juju and machine security groups are created"""
229 self.expect_nova_get("os-security-groups",
230 response={'security_groups': []})
231 self.expect_create_juju_group()
232 self.expect_create_machine_group(0)
233 self.mocker.replay()
234 deferred = self.get_port_manager().ensure_groups(0)
235 return deferred.addCallback(self.check_group_names, 0)
236
237 def test_other_existing(self):
238 """Existing groups in a different environment are not affected"""
239 self.expect_nova_get("os-security-groups",
240 response={'security_groups': [
241 {'name': "juju-testingish", 'id': 7},
242 {'name': "juju-testingish-0", 'id': 8},
243 ]})
244 self.expect_create_juju_group()
245 self.expect_create_machine_group(0)
246 self.mocker.replay()
247 deferred = self.get_port_manager().ensure_groups(0)
248 return deferred.addCallback(self.check_group_names, 0)
249
250 def test_existing_juju_group(self):
251 """An exisiting juju security group is reused"""
252 self.expect_nova_get("os-security-groups",
253 response={'security_groups': [
254 {'name': "juju-testing", 'id': 1},
255 ]})
256 self.expect_create_machine_group(0)
257 self.mocker.replay()
258 deferred = self.get_port_manager().ensure_groups(0)
259 return deferred.addCallback(self.check_group_names, 0)
260
261 def test_existing_machine_group(self):
262 """An existing machine security group is deleted and remade"""
263 self.expect_nova_get("os-security-groups",
264 response={'security_groups': [
265 {'name': "juju-testing-6", 'id': 3},
266 ]})
267 self.expect_create_juju_group()
268 self.expect_nova_delete("os-security-groups/3")
269 self.expect_create_machine_group(6)
270 self.mocker.replay()
271 deferred = self.get_port_manager().ensure_groups(6)
272 return deferred.addCallback(self.check_group_names, 6)
273
274
275class GetMachineGroupsTests(PortManagerTestMixin, TestCase):
276 """Tests for get_machine_groups method needed for machine shutdown"""
277
278 def test_normal(self):
279 """A standard juju machine returns the machine group name and id"""
280 self.expect_nova_get("servers/1000/os-security-groups",
281 response={'security_groups': [
282 {'id': 7, 'name': "juju-testing"},
283 {'id': 8, 'name': "juju-testing-0"},
284 ]})
285 self.mocker.replay()
286 machine = NovaProviderMachine(1000)
287 deferred = self.get_port_manager().get_machine_groups(machine)
288 return deferred.addCallback(self.assertEqual, {"juju-testing-0": 8})
289
290 def test_normal_include_juju(self):
291 """If param with_juju_group=True the juju group is also returned"""
292 self.expect_nova_get("servers/1000/os-security-groups",
293 response={'security_groups': [
294 {'id': 7, 'name': "juju-testing"},
295 {'id': 8, 'name': "juju-testing-0"},
296 ]})
297 self.mocker.replay()
298 machine = NovaProviderMachine(1000)
299 deferred = self.get_port_manager().get_machine_groups(machine, True)
300 return deferred.addCallback(self.assertEqual,
301 {"juju-testing": 7, "juju-testing-0": 8})
302
303 def test_extra_group(self):
304 """Additional groups not in the juju namespace are ignored"""
305 self.expect_nova_get("servers/1000/os-security-groups",
306 response={'security_groups': [
307 {'id': 1, 'name': "default"},
308 {'id': 7, 'name': "juju-testing"},
309 {'id': 8, 'name': "juju-testing-0"},
310 ]})
311 self.mocker.replay()
312 machine = NovaProviderMachine(1000)
313 deferred = self.get_port_manager().get_machine_groups(machine)
314 return deferred.addCallback(self.assertEqual, {"juju-testing-0": 8})
315
316 def test_other_group(self):
317 """A server not managed by juju returns nothing"""
318 self.expect_nova_get("servers/1000/os-security-groups",
319 response={'security_groups': [
320 {'id': 1, 'name': "default"},
321 ]})
322 self.mocker.replay()
323 machine = NovaProviderMachine(1000)
324 deferred = self.get_port_manager().get_machine_groups(machine)
325 return deferred.addCallback(self.assertEqual, None)
326
327 def test_missing_groups(self):
328 """A server with no groups returns nothing"""
329 self.expect_nova_get("servers/1000/os-security-groups",
330 response={'security_groups': []})
331 self.mocker.replay()
332 machine = NovaProviderMachine(1000)
333 deferred = self.get_port_manager().get_machine_groups(machine)
334 return deferred.addCallback(self.assertEqual, None)
335
336 def test_error_missing_server(self):
337 """A server that doesn't exist or has been deleted returns nothing"""
338 self.expect_nova_get("servers/1000/os-security-groups",
339 code=404, response={"itemNotFound": {
340 "message": "Instance 1000 could not be found.",
341 "code": 404,
342 }})
343 self.mocker.replay()
344 machine = NovaProviderMachine(1000)
345 deferred = self.get_port_manager().get_machine_groups(machine)
346 return deferred.addCallback(self.assertEqual, None)
347 # XXX: Broken by workaround for HP not supporting this api
348 test_error_missing_server.skip = True
349
350 def test_error_missing_page(self):
351 """Unexpected errors from the client are propogated"""
352 self.expect_nova_get("servers/1000/os-security-groups",
353 code=404, response="404 Not Found\n\n"
354 "The resource could not be found.\n\n ")
355 self.mocker.replay()
356 machine = NovaProviderMachine(1000)
357 deferred = self.get_port_manager().get_machine_groups(machine)
358 return self.assertFailure(deferred, errors.ProviderInteractionError)
359 # XXX: Need implemention of fancy error to exception mapping
360 test_error_missing_page.skip = True
361
362 def test_error_missing_server_fault(self):
363 "A bogus compute fault due to lp:1010486 returns nothing"""
364 self.expect_nova_get("servers/1000/os-security-groups",
365 code=500, response={"computeFault": {
366 "message": "The server has either erred or is incapable of"
367 " performing the requested operation.",
368 "code": 500,
369 }})
370 self.expect_nova_get("servers/1000",
371 code=404, response={"itemNotFound": {
372 "message": "The resource could not be found.",
373 "code": 404,
374 }})
375 self.mocker.replay()
376 machine = NovaProviderMachine(1000)
377 deferred = self.get_port_manager().get_machine_groups(machine)
378 return deferred.addCallback(self.assertEqual, None)
379 # XXX: Need implemention of fancy error to exception mapping
380 test_error_missing_server_fault.skip = True
381
382 def test_error_really_fault(self):
383 """A real compute fault is propogated"""
384 self.expect_nova_get("servers/1000/os-security-groups",
385 code=500, response={"computeFault": {
386 "message": "The server has either erred or is incapable of"
387 " performing the requested operation.",
388 "code": 500,
389 }})
390 self.expect_nova_get("servers/1000", response={"server": {"id": 1000}})
391 self.mocker.replay()
392 machine = NovaProviderMachine(1000)
393 deferred = self.get_port_manager().get_machine_groups(machine)
394 return self.assertFailure(deferred, errors.ProviderInteractionError)
395 # XXX: Need implemention of fancy error to exception mapping
396 test_error_really_fault.skip = True
0397
=== added file 'juju/providers/openstack/tests/test_provider.py'
--- juju/providers/openstack/tests/test_provider.py 1970-01-01 00:00:00 +0000
+++ juju/providers/openstack/tests/test_provider.py 2012-07-18 19:47:20 +0000
@@ -0,0 +1,116 @@
1"""Testing for the OpenStack provider interface"""
2
3from juju.lib.testing import TestCase
4from juju.environment.errors import EnvironmentsConfigError
5
6from juju.providers.openstack.files import FileStorage
7from juju.providers.openstack.provider import MachineProvider
8
9
10class ProviderTestCase(TestCase):
11
12 environment_name = "testing"
13
14 _test_environ = {
15 "NOVA_URL": "http://environ.invalid",
16 "NOVA_API_KEY": "env-key",
17 "EC2_SECRET_KEY": "env-xxxx",
18 "NOVA_PROJECT_ID": "env-project",
19 }
20
21 def get_config(self):
22 return {
23 "type": "openstack",
24 "auth-mode": "keypair",
25 "access-key": "key",
26 "secret-key": "xxxxxxxx",
27 "auth-url": "http://testing.invalid",
28 "project-name": "project",
29 "control-bucket": self.environment_name,
30 }
31
32 def test_empty_config_raises(self):
33 """Passing no config raises an exception about lacking credentials"""
34 self.change_environment()
35 # XXX: Should this raise EnvironmentsConfigError instead?
36 self.assertRaises(ValueError,
37 MachineProvider, self.environment_name, {})
38
39 def test_client_params(self):
40 """Config details get passed through to OpenStack client correctly"""
41 config = self.get_config()
42 provider = MachineProvider(self.environment_name, config)
43 creds = provider.credentials
44 self.assertEquals("key", creds.access_key)
45 self.assertEquals("xxxxxxxx", creds.secret_key)
46 self.assertEquals("http://testing.invalid", creds.url)
47 self.assertEquals("project", creds.project_name)
48 self.assertIs(creds, provider.nova._client.credentials)
49 self.assertIs(creds, provider.swift._client.credentials)
50
51 def test_provider_attributes(self):
52 """
53 The provider environment name and config should be available as
54 parameters in the provider.
55 """
56 provider = MachineProvider(self.environment_name, self.get_config())
57 self.assertEqual(provider.environment_name, self.environment_name)
58 self.assertEqual(provider.config.get("type"), "openstack")
59 self.assertEqual(provider.provider_type, "openstack")
60
61 def test_get_file_storage(self):
62 """The file storage is accessible via the machine provider."""
63 provider = MachineProvider(self.environment_name, self.get_config())
64 storage = provider.get_file_storage()
65 self.assertTrue(isinstance(storage, FileStorage))
66
67 def test_config_serialization(self):
68 """
69 The provider configuration can be serialized to yaml.
70 """
71 self.change_environment()
72 config = self.get_config()
73 expected = config.copy()
74 config["authorized-keys-path"] = self.makeFile("key contents")
75 expected["authorized-keys"] = "key contents"
76 provider = MachineProvider(self.environment_name, config)
77 self.assertEqual(expected, provider.get_serialization_data())
78
79 def test_config_environment_extraction(self):
80 """
81 The provider serialization loads keys as needed from the environment.
82
83 Variables from the configuration take precendence over those from
84 the environment, when serializing.
85 """
86 self.change_environment(**self._test_environ)
87 provider = MachineProvider(self.environment_name, {
88 "auth-mode": "keypair",
89 "project-name": "other-project",
90 "authorized-keys": "key-data",
91 })
92 serialized = provider.get_serialization_data()
93 expected = {
94 "auth-mode": "keypair",
95 "access-key": "env-key",
96 "secret-key": "env-xxxx",
97 "auth-url": "http://environ.invalid",
98 "project-name": "other-project",
99 "authorized-keys": "key-data",
100 }
101 self.assertEqual(expected, serialized)
102
103 def test_conflicting_authorized_keys_options(self):
104 """
105 We can't handle two different authorized keys options, so deny
106 constructing an environment that way.
107 """
108 config = self.get_config()
109 config["authorized-keys"] = "File content"
110 config["authorized-keys-path"] = "File path"
111 error = self.assertRaises(EnvironmentsConfigError,
112 MachineProvider, self.environment_name, config)
113 self.assertEquals(
114 str(error),
115 "Environment config cannot define both authorized-keys and "
116 "authorized-keys-path. Pick one!")
0117
=== added file 'juju/providers/openstack/tests/test_shutdown.py'
--- juju/providers/openstack/tests/test_shutdown.py 1970-01-01 00:00:00 +0000
+++ juju/providers/openstack/tests/test_shutdown.py 2012-07-18 19:47:20 +0000
@@ -0,0 +1,130 @@
1"""Tests for terminating machines and cleaning up the environment"""
2
3from juju import errors
4from juju.lib import testing
5from juju.machine import ProviderMachine
6from juju.providers.openstack.machine import NovaProviderMachine
7from juju.providers.openstack.tests import OpenStackTestMixin
8
9
10class ShutdownMachineTests(OpenStackTestMixin, testing.TestCase):
11
12 def test_shutdown_single(self):
13 self.expect_nova_get("servers/1000",
14 response={"server": {
15 'name': "juju testing instance 0",
16 }})
17 self.expect_nova_delete("servers/1000", code=204)
18 self.mocker.replay()
19 machine = NovaProviderMachine(1000)
20 deferred = self.get_provider().shutdown_machine(machine)
21 deferred.addCallback(self.assertIs, machine)
22
23 def test_shutdown_single_other(self):
24 self.expect_nova_get("servers/1000",
25 response={"server": {
26 'name': "some other instance",
27 }})
28 self.mocker.replay()
29 machine = NovaProviderMachine(1000)
30 deferred = self.get_provider().shutdown_machine(machine)
31 return self.assert_not_found(deferred, [1000])
32
33 def test_shutdown_single_wrong_machine(self):
34 self.mocker.reset()
35 machine = ProviderMachine("i-000003E8")
36 e = self.assertRaises(errors.ProviderError,
37 self.get_provider().shutdown_machine, machine)
38 self.assertIn("Need a NovaProviderMachine to shutdown", str(e))
39
40 def test_shutdown_multi_none(self):
41 self.mocker.reset()
42 deferred = self.get_provider().shutdown_machines([])
43 return deferred.addCallback(self.assertEqual, [])
44
45 def test_shutdown_multi_some_invalid(self):
46 """No machines are shutdown if some are invalid"""
47 self.mocker.unorder()
48 self.expect_nova_get("servers/1001",
49 response={"server": {
50 'name': "juju testing instance 1",
51 }})
52 self.expect_nova_get("servers/1002",
53 response={"server": {
54 'name': "some other instance",
55 }})
56 self.mocker.replay()
57 machines = [NovaProviderMachine(1001), NovaProviderMachine(1002)]
58 deferred = self.get_provider().shutdown_machines(machines)
59 return self.assert_not_found(deferred, [1002])
60 # XXX: dumb requirement to keep all running if some invalid, drop this
61 test_shutdown_multi_some_invalid.skip = True
62
63 # GZ 2012-06-11: Corner case difference, EC2 rechecks machine statuses and
64 # group membership on shutdown.
65
66 def test_shutdown_multi_success(self):
67 """Machines are shutdown and groups except juju group are deleted"""
68 self.mocker.unorder()
69 self.expect_nova_get("servers/1001",
70 response={"server": {
71 'name': "juju testing instance 1",
72 }})
73 self.expect_nova_get("servers/1002",
74 response={"server": {
75 'name': "juju testing instance 2",
76 }})
77 self.expect_nova_delete("servers/1001", code=204)
78 self.expect_nova_delete("servers/1002", code=204)
79 self.mocker.replay()
80 machines = [NovaProviderMachine(1001), NovaProviderMachine(1002)]
81 deferred = self.get_provider().shutdown_machines(machines)
82 return deferred
83
84
85class DestroyEnvironmentTests(OpenStackTestMixin, testing.TestCase):
86
87 def check_machine_ids(self, machines, server_ids):
88 self.assertEqual(set(m.instance_id for m in machines), set(server_ids))
89
90 def test_destroy_environment(self):
91 self.mocker.unorder()
92 self.expect_swift_put("testing/provider-state", "{}\n")
93 self.expect_nova_get("servers", response={"servers": [
94 self.make_server(1001),
95 self.make_server(1002),
96 ]})
97 self.expect_nova_get("servers/1001",
98 response={"server": {
99 'name': "juju testing instance 1",
100 }})
101 self.expect_nova_get("servers/1002",
102 response={"server": {
103 'name': "juju testing instance 2",
104 }})
105 self.expect_nova_delete("servers/1001", code=204)
106 self.expect_nova_delete("servers/1002", code=204)
107 self.mocker.replay()
108 deferred = self.get_provider().destroy_environment()
109 return deferred.addCallback(self.check_machine_ids, [1001, 1002])
110
111 def test_s3_failure(self):
112 self.mocker.unorder()
113 self.expect_swift_put("testing/provider-state", "{}\n",
114 code=500, response="Server unavailable or something")
115 # XXX: normal server shutdown should be expected here
116 self.mocker.replay()
117 deferred = self.get_provider().destroy_environment()
118 return deferred.addCallback(self.assertEqual, [])
119 # XXX: Need to bolster swift robustness in response to api errors
120 test_s3_failure.skip = True
121
122 # GZ 2012-06-15: Always tries removing juju group unlike EC2 currently
123 def test_shutdown_no_instances(self):
124 """With no instances no shutdowns are attempted"""
125 self.mocker.unorder()
126 self.expect_swift_put("testing/provider-state", "{}\n")
127 self.expect_nova_get("servers", response={"servers": []})
128 self.mocker.replay()
129 deferred = self.get_provider().destroy_environment()
130 return deferred.addCallback(self.assertEqual, [])
0131
=== added file 'juju/providers/openstack/tests/test_state.py'
--- juju/providers/openstack/tests/test_state.py 1970-01-01 00:00:00 +0000
+++ juju/providers/openstack/tests/test_state.py 2012-07-18 19:47:20 +0000
@@ -0,0 +1,58 @@
1"""Tests for the common state interface over the the Openstack provider
2
3The testcases here largely duplicate those in openstack.tests.test_files as
4state handling is a pretty thin layer over the file storage.
5"""
6
7import yaml
8
9from juju.lib.testing import TestCase
10from juju.providers.openstack.tests import OpenStackTestMixin
11
12
13class OpenStackStateTest(OpenStackTestMixin, TestCase):
14
15 def test_save(self):
16 """Saving a dict puts yaml serialized bytes in provider-state"""
17 state = {"zookeeper-instances": [
18 [1000, "x1.example.com"],
19 ]}
20 self.expect_swift_put("testing/provider-state", yaml.dump(state))
21 self.mocker.replay()
22 return self.get_provider().save_state(state)
23
24 def test_save_missing_container(self):
25 """Saving will create the container when it does not exist already"""
26 state = {"zookeeper-instances": [
27 [1000, "x1.example.com"],
28 ]}
29 state_bytes = yaml.dump(state)
30 self.expect_swift_put("testing/provider-state", state_bytes, code=404)
31 self.expect_swift_put_container("testing")
32 self.expect_swift_put("testing/provider-state", state_bytes)
33 self.mocker.replay()
34 return self.get_provider().save_state(state)
35
36 def test_load(self):
37 """Loading deserializes yaml from provider-state to a python dict"""
38 state = {"zookeeper-instances": []}
39 self.expect_swift_get("testing/provider-state",
40 response=yaml.dump(state))
41 self.mocker.replay()
42 deferred = self.get_provider().load_state()
43 return deferred.addCallback(self.assertEqual, state)
44
45 def test_load_missing(self):
46 """Loading returns False if provider-state does not exist"""
47 self.expect_swift_get("testing/provider-state", code=404,
48 response={})
49 self.mocker.replay()
50 deferred = self.get_provider().load_state()
51 return deferred.addCallback(self.assertIs, False)
52
53 def test_load_no_content(self):
54 """Loading returns False if provider-state is empty"""
55 self.expect_swift_get("testing/provider-state", response="")
56 self.mocker.replay()
57 deferred = self.get_provider().load_state()
58 return deferred.addCallback(self.assertIs, False)
059
=== added directory 'juju/providers/openstack_s3'
=== added file 'juju/providers/openstack_s3/__init__.py'
--- juju/providers/openstack_s3/__init__.py 1970-01-01 00:00:00 +0000
+++ juju/providers/openstack_s3/__init__.py 2012-07-18 19:47:20 +0000
@@ -0,0 +1,48 @@
1"""Provider interface implementation for Openstack with S3 storage"""
2
3import os
4
5from txaws.service import AWSServiceRegion
6
7from juju.providers.openstack import provider as os_provider
8from juju.providers.openstack import credentials
9from juju.providers.ec2 import files as s3_files
10
11
12class HybridCredentials(credentials.OpenStackCredentials):
13 """Encapsulation of credentials with S3 required values included"""
14
15 _config_vars = {
16 'combined-key': ("EC2_ACCESS_KEY", "AWS_ACCESS_KEY_ID"),
17 's3-uri': ("S3_URL",),
18 }
19 _config_vars.update(credentials.OpenStackCredentials._config_vars)
20
21
22class MachineProvider(os_provider.MachineProvider):
23 """MachineProvider for use in Openstack environment but with S3 API"""
24
25 Credentials = HybridCredentials
26
27 def __init__(self, environment_name, config):
28 super(MachineProvider, self).__init__(environment_name, config)
29
30 del self.swift
31
32 # If access or secret keys are still blank, inside txaws environment
33 # a ValueError will be raised after rechecking the environment.
34 self._aws_service = AWSServiceRegion(
35 access_key=self.credentials.combined_key,
36 secret_key=self.credentials.secret_key,
37 ec2_uri="", # The EC2 client will not be used
38 s3_uri=self.credentials.s3_uri)
39
40 self.s3 = self._aws_service.get_s3_client()
41
42 @property
43 def provider_type(self):
44 return "openstack_s3"
45
46 def get_file_storage(self):
47 """Retrieve a S3 API compatible backend FileStorage class"""
48 return s3_files.FileStorage(self.s3, self.config["control-bucket"])
049
=== added directory 'juju/providers/openstack_s3/tests'
=== added file 'juju/providers/openstack_s3/tests/__init__.py'
=== added file 'juju/providers/openstack_s3/tests/test_provider.py'
--- juju/providers/openstack_s3/tests/test_provider.py 1970-01-01 00:00:00 +0000
+++ juju/providers/openstack_s3/tests/test_provider.py 2012-07-18 19:47:20 +0000
@@ -0,0 +1,122 @@
1"""Testing for the OpenStack provider interface"""
2
3from juju.lib.testing import TestCase
4from juju.environment.errors import EnvironmentsConfigError
5
6from juju.providers.ec2.files import FileStorage
7from juju.providers.openstack_s3 import MachineProvider
8
9
10class ProviderTestCase(TestCase):
11
12 environment_name = "testing"
13
14 _test_environ = {
15 "NOVA_URL": "http://environ.invalid",
16 "NOVA_API_KEY": "env-key",
17 "EC2_ACCESS_KEY": "env-key:env-project",
18 "EC2_SECRET_KEY": "env-xxxx",
19 "NOVA_PROJECT_ID": "env-project",
20 "S3_URL": "http://environ.invalid:3333",
21 }
22
23 def get_config(self):
24 return {
25 "type": "openstack_s3",
26 "auth-mode": "keypair",
27 "access-key": "key",
28 "secret-key": "xxxxxxxx",
29 "auth-url": "http://testing.invalid",
30 "project-name": "project",
31 "control-bucket": self.environment_name,
32 "combined-key": "key:project",
33 "s3-uri": "http://testing.invalid:3333",
34 }
35
36 def test_client_params(self):
37 """Config details get passed through to OpenStack client correctly"""
38 config = self.get_config()
39 provider = MachineProvider(self.environment_name, config)
40 creds = provider.credentials
41 self.assertEquals("key", creds.access_key)
42 self.assertEquals("xxxxxxxx", creds.secret_key)
43 self.assertEquals("http://testing.invalid", creds.url)
44 self.assertEquals("project", creds.project_name)
45 self.assertIs(creds, provider.nova._client.credentials)
46
47 def test_s3_params(self):
48 """Config details get passed through to txaws S3 client correctly"""
49 config = self.get_config()
50 s3 = MachineProvider(self.environment_name, config).s3
51 self.assertEquals("http://testing.invalid:3333/", s3.endpoint.get_uri())
52 self.assertEquals("key:project", s3.creds.access_key)
53 self.assertEquals("xxxxxxxx", s3.creds.secret_key)
54
55 def test_provider_attributes(self):
56 """
57 The provider environment name and config should be available as
58 parameters in the provider.
59 """
60 provider = MachineProvider(self.environment_name, self.get_config())
61 self.assertEqual(provider.environment_name, self.environment_name)
62 self.assertEqual(provider.config.get("type"), "openstack_s3")
63 self.assertEqual(provider.provider_type, "openstack_s3")
64
65 def test_get_file_storage(self):
66 """The file storage is accessible via the machine provider."""
67 provider = MachineProvider(self.environment_name, self.get_config())
68 storage = provider.get_file_storage()
69 self.assertTrue(isinstance(storage, FileStorage))
70
71 def test_config_serialization(self):
72 """
73 The provider configuration can be serialized to yaml.
74 """
75 self.change_environment()
76 config = self.get_config()
77 expected = config.copy()
78 config["authorized-keys-path"] = self.makeFile("key contents")
79 expected["authorized-keys"] = "key contents"
80 provider = MachineProvider(self.environment_name, config)
81 self.assertEqual(expected, provider.get_serialization_data())
82
83 def test_config_environment_extraction(self):
84 """
85 The provider serialization loads keys as needed from the environment.
86
87 Variables from the configuration take precendence over those from
88 the environment, when serializing.
89 """
90 self.change_environment(**self._test_environ)
91 provider = MachineProvider(self.environment_name, {
92 "auth-mode": "keypair",
93 "project-name": "other-project",
94 "authorized-keys": "key-data",
95 })
96 serialized = provider.get_serialization_data()
97 expected = {
98 "auth-mode": "keypair",
99 "access-key": "env-key",
100 "secret-key": "env-xxxx",
101 "auth-url": "http://environ.invalid",
102 "project-name": "other-project",
103 "authorized-keys": "key-data",
104 "combined-key": "env-key:env-project",
105 "s3-uri": "http://environ.invalid:3333",
106 }
107 self.assertEqual(expected, serialized)
108
109 def test_conflicting_authorized_keys_options(self):
110 """
111 We can't handle two different authorized keys options, so deny
112 constructing an environment that way.
113 """
114 config = self.get_config()
115 config["authorized-keys"] = "File content"
116 config["authorized-keys-path"] = "File path"
117 error = self.assertRaises(EnvironmentsConfigError,
118 MachineProvider, self.environment_name, config)
119 self.assertEquals(
120 str(error),
121 "Environment config cannot define both authorized-keys and "
122 "authorized-keys-path. Pick one!")
0123
=== modified file 'juju/state/initialize.py'
--- juju/state/initialize.py 2012-03-29 01:37:57 +0000
+++ juju/state/initialize.py 2012-07-18 19:47:20 +0000
@@ -30,7 +30,10 @@
30 """30 """
31 self.client = client31 self.client = client
32 self.admin_identity = admin_identity32 self.admin_identity = admin_identity
33 if instance_id.isdigit():
34 instance_id = int(instance_id)
33 self.instance_id = instance_id35 self.instance_id = instance_id
36
34 self.constraints_data = constraints_data37 self.constraints_data = constraints_data
35 self.provider_type = provider_type38 self.provider_type = provider_type
3639
@@ -57,6 +60,7 @@
57 # Poke constraints data into a machine state to represent this machine.60 # Poke constraints data into a machine state to represent this machine.
58 manager = MachineStateManager(self.client)61 manager = MachineStateManager(self.client)
59 machine_state = yield manager.add_machine_state(constraints)62 machine_state = yield manager.add_machine_state(constraints)
63
60 yield machine_state.set_instance_id(self.instance_id)64 yield machine_state.set_instance_id(self.instance_id)
6165
62 # Set up environment constraints similarly.66 # Set up environment constraints similarly.
6367
=== modified file 'juju/unit/address.py'
--- juju/unit/address.py 2012-03-22 09:08:12 +0000
+++ juju/unit/address.py 2012-07-18 19:47:20 +0000
@@ -16,6 +16,8 @@
16 provider_type = yield settings.get_provider_type()16 provider_type = yield settings.get_provider_type()
17 if provider_type == "ec2":17 if provider_type == "ec2":
18 returnValue(EC2UnitAddress())18 returnValue(EC2UnitAddress())
19 if provider_type in ("openstack", "openstack_s3"):
20 returnValue(OpenStackUnitAddress())
19 elif provider_type == "local":21 elif provider_type == "local":
20 returnValue(LocalUnitAddress())22 returnValue(LocalUnitAddress())
21 elif provider_type == "orchestra":23 elif provider_type == "orchestra":
@@ -24,7 +26,6 @@
24 returnValue(DummyUnitAddress())26 returnValue(DummyUnitAddress())
25 elif provider_type == "maas":27 elif provider_type == "maas":
26 returnValue(MAASUnitAddress())28 returnValue(MAASUnitAddress())
27
28 raise JujuError(29 raise JujuError(
29 "Unknown provider type: %r, unit addresses unknown." % provider_type)30 "Unknown provider type: %r, unit addresses unknown." % provider_type)
3031
@@ -32,10 +33,10 @@
32class UnitAddress(object):33class UnitAddress(object):
3334
34 def get_private_address(self):35 def get_private_address(self):
35 raise NotImplemented()36 raise NotImplementedError(self.get_private_address)
3637
37 def get_public_address(self):38 def get_public_address(self):
38 raise NotImplemented()39 raise NotImplementedError(self.get_public_address)
3940
4041
41class DummyUnitAddress(UnitAddress):42class DummyUnitAddress(UnitAddress):
@@ -62,6 +63,27 @@
62 returnValue(content.strip())63 returnValue(content.strip())
6364
6465
66class OpenStackUnitAddress(UnitAddress):
67 """Address determination of a service unit on an OpenStack server
68
69 Unlike EC2 there are no promises that an instance will have a resolvable
70 hostname, or for that matter a public ip address.
71 """
72
73 def _get_metadata_string(self, key):
74 return client.getPage("http://169.254.169.254/1.0/meta-data/" + key)
75
76 def get_private_address(self):
77 return self._get_metadata_string("local-ipv4")
78
79 @inlineCallbacks
80 def get_public_address(self):
81 address = yield self._get_metadata_string("public-ipv4")
82 if not address:
83 address = yield self.get_private_address()
84 returnValue(address)
85
86
65class LocalUnitAddress(UnitAddress):87class LocalUnitAddress(UnitAddress):
6688
67 def get_private_address(self):89 def get_private_address(self):
@@ -87,6 +109,7 @@
87 output = subprocess.check_output(["hostname", "-f"])109 output = subprocess.check_output(["hostname", "-f"])
88 return output.strip()110 return output.strip()
89111
112
90class MAASUnitAddress(UnitAddress):113class MAASUnitAddress(UnitAddress):
91114
92 def get_private_address(self):115 def get_private_address(self):
93116
=== modified file 'juju/unit/tests/test_address.py'
--- juju/unit/tests/test_address.py 2012-03-22 09:08:12 +0000
+++ juju/unit/tests/test_address.py 2012-07-18 19:47:20 +0000
@@ -8,7 +8,7 @@
8from juju.lib.testing import TestCase8from juju.lib.testing import TestCase
9from juju.unit.address import (9from juju.unit.address import (
10 EC2UnitAddress, LocalUnitAddress, OrchestraUnitAddress, DummyUnitAddress,10 EC2UnitAddress, LocalUnitAddress, OrchestraUnitAddress, DummyUnitAddress,
11 MAASUnitAddress, get_unit_address)11 MAASUnitAddress, OpenStackUnitAddress, UnitAddress, get_unit_address)
12from juju.state.environment import GlobalSettingsStateManager12from juju.state.environment import GlobalSettingsStateManager
1313
1414
@@ -32,6 +32,16 @@
32 self.assertTrue(isinstance(address, EC2UnitAddress))32 self.assertTrue(isinstance(address, EC2UnitAddress))
3333
34 @inlineCallbacks34 @inlineCallbacks
35 def test_get_openstack_address(self):
36 address = yield self.get_address_for("openstack")
37 self.assertTrue(isinstance(address, OpenStackUnitAddress))
38
39 @inlineCallbacks
40 def test_get_openstack_s3_address(self):
41 address = yield self.get_address_for("openstack_s3")
42 self.assertTrue(isinstance(address, OpenStackUnitAddress))
43
44 @inlineCallbacks
35 def test_get_local_address(self):45 def test_get_local_address(self):
36 address = yield self.get_address_for("local")46 address = yield self.get_address_for("local")
37 self.assertTrue(isinstance(address, LocalUnitAddress))47 self.assertTrue(isinstance(address, LocalUnitAddress))
@@ -55,6 +65,22 @@
55 return self.assertFailure(self.get_address_for("foobar"), JujuError)65 return self.assertFailure(self.get_address_for("foobar"), JujuError)
5666
5767
68class SubclassAddressTest(TestCase):
69
70 class TestingAddress(UnitAddress):
71 """An address class that neglects to implement the required methods"""
72
73 def test_get_public_address(self):
74 err = self.assertRaises(NotImplementedError,
75 self.TestingAddress().get_public_address)
76 self.assertIn("TestingAddress.get_public_address", str(err))
77
78 def test_get_private_address(self):
79 err = self.assertRaises(NotImplementedError,
80 self.TestingAddress().get_private_address)
81 self.assertIn("TestingAddress.get_private_address", str(err))
82
83
58class DummyAddressTest(TestCase):84class DummyAddressTest(TestCase):
5985
60 def setUp(self):86 def setUp(self):
@@ -93,6 +119,35 @@
93 (yield self.address.get_public_address()), "foobar")119 (yield self.address.get_public_address()), "foobar")
94120
95121
122class OpenStackAddressTest(TestCase):
123
124 def setUp(self):
125 self.address = OpenStackUnitAddress()
126 self.patch(client, "getPage", self._fetch_metadata)
127
128 def _fetch_metadata(self, url):
129 head, tail = url.rsplit("/", 1)
130 self.assertEqual("http://169.254.169.254/1.0/meta-data", head)
131 return succeed(self.meta.pop(tail))
132
133 @inlineCallbacks
134 def test_get_private_address(self):
135 self.meta = {"local-ipv4": "192.168.0.2"}
136 self.assertEqual("192.168.0.2",
137 (yield self.address.get_private_address()))
138
139 @inlineCallbacks
140 def test_get_public_address_present(self):
141 self.meta = {"public-ipv4": "8.8.8.8"}
142 self.assertEqual("8.8.8.8", (yield self.address.get_public_address()))
143
144 @inlineCallbacks
145 def test_get_public_address_missing(self):
146 self.meta = {"public-ipv4": "", "local-ipv4": "192.168.0.2"}
147 self.assertEqual("192.168.0.2",
148 (yield self.address.get_public_address()))
149
150
96class LocalAddressTest(TestCase):151class LocalAddressTest(TestCase):
97152
98 def setUp(self):153 def setUp(self):

Subscribers

People subscribed via source and target branches

to status/vote changes: