Merge lp:~oubiwann/txaws/416109-arbitrary-endpoints into lp:~txawsteam/txaws/trunk
- 416109-arbitrary-endpoints
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Merge reported by: | Duncan McGreggor | ||||
Merged at revision: | not available | ||||
Proposed branch: | lp:~oubiwann/txaws/416109-arbitrary-endpoints | ||||
Merge into: | lp:~txawsteam/txaws/trunk | ||||
Diff against target: | None lines | ||||
To merge this branch: | bzr merge lp:~oubiwann/txaws/416109-arbitrary-endpoints | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jamu Kakar (community) | Approve | ||
Thomas Herve (community) | Approve | ||
Review via email: mp+10671@code.launchpad.net |
This proposal supersedes a proposal from 2009-08-23.
Commit message
Description of the change
Duncan McGreggor (oubiwann) wrote : Posted in a previous version of this proposal | # |
Robert Collins (lifeless) wrote : Posted in a previous version of this proposal | # |
On Thu, 2009-08-20 at 19:25 +0000, Duncan McGreggor wrote:
> Duncan McGreggor has proposed merging
> lp:~oubiwann/txaws/416109-arbitrary-endpoints into lp:txaws.
>
> Requested reviews:
> txAWS Team (txawsteam)
>
> This branch adds support for a service object that manages host
> endpoints as well as authorization keys (thus obviating the need for
> the AWSCredential object).
Lets be careful to keep space under storage, ec2 etc for server
components. storage.service isn't really a storage service :) Lets call
the description of an end point AWSServiceEndpoint, or something like
that.
local and credentials appear orthogonal to me - for instance,
EC2 EU and EC2 US are different endpoints/services with common
credentials. I think conflating them is unnecessary and undesirable.
Further to that, the AWSCredentials are usable on related services in a
single region - EC2, S3 and so on, so when we're passing around a
description, we probably want to have a region that describes the
endpoints for a collection of services. The goal being able to have a
static object
AWS_US1 = #...
AWS_US2 = #...
and for people to make their own;
my_eucalyptus_
At runtime then, one would ask a region for a client of a particular
service, using some credentials.
AWS_US1.
AWS_US1.
etc.
We could do this without changing the existing clients at all, by just
storing scheme,host tuples in a AWSRegion - but I think it is cleaner to
do the sort of refactoring you have done. I think it would be best by
having an AWSServiceEndpoint which has the scheme and url, and keeping
the creds separate. For instance,
class AWSServiceRegion:
def make_ec2_
return EC2Client(
Also a bit of detail review - 'default_schema = https' - in URL terms
(see http://
_schema_.
review needsfixing
Duncan McGreggor (oubiwann) wrote : Posted in a previous version of this proposal | # |
> On Thu, 2009-08-20 at 19:25 +0000, Duncan McGreggor wrote:
> > Duncan McGreggor has proposed merging
> > lp:~oubiwann/txaws/416109-arbitrary-endpoints into lp:txaws.
> >
> > Requested reviews:
> > txAWS Team (txawsteam)
> >
> > This branch adds support for a service object that manages host
> > endpoints as well as authorization keys (thus obviating the need for
> > the AWSCredential object).
>
>
> Lets be careful to keep space under storage, ec2 etc for server
> components. storage.service isn't really a storage service :) Lets call
> the description of an end point AWSServiceEndpoint, or something like
> that.
>
> local and credentials appear orthogonal to me - for instance,
> EC2 EU and EC2 US are different endpoints/services with common
> credentials. I think conflating them is unnecessary and undesirable.
> Further to that, the AWSCredentials are usable on related services in a
> single region - EC2, S3 and so on, so when we're passing around a
> description, we probably want to have a region that describes the
> endpoints for a collection of services. The goal being able to have a
> static object
> AWS_US1 = #...
> AWS_US2 = #...
> and for people to make their own;
> my_eucalyptus_
>
> At runtime then, one would ask a region for a client of a particular
> service, using some credentials.
>
> AWS_US1.
> AWS_US1.
>
> etc.
>
> We could do this without changing the existing clients at all, by just
> storing scheme,host tuples in a AWSRegion - but I think it is cleaner to
> do the sort of refactoring you have done. I think it would be best by
> having an AWSServiceEndpoint which has the scheme and url, and keeping
> the creds separate. For instance,
> class AWSServiceRegion:
> def make_ec2_
> return EC2Client(
>
> Also a bit of detail review - 'default_schema = https' - in URL terms
> (see http://
> _schema_.
>
> review needsfixing
+1 on these suggestions. I'll give it another go with this in mind.
Duncan McGreggor (oubiwann) wrote : Posted in a previous version of this proposal | # |
> On Thu, 2009-08-20 at 19:25 +0000, Duncan McGreggor wrote:
> > Duncan McGreggor has proposed merging
> > lp:~oubiwann/txaws/416109-arbitrary-endpoints into lp:txaws.
> >
> > Requested reviews:
> > txAWS Team (txawsteam)
> >
> > This branch adds support for a service object that manages host
> > endpoints as well as authorization keys (thus obviating the need for
> > the AWSCredential object).
>
>
> Lets be careful to keep space under storage, ec2 etc for server
> components. storage.service isn't really a storage service :) Lets call
> the description of an end point AWSServiceEndpoint, or something like
> that.
[1] Renamed.
> local and credentials appear orthogonal to me - for instance,
> EC2 EU and EC2 US are different endpoints/services with common
> credentials. I think conflating them is unnecessary and undesirable.
> Further to that, the AWSCredentials are usable on related services in a
> single region - EC2, S3 and so on, so when we're passing around a
> description, we probably want to have a region that describes the
> endpoints for a collection of services.
[2]
Brought the credentials back into the source. Pulled credential code out of service endpoint code.
> The goal being able to have a
> static object
> AWS_US1 = #...
> AWS_US2 = #...
> and for people to make their own;
> my_eucalyptus_
>
> At runtime then, one would ask a region for a client of a particular
> service, using some credentials.
>
> AWS_US1.
> AWS_US1.
>
> etc.
>
> We could do this without changing the existing clients at all, by just
> storing scheme,host tuples in a AWSRegion - but I think it is cleaner to
> do the sort of refactoring you have done. I think it would be best by
> having an AWSServiceEndpoint which has the scheme and url, and keeping
> the creds separate. For instance,
> class AWSServiceRegion:
> def make_ec2_
> return EC2Client(
[3]
I'm got an implementation of this in place right now. It ended up pretty similar to what you suggested. There are some missing unit tests right now -- I'll be hitting those this afternoon.
> Also a bit of detail review - 'default_schema = https' - in URL terms
> (see http://
> _schema_.
[4]
Ugh, thanks. The first place I wrote it was good, then I copied a typo everywhere else. Fixed.
Duncan McGreggor (oubiwann) wrote : Posted in a previous version of this proposal | # |
Okay! Just pushed up the latest code for the missing unit tests. It's ready for another review :-)
Robert Collins (lifeless) wrote : Posted in a previous version of this proposal | # |
On Sun, 2009-08-23 at 21:30 +0000, Robert Collins wrote:
> === modified file 'txaws/
> --- txaws/client/
> +++ txaws/client/
> @@ -8,7 +8,7 @@
> import gobject
> import gtk
>
> -from txaws.credentials import AWSCredentials
> +from txaws.ec2.service import EC2Service
>
>
> __all__ = ['main']
> @@ -27,10 +27,10 @@
> # Nested import because otherwise we get 'reactor already
> installed'.
> self.password_
> try:
> - creds = AWSCredentials()
> + service = AWSService()
This is going to be a NameError :P
...
> === added file 'txaws/
this file already exists. I think you've done something weird in your
branch. lets review once thats fixed. Find me in #bzr :P
review needsfixing
--
- 32. By Duncan McGreggor
-
- Added access and secret key parameters to the AWSServiceRegion constructor.
- Updated AWSServiceRegion to create creds based on access and secret key if no
creds are supplied.
- Updated docstrings. - 33. By Duncan McGreggor
-
Added a uri parameter for service region creation to ease the creation of
service region objects with non-Amazon endpoints (e.g., in Landscape). - 34. By Duncan McGreggor
-
Swapped the ordering of an import to be in alphabetical order.
- 35. By Duncan McGreggor
-
- Fixed the creds parameter in the get_ec2_client method.
- Removed redundant code in check_parsed_instances.
- Created a testing subpackage for generally useful testing classes.
- Added fake ec2 client and region classes.
- Moved base test case into new testing module.
Thomas Herve (therve) wrote : | # |
[1] Some flakes
txaws/ec2/
txaws/storage/
txaws/storage/
txaws/storage/
txaws/tests/
[2]
+ try:
+ gtk2reactor.
+ except AssertionError:
+ pass
Really? What's the reason behind this change?
[3]
key_id = content.
creds = AWSCredentials(
+ region = AWSServiceRegio
This change seems unecessary.
[4]
+from twisted.web.client import _parse
It's pretty sad, but you can't import that, it's private. You have to copy the function.
[5]
+ self.assertEqua
+
+
+ def test_get_
There is an extra blank line here.
Nice branch, +1!
Jamu Kakar (jkakar) wrote : | # |
[1]
+ def get_s3_
+ raise NotImplementedError
+
+ def get_simpledb_
+ raise NotImplementedError
+
+ def get_sqs_
+ raise NotImplementedError
I think you should leave these out since they don't add any useful
functionality.
[2]
You have:
+ self.endpoint = endpoint or self.get_uri()
and then a bit later:
def get_uri(self):
- return self.root_uri + self.get_uri_path()
+ self.endpoint.
+ return self.endpoint.
When endpoint is None, the default situation, an AttributeError will
be raised because self.endpoint is not defined when get_uri tries to
access it. It looks like get_uri should be instantiating and
returning a new endpoint instance, instead of mutating the existing
one. It would be good to add a test to ensure defaults are handled
properly.
[3]
+ def stash_environ(
+ self.orig_environ = dict(os.environ)
+ self.addCleanup
+ if 'AWS_ACCESS_KEY_ID' in os.environ:
+ del os.environ[
+ if 'AWS_SECRET_
+ del os.environ[
+
+ def restore_
+ for key in set(os.environ) - set(self.
+ del os.environ[key]
+ os.environ.
I think these methods should be private. It's not safe for a user
to call stash_environ during a test because it will rewrite
self.orig_environ with the test environment, defeating the purpose
of this code.
If you have already seen it you may want to check out
https:/
for writing helpers that provide per-test services. It could be
useful for doing the kind of thing you're doing above (though maybe
testscenarios is more relevant here?).
[4]
+ def __init__(self, creds, endpoint, instances=[]):
The default list value here will be shared by all FakeEC2Client
instances, which could cause tricky bugs. I recommend you use None
as the default here along with 'self.instances = instances or []'.
[5]
+ def setUp(self):
+ self.addCleanup
+
+ def clean_environme
+ if os.environ.
+ del os.environ[
+ if os.environ.
+ del os.environ[
This code is unnecessary given the {stash,
in TXAWSTestCase.
Looks good, +1!
- 36. By Duncan McGreggor
-
- Removed unimplemented methods (jkakar 1).
- Made environment mutation methods private (jkakar 3).
- Tweaked the default values for the FakeEC2Client (jkakar 4).
- Removed unnecessary test case methods (jkakar 5). - 37. By Duncan McGreggor
-
Tweaked the storage request object's enpoint/uri stuff and added some unit
tests (jkakar 2). - 38. By Duncan McGreggor
-
Removed unnecessary region instantiation (therve 3).
Added parse utility function (therve 4). - 39. By Duncan McGreggor
-
Fixed pyflakes (therve 1).
Duncan McGreggor (oubiwann) wrote : | # |
> [1] Some flakes
> txaws/ec2/
> txaws/storage/
> txaws/storage/
> txaws/storage/
> txaws/tests/
Fixed.
> [2]
> + try:
> + gtk2reactor.
> + except AssertionError:
> + pass
>
> Really? What's the reason behind this change?
I was getting errors in the unit tests without this. I've just commented it out, and run it again, but I'm not longer getting the errors. Dunno. Removed.
> [3]
> key_id = content.
> secret_key =
> content.
> creds = AWSCredentials(
> + region = AWSServiceRegio
> self.create_
> gnomekeyring.
> None,
>
> This change seems unecessary.
Yup, it is. Removed.
> [4]
> +from twisted.web.client import _parse
>
> It's pretty sad, but you can't import that, it's private. You have to copy the
> function.
Copied this function and its unit tests from twisted.
> [5]
> + self.assertEqua
> +
> +
> + def test_get_
>
> There is an extra blank line here.
This was removed with the changes I made from Jamu's comments.
- 40. By Duncan McGreggor
-
Removed unneeded try/except block in gtk client (therve 2).
Preview Diff
1 | === modified file 'txaws/client/gui/gtk.py' | |||
2 | --- txaws/client/gui/gtk.py 2009-08-18 22:53:53 +0000 | |||
3 | +++ txaws/client/gui/gtk.py 2009-08-25 16:04:05 +0000 | |||
4 | @@ -9,6 +9,7 @@ | |||
5 | 9 | import gtk | 9 | import gtk |
6 | 10 | 10 | ||
7 | 11 | from txaws.credentials import AWSCredentials | 11 | from txaws.credentials import AWSCredentials |
8 | 12 | from txaws.service import AWSServiceRegion | ||
9 | 12 | 13 | ||
10 | 13 | 14 | ||
11 | 14 | __all__ = ['main'] | 15 | __all__ = ['main'] |
12 | @@ -30,6 +31,7 @@ | |||
13 | 30 | creds = AWSCredentials() | 31 | creds = AWSCredentials() |
14 | 31 | except ValueError: | 32 | except ValueError: |
15 | 32 | creds = self.from_gnomekeyring() | 33 | creds = self.from_gnomekeyring() |
16 | 34 | self.region = AWSServiceRegion(creds) | ||
17 | 33 | self.create_client(creds) | 35 | self.create_client(creds) |
18 | 34 | menu = ''' | 36 | menu = ''' |
19 | 35 | <ui> | 37 | <ui> |
20 | @@ -55,9 +57,8 @@ | |||
21 | 55 | self.connect('popup-menu', self.on_popup_menu) | 57 | self.connect('popup-menu', self.on_popup_menu) |
22 | 56 | 58 | ||
23 | 57 | def create_client(self, creds): | 59 | def create_client(self, creds): |
24 | 58 | from txaws.ec2.client import EC2Client | ||
25 | 59 | if creds is not None: | 60 | if creds is not None: |
27 | 60 | self.client = EC2Client(creds=creds) | 61 | self.client = self.region.get_ec2_client() |
28 | 61 | self.on_activate(None) | 62 | self.on_activate(None) |
29 | 62 | else: | 63 | else: |
30 | 63 | # waiting on user entered credentials. | 64 | # waiting on user entered credentials. |
31 | @@ -134,6 +135,7 @@ | |||
32 | 134 | key_id = content.get_children()[0].get_children()[1].get_text() | 135 | key_id = content.get_children()[0].get_children()[1].get_text() |
33 | 135 | secret_key = content.get_children()[1].get_children()[1].get_text() | 136 | secret_key = content.get_children()[1].get_children()[1].get_text() |
34 | 136 | creds = AWSCredentials(access_key=key_id, secret_key=secret_key) | 137 | creds = AWSCredentials(access_key=key_id, secret_key=secret_key) |
35 | 138 | region = AWSServiceRegion(creds=creds) | ||
36 | 137 | self.create_client(creds) | 139 | self.create_client(creds) |
37 | 138 | gnomekeyring.item_create_sync( | 140 | gnomekeyring.item_create_sync( |
38 | 139 | None, | 141 | None, |
39 | @@ -196,7 +198,10 @@ | |||
40 | 196 | """ | 198 | """ |
41 | 197 | if reactor is None: | 199 | if reactor is None: |
42 | 198 | from twisted.internet import gtk2reactor | 200 | from twisted.internet import gtk2reactor |
44 | 199 | gtk2reactor.install() | 201 | try: |
45 | 202 | gtk2reactor.install() | ||
46 | 203 | except AssertionError: | ||
47 | 204 | pass | ||
48 | 200 | from twisted.internet import reactor | 205 | from twisted.internet import reactor |
49 | 201 | status = AWSStatusIcon(reactor) | 206 | status = AWSStatusIcon(reactor) |
50 | 202 | gobject.set_application_name('aws-status') | 207 | gobject.set_application_name('aws-status') |
51 | 203 | 208 | ||
52 | === modified file 'txaws/credentials.py' | |||
53 | --- txaws/credentials.py 2009-08-17 11:18:56 +0000 | |||
54 | +++ txaws/credentials.py 2009-08-25 14:36:20 +0000 | |||
55 | @@ -5,32 +5,37 @@ | |||
56 | 5 | 5 | ||
57 | 6 | import os | 6 | import os |
58 | 7 | 7 | ||
60 | 8 | from txaws.util import * | 8 | from txaws.util import hmac_sha1 |
61 | 9 | 9 | ||
62 | 10 | 10 | ||
63 | 11 | __all__ = ['AWSCredentials'] | 11 | __all__ = ['AWSCredentials'] |
64 | 12 | 12 | ||
65 | 13 | 13 | ||
66 | 14 | ENV_ACCESS_KEY = "AWS_ACCESS_KEY_ID" | ||
67 | 15 | ENV_SECRET_KEY = "AWS_SECRET_ACCESS_KEY" | ||
68 | 16 | |||
69 | 17 | |||
70 | 14 | class AWSCredentials(object): | 18 | class AWSCredentials(object): |
71 | 15 | 19 | ||
73 | 16 | def __init__(self, access_key=None, secret_key=None): | 20 | def __init__(self, access_key="", secret_key=""): |
74 | 17 | """Create an AWSCredentials object. | 21 | """Create an AWSCredentials object. |
75 | 18 | 22 | ||
77 | 19 | :param access_key: The access key to use. If None the environment | 23 | @param access_key: The access key to use. If None the environment |
78 | 20 | variable AWS_ACCESS_KEY_ID is consulted. | 24 | variable AWS_ACCESS_KEY_ID is consulted. |
80 | 21 | :param secret_key: The secret key to use. If None the environment | 25 | @param secret_key: The secret key to use. If None the environment |
81 | 22 | variable AWS_SECRET_ACCESS_KEY is consulted. | 26 | variable AWS_SECRET_ACCESS_KEY is consulted. |
82 | 23 | """ | 27 | """ |
83 | 28 | self.access_key = access_key | ||
84 | 24 | self.secret_key = secret_key | 29 | self.secret_key = secret_key |
94 | 25 | if self.secret_key is None: | 30 | if not self.access_key: |
95 | 26 | self.secret_key = os.environ.get('AWS_SECRET_ACCESS_KEY') | 31 | self.access_key = os.environ.get(ENV_ACCESS_KEY) |
96 | 27 | if self.secret_key is None: | 32 | if not self.access_key: |
97 | 28 | raise ValueError('Could not find AWS_SECRET_ACCESS_KEY') | 33 | raise ValueError("Could not find %s" % ENV_ACCESS_KEY) |
98 | 29 | self.access_key = access_key | 34 | # perform checks for secret key |
99 | 30 | if self.access_key is None: | 35 | if not self.secret_key: |
100 | 31 | self.access_key = os.environ.get('AWS_ACCESS_KEY_ID') | 36 | self.secret_key = os.environ.get(ENV_SECRET_KEY) |
101 | 32 | if self.access_key is None: | 37 | if not self.secret_key: |
102 | 33 | raise ValueError('Could not find AWS_ACCESS_KEY_ID') | 38 | raise ValueError("Could not find %s" % ENV_SECRET_KEY) |
103 | 34 | 39 | ||
104 | 35 | def sign(self, bytes): | 40 | def sign(self, bytes): |
105 | 36 | """Sign some bytes.""" | 41 | """Sign some bytes.""" |
106 | 37 | 42 | ||
107 | === modified file 'txaws/ec2/client.py' | |||
108 | --- txaws/ec2/client.py 2009-08-21 03:26:35 +0000 | |||
109 | +++ txaws/ec2/client.py 2009-08-25 16:04:05 +0000 | |||
110 | @@ -8,7 +8,8 @@ | |||
111 | 8 | 8 | ||
112 | 9 | from twisted.web.client import getPage | 9 | from twisted.web.client import getPage |
113 | 10 | 10 | ||
115 | 11 | from txaws import credentials | 11 | from txaws.credentials import AWSCredentials |
116 | 12 | from txaws.service import AWSServiceEndpoint | ||
117 | 12 | from txaws.util import iso8601time, XML | 13 | from txaws.util import iso8601time, XML |
118 | 13 | 14 | ||
119 | 14 | 15 | ||
120 | @@ -77,16 +78,16 @@ | |||
121 | 77 | 78 | ||
122 | 78 | name_space = '{http://ec2.amazonaws.com/doc/2008-12-01/}' | 79 | name_space = '{http://ec2.amazonaws.com/doc/2008-12-01/}' |
123 | 79 | 80 | ||
125 | 80 | def __init__(self, creds=None, query_factory=None): | 81 | def __init__(self, creds=None, endpoint=None, query_factory=None): |
126 | 81 | """Create an EC2Client. | 82 | """Create an EC2Client. |
127 | 82 | 83 | ||
130 | 83 | @param creds: Explicit credentials to use. If None, credentials are | 84 | @param creds: User authentication credentials to use. |
131 | 84 | inferred as per txaws.credentials.AWSCredentials. | 85 | @param endpoint: The service endpoint URI. |
132 | 86 | @param query_factory: The class or function that produces a query | ||
133 | 87 | object for making requests to the EC2 service. | ||
134 | 85 | """ | 88 | """ |
139 | 86 | if creds is None: | 89 | self.creds = creds or AWSCredentials() |
140 | 87 | self.creds = credentials.AWSCredentials() | 90 | self.endpoint = endpoint or AWSServiceEndpoint() |
137 | 88 | else: | ||
138 | 89 | self.creds = creds | ||
141 | 90 | if query_factory is None: | 91 | if query_factory is None: |
142 | 91 | self.query_factory = Query | 92 | self.query_factory = Query |
143 | 92 | else: | 93 | else: |
144 | @@ -94,7 +95,7 @@ | |||
145 | 94 | 95 | ||
146 | 95 | def describe_instances(self): | 96 | def describe_instances(self): |
147 | 96 | """Describe current instances.""" | 97 | """Describe current instances.""" |
149 | 97 | q = self.query_factory('DescribeInstances', self.creds) | 98 | q = self.query_factory('DescribeInstances', self.creds, self.endpoint) |
150 | 98 | d = q.submit() | 99 | d = q.submit() |
151 | 99 | return d.addCallback(self._parse_instances) | 100 | return d.addCallback(self._parse_instances) |
152 | 100 | 101 | ||
153 | @@ -177,7 +178,8 @@ | |||
154 | 177 | instanceset = {} | 178 | instanceset = {} |
155 | 178 | for pos, instance_id in enumerate(instance_ids): | 179 | for pos, instance_id in enumerate(instance_ids): |
156 | 179 | instanceset["InstanceId.%d" % (pos+1)] = instance_id | 180 | instanceset["InstanceId.%d" % (pos+1)] = instance_id |
158 | 180 | q = self.query_factory('TerminateInstances', self.creds, instanceset) | 181 | q = self.query_factory('TerminateInstances', self.creds, self.endpoint, |
159 | 182 | instanceset) | ||
160 | 181 | d = q.submit() | 183 | d = q.submit() |
161 | 182 | return d.addCallback(self._parse_terminate_instances) | 184 | return d.addCallback(self._parse_terminate_instances) |
162 | 183 | 185 | ||
163 | @@ -200,24 +202,24 @@ | |||
164 | 200 | class Query(object): | 202 | class Query(object): |
165 | 201 | """A query that may be submitted to EC2.""" | 203 | """A query that may be submitted to EC2.""" |
166 | 202 | 204 | ||
168 | 203 | def __init__(self, action, creds, other_params=None, time_tuple=None): | 205 | def __init__(self, action, creds, endpoint, other_params=None, |
169 | 206 | time_tuple=None): | ||
170 | 204 | """Create a Query to submit to EC2.""" | 207 | """Create a Query to submit to EC2.""" |
171 | 208 | self.creds = creds | ||
172 | 209 | self.endpoint = endpoint | ||
173 | 205 | # Require params (2008-12-01 API): | 210 | # Require params (2008-12-01 API): |
174 | 206 | # Version, SignatureVersion, SignatureMethod, Action, AWSAccessKeyId, | 211 | # Version, SignatureVersion, SignatureMethod, Action, AWSAccessKeyId, |
175 | 207 | # Timestamp || Expires, Signature, | 212 | # Timestamp || Expires, Signature, |
177 | 208 | self.params = {'Version': '2008-12-01', | 213 | self.params = { |
178 | 214 | 'Version': '2008-12-01', | ||
179 | 209 | 'SignatureVersion': '2', | 215 | 'SignatureVersion': '2', |
180 | 210 | 'SignatureMethod': 'HmacSHA1', | 216 | 'SignatureMethod': 'HmacSHA1', |
181 | 211 | 'Action': action, | 217 | 'Action': action, |
183 | 212 | 'AWSAccessKeyId': creds.access_key, | 218 | 'AWSAccessKeyId': self.creds.access_key, |
184 | 213 | 'Timestamp': iso8601time(time_tuple), | 219 | 'Timestamp': iso8601time(time_tuple), |
185 | 214 | } | 220 | } |
186 | 215 | if other_params: | 221 | if other_params: |
187 | 216 | self.params.update(other_params) | 222 | self.params.update(other_params) |
188 | 217 | self.method = 'GET' | ||
189 | 218 | self.host = 'ec2.amazonaws.com' | ||
190 | 219 | self.uri = '/' | ||
191 | 220 | self.creds = creds | ||
192 | 221 | 223 | ||
193 | 222 | def canonical_query_params(self): | 224 | def canonical_query_params(self): |
194 | 223 | """Return the canonical query params (used in signing).""" | 225 | """Return the canonical query params (used in signing).""" |
195 | @@ -230,18 +232,19 @@ | |||
196 | 230 | """Encode a_string as per the canonicalisation encoding rules. | 232 | """Encode a_string as per the canonicalisation encoding rules. |
197 | 231 | 233 | ||
198 | 232 | See the AWS dev reference page 90 (2008-12-01 version). | 234 | See the AWS dev reference page 90 (2008-12-01 version). |
200 | 233 | :return: a_string encoded. | 235 | @return: a_string encoded. |
201 | 234 | """ | 236 | """ |
202 | 235 | return quote(a_string, safe='~') | 237 | return quote(a_string, safe='~') |
203 | 236 | 238 | ||
204 | 237 | def signing_text(self): | 239 | def signing_text(self): |
205 | 238 | """Return the text to be signed when signing the query.""" | 240 | """Return the text to be signed when signing the query.""" |
208 | 239 | result = "%s\n%s\n%s\n%s" % (self.method, self.host, self.uri, | 241 | result = "%s\n%s\n%s\n%s" % (self.endpoint.method, self.endpoint.host, |
209 | 240 | self.canonical_query_params()) | 242 | self.endpoint.path, |
210 | 243 | self.canonical_query_params()) | ||
211 | 241 | return result | 244 | return result |
212 | 242 | 245 | ||
213 | 243 | def sign(self): | 246 | def sign(self): |
215 | 244 | """Sign this query using its built in credentials. | 247 | """Sign this query using its built in creds. |
216 | 245 | 248 | ||
217 | 246 | This prepares it to be sent, and should be done as the last step before | 249 | This prepares it to be sent, and should be done as the last step before |
218 | 247 | submitting the query. Signing is done automatically - this is a public | 250 | submitting the query. Signing is done automatically - this is a public |
219 | @@ -256,9 +259,9 @@ | |||
220 | 256 | def submit(self): | 259 | def submit(self): |
221 | 257 | """Submit this query. | 260 | """Submit this query. |
222 | 258 | 261 | ||
224 | 259 | :return: A deferred from twisted.web.client.getPage | 262 | @return: A deferred from twisted.web.client.getPage |
225 | 260 | """ | 263 | """ |
226 | 261 | self.sign() | 264 | self.sign() |
230 | 262 | url = 'http://%s%s?%s' % (self.host, self.uri, | 265 | url = "%s?%s" % (self.endpoint.get_uri(), |
231 | 263 | self.canonical_query_params()) | 266 | self.canonical_query_params()) |
232 | 264 | return getPage(url, method=self.method) | 267 | return getPage(url, method=self.endpoint.method) |
233 | 265 | 268 | ||
234 | === modified file 'txaws/ec2/tests/test_client.py' | |||
235 | --- txaws/ec2/tests/test_client.py 2009-08-21 03:26:35 +0000 | |||
236 | +++ txaws/ec2/tests/test_client.py 2009-08-25 16:04:05 +0000 | |||
237 | @@ -7,6 +7,7 @@ | |||
238 | 7 | 7 | ||
239 | 8 | from txaws.credentials import AWSCredentials | 8 | from txaws.credentials import AWSCredentials |
240 | 9 | from txaws.ec2 import client | 9 | from txaws.ec2 import client |
241 | 10 | from txaws.service import AWSServiceEndpoint, EC2_ENDPOINT_US | ||
242 | 10 | from txaws.tests import TXAWSTestCase | 11 | from txaws.tests import TXAWSTestCase |
243 | 11 | 12 | ||
244 | 12 | 13 | ||
245 | @@ -117,7 +118,7 @@ | |||
246 | 117 | self.assertEquals(instance.ramdisk_id, "id4") | 118 | self.assertEquals(instance.ramdisk_id, "id4") |
247 | 118 | 119 | ||
248 | 119 | 120 | ||
250 | 120 | class TestEC2Client(TXAWSTestCase): | 121 | class EC2ClientTestCase(TXAWSTestCase): |
251 | 121 | 122 | ||
252 | 122 | def test_init_no_creds(self): | 123 | def test_init_no_creds(self): |
253 | 123 | os.environ['AWS_SECRET_ACCESS_KEY'] = 'foo' | 124 | os.environ['AWS_SECRET_ACCESS_KEY'] = 'foo' |
254 | @@ -129,7 +130,7 @@ | |||
255 | 129 | self.assertRaises(ValueError, client.EC2Client) | 130 | self.assertRaises(ValueError, client.EC2Client) |
256 | 130 | 131 | ||
257 | 131 | def test_init_explicit_creds(self): | 132 | def test_init_explicit_creds(self): |
259 | 132 | creds = 'foo' | 133 | creds = AWSCredentials("foo", "bar") |
260 | 133 | ec2 = client.EC2Client(creds=creds) | 134 | ec2 = client.EC2Client(creds=creds) |
261 | 134 | self.assertEqual(creds, ec2.creds) | 135 | self.assertEqual(creds, ec2.creds) |
262 | 135 | 136 | ||
263 | @@ -162,33 +163,40 @@ | |||
264 | 162 | 163 | ||
265 | 163 | 164 | ||
266 | 164 | def test_parse_reservation(self): | 165 | def test_parse_reservation(self): |
268 | 165 | ec2 = client.EC2Client(creds='foo') | 166 | creds = AWSCredentials("foo", "bar") |
269 | 167 | ec2 = client.EC2Client(creds=creds) | ||
270 | 166 | results = ec2._parse_instances(sample_describe_instances_result) | 168 | results = ec2._parse_instances(sample_describe_instances_result) |
271 | 167 | self.check_parsed_instances(results) | 169 | self.check_parsed_instances(results) |
272 | 168 | 170 | ||
273 | 169 | def test_describe_instances(self): | 171 | def test_describe_instances(self): |
274 | 170 | class StubQuery(object): | 172 | class StubQuery(object): |
276 | 171 | def __init__(stub, action, creds): | 173 | def __init__(stub, action, creds, endpoint): |
277 | 172 | self.assertEqual(action, 'DescribeInstances') | 174 | self.assertEqual(action, 'DescribeInstances') |
279 | 173 | self.assertEqual('foo', creds) | 175 | self.assertEqual(creds.access_key, "foo") |
280 | 176 | self.assertEqual(creds.secret_key, "bar") | ||
281 | 174 | def submit(self): | 177 | def submit(self): |
282 | 175 | return succeed(sample_describe_instances_result) | 178 | return succeed(sample_describe_instances_result) |
284 | 176 | ec2 = client.EC2Client(creds='foo', query_factory=StubQuery) | 179 | creds = AWSCredentials("foo", "bar") |
285 | 180 | ec2 = client.EC2Client(creds, query_factory=StubQuery) | ||
286 | 177 | d = ec2.describe_instances() | 181 | d = ec2.describe_instances() |
287 | 178 | d.addCallback(self.check_parsed_instances) | 182 | d.addCallback(self.check_parsed_instances) |
288 | 179 | return d | 183 | return d |
289 | 180 | 184 | ||
290 | 181 | def test_terminate_instances(self): | 185 | def test_terminate_instances(self): |
291 | 182 | class StubQuery(object): | 186 | class StubQuery(object): |
293 | 183 | def __init__(stub, action, creds, other_params): | 187 | def __init__(stub, action, creds, endpoint, other_params): |
294 | 184 | self.assertEqual(action, 'TerminateInstances') | 188 | self.assertEqual(action, 'TerminateInstances') |
296 | 185 | self.assertEqual('foo', creds) | 189 | self.assertEqual(creds.access_key, "foo") |
297 | 190 | self.assertEqual(creds.secret_key, "bar") | ||
298 | 186 | self.assertEqual( | 191 | self.assertEqual( |
299 | 187 | {'InstanceId.1': 'i-1234', 'InstanceId.2': 'i-5678'}, | 192 | {'InstanceId.1': 'i-1234', 'InstanceId.2': 'i-5678'}, |
300 | 188 | other_params) | 193 | other_params) |
301 | 189 | def submit(self): | 194 | def submit(self): |
302 | 190 | return succeed(sample_terminate_instances_result) | 195 | return succeed(sample_terminate_instances_result) |
304 | 191 | ec2 = client.EC2Client(creds='foo', query_factory=StubQuery) | 196 | creds = AWSCredentials("foo", "bar") |
305 | 197 | endpoint = AWSServiceEndpoint(uri=EC2_ENDPOINT_US) | ||
306 | 198 | ec2 = client.EC2Client(creds=creds, endpoint=endpoint, | ||
307 | 199 | query_factory=StubQuery) | ||
308 | 192 | d = ec2.terminate_instances('i-1234', 'i-5678') | 200 | d = ec2.terminate_instances('i-1234', 'i-5678') |
309 | 193 | def check_transition(changes): | 201 | def check_transition(changes): |
310 | 194 | self.assertEqual([('i-1234', 'running', 'shutting-down'), | 202 | self.assertEqual([('i-1234', 'running', 'shutting-down'), |
311 | @@ -196,14 +204,15 @@ | |||
312 | 196 | return d | 204 | return d |
313 | 197 | 205 | ||
314 | 198 | 206 | ||
316 | 199 | class TestQuery(TXAWSTestCase): | 207 | class QueryTestCase(TXAWSTestCase): |
317 | 200 | 208 | ||
318 | 201 | def setUp(self): | 209 | def setUp(self): |
319 | 202 | TXAWSTestCase.setUp(self) | 210 | TXAWSTestCase.setUp(self) |
320 | 203 | self.creds = AWSCredentials('foo', 'bar') | 211 | self.creds = AWSCredentials('foo', 'bar') |
321 | 212 | self.endpoint = AWSServiceEndpoint(uri=EC2_ENDPOINT_US) | ||
322 | 204 | 213 | ||
323 | 205 | def test_init_minimum(self): | 214 | def test_init_minimum(self): |
325 | 206 | query = client.Query('DescribeInstances', self.creds) | 215 | query = client.Query('DescribeInstances', self.creds, self.endpoint) |
326 | 207 | self.assertTrue('Timestamp' in query.params) | 216 | self.assertTrue('Timestamp' in query.params) |
327 | 208 | del query.params['Timestamp'] | 217 | del query.params['Timestamp'] |
328 | 209 | self.assertEqual( | 218 | self.assertEqual( |
329 | @@ -221,7 +230,7 @@ | |||
330 | 221 | self.assertRaises(TypeError, client.Query, None) | 230 | self.assertRaises(TypeError, client.Query, None) |
331 | 222 | 231 | ||
332 | 223 | def test_init_other_args_are_params(self): | 232 | def test_init_other_args_are_params(self): |
334 | 224 | query = client.Query('DescribeInstances', self.creds, | 233 | query = client.Query('DescribeInstances', self.creds, self.endpoint, |
335 | 225 | {'InstanceId.0': '12345'}, | 234 | {'InstanceId.0': '12345'}, |
336 | 226 | time_tuple=(2007,11,12,13,14,15,0,0,0)) | 235 | time_tuple=(2007,11,12,13,14,15,0,0,0)) |
337 | 227 | self.assertEqual( | 236 | self.assertEqual( |
338 | @@ -235,7 +244,7 @@ | |||
339 | 235 | query.params) | 244 | query.params) |
340 | 236 | 245 | ||
341 | 237 | def test_sorted_params(self): | 246 | def test_sorted_params(self): |
343 | 238 | query = client.Query('DescribeInstances', self.creds, | 247 | query = client.Query('DescribeInstances', self.creds, self.endpoint, |
344 | 239 | {'fun': 'games'}, | 248 | {'fun': 'games'}, |
345 | 240 | time_tuple=(2007,11,12,13,14,15,0,0,0)) | 249 | time_tuple=(2007,11,12,13,14,15,0,0,0)) |
346 | 241 | self.assertEqual([ | 250 | self.assertEqual([ |
347 | @@ -251,16 +260,16 @@ | |||
348 | 251 | def test_encode_unreserved(self): | 260 | def test_encode_unreserved(self): |
349 | 252 | all_unreserved = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' | 261 | all_unreserved = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' |
350 | 253 | 'abcdefghijklmnopqrstuvwxyz0123456789-_.~') | 262 | 'abcdefghijklmnopqrstuvwxyz0123456789-_.~') |
352 | 254 | query = client.Query('DescribeInstances', self.creds) | 263 | query = client.Query('DescribeInstances', self.creds, self.endpoint) |
353 | 255 | self.assertEqual(all_unreserved, query.encode(all_unreserved)) | 264 | self.assertEqual(all_unreserved, query.encode(all_unreserved)) |
354 | 256 | 265 | ||
355 | 257 | def test_encode_space(self): | 266 | def test_encode_space(self): |
356 | 258 | """This may be just 'url encode', but the AWS manual isn't clear.""" | 267 | """This may be just 'url encode', but the AWS manual isn't clear.""" |
358 | 259 | query = client.Query('DescribeInstances', self.creds) | 268 | query = client.Query('DescribeInstances', self.creds, self.endpoint) |
359 | 260 | self.assertEqual('a%20space', query.encode('a space')) | 269 | self.assertEqual('a%20space', query.encode('a space')) |
360 | 261 | 270 | ||
361 | 262 | def test_canonical_query(self): | 271 | def test_canonical_query(self): |
363 | 263 | query = client.Query('DescribeInstances', self.creds, | 272 | query = client.Query('DescribeInstances', self.creds, self.endpoint, |
364 | 264 | {'fu n': 'g/ames', 'argwithnovalue':'', | 273 | {'fu n': 'g/ames', 'argwithnovalue':'', |
365 | 265 | 'InstanceId.1': 'i-1234'}, | 274 | 'InstanceId.1': 'i-1234'}, |
366 | 266 | time_tuple=(2007,11,12,13,14,15,0,0,0)) | 275 | time_tuple=(2007,11,12,13,14,15,0,0,0)) |
367 | @@ -272,17 +281,17 @@ | |||
368 | 272 | self.assertEqual(expected_query, query.canonical_query_params()) | 281 | self.assertEqual(expected_query, query.canonical_query_params()) |
369 | 273 | 282 | ||
370 | 274 | def test_signing_text(self): | 283 | def test_signing_text(self): |
372 | 275 | query = client.Query('DescribeInstances', self.creds, | 284 | query = client.Query('DescribeInstances', self.creds, self.endpoint, |
373 | 276 | time_tuple=(2007,11,12,13,14,15,0,0,0)) | 285 | time_tuple=(2007,11,12,13,14,15,0,0,0)) |
375 | 277 | signing_text = ('GET\nec2.amazonaws.com\n/\n' | 286 | signing_text = ('GET\n%s\n/\n' % self.endpoint.host + |
376 | 278 | 'AWSAccessKeyId=foo&Action=DescribeInstances&' | 287 | 'AWSAccessKeyId=foo&Action=DescribeInstances&' |
377 | 279 | 'SignatureMethod=HmacSHA1&SignatureVersion=2&' | 288 | 'SignatureMethod=HmacSHA1&SignatureVersion=2&' |
378 | 280 | 'Timestamp=2007-11-12T13%3A14%3A15Z&Version=2008-12-01') | 289 | 'Timestamp=2007-11-12T13%3A14%3A15Z&Version=2008-12-01') |
379 | 281 | self.assertEqual(signing_text, query.signing_text()) | 290 | self.assertEqual(signing_text, query.signing_text()) |
380 | 282 | 291 | ||
381 | 283 | def test_sign(self): | 292 | def test_sign(self): |
383 | 284 | query = client.Query('DescribeInstances', self.creds, | 293 | query = client.Query('DescribeInstances', self.creds, self.endpoint, |
384 | 285 | time_tuple=(2007,11,12,13,14,15,0,0,0)) | 294 | time_tuple=(2007,11,12,13,14,15,0,0,0)) |
385 | 286 | query.sign() | 295 | query.sign() |
387 | 287 | self.assertEqual('4hEtLuZo9i6kuG3TOXvRQNOrE/U=', | 296 | self.assertEqual('JuCpwFA2H4OVF3Ql/lAQs+V6iMc=', |
388 | 288 | query.params['Signature']) | 297 | query.params['Signature']) |
389 | 289 | 298 | ||
390 | === added file 'txaws/service.py' | |||
391 | --- txaws/service.py 1970-01-01 00:00:00 +0000 | |||
392 | +++ txaws/service.py 2009-08-25 16:04:05 +0000 | |||
393 | @@ -0,0 +1,100 @@ | |||
394 | 1 | # Copyright (C) 2009 Duncan McGreggor <duncan@canonical.com> | ||
395 | 2 | # Copyright (C) 2009 Robert Collins <robertc@robertcollins.net> | ||
396 | 3 | # Licenced under the txaws licence available at /LICENSE in the txaws source. | ||
397 | 4 | |||
398 | 5 | import os | ||
399 | 6 | |||
400 | 7 | from twisted.web.client import _parse | ||
401 | 8 | |||
402 | 9 | |||
403 | 10 | |||
404 | 11 | __all__ = ["AWSServiceEndpoint", "AWSServiceRegion", "REGION_US", "REGION_EU"] | ||
405 | 12 | |||
406 | 13 | |||
407 | 14 | REGION_US = "US" | ||
408 | 15 | REGION_EU = "EU" | ||
409 | 16 | EC2_ENDPOINT_US = "https://us-east-1.ec2.amazonaws.com/" | ||
410 | 17 | EC2_ENDPOINT_EU = "https://eu-west-1.ec2.amazonaws.com/" | ||
411 | 18 | DEFAULT_PORT = 80 | ||
412 | 19 | |||
413 | 20 | |||
414 | 21 | class AWSServiceEndpoint(object): | ||
415 | 22 | """ | ||
416 | 23 | @param uri: The URL for the service. | ||
417 | 24 | @param method: The HTTP method used when accessing a service. | ||
418 | 25 | """ | ||
419 | 26 | |||
420 | 27 | def __init__(self, uri="", method="GET"): | ||
421 | 28 | self.host = "" | ||
422 | 29 | self.port = DEFAULT_PORT | ||
423 | 30 | self.path = "/" | ||
424 | 31 | self.method = method | ||
425 | 32 | self._parse_uri(uri) | ||
426 | 33 | if not self.scheme: | ||
427 | 34 | self.scheme = "http" | ||
428 | 35 | |||
429 | 36 | def _parse_uri(self, uri): | ||
430 | 37 | scheme, host, port, path = _parse( | ||
431 | 38 | str(uri), defaultPort=DEFAULT_PORT) | ||
432 | 39 | self.scheme = scheme | ||
433 | 40 | self.host = host | ||
434 | 41 | self.port = port | ||
435 | 42 | self.path = path | ||
436 | 43 | |||
437 | 44 | def set_path(self, path): | ||
438 | 45 | self.path = path | ||
439 | 46 | |||
440 | 47 | def get_uri(self): | ||
441 | 48 | """Get a URL representation of the service.""" | ||
442 | 49 | uri = "%s://%s" % (self.scheme, self.host) | ||
443 | 50 | if self.port and self.port != DEFAULT_PORT: | ||
444 | 51 | uri = "%s:%s" % (uri, self.port) | ||
445 | 52 | return uri + self.path | ||
446 | 53 | |||
447 | 54 | |||
448 | 55 | class AWSServiceRegion(object): | ||
449 | 56 | """ | ||
450 | 57 | This object represents a collection of client factories that use the same | ||
451 | 58 | credentials. With Amazon, this collection is associated with a region | ||
452 | 59 | (e.g., US or EU). | ||
453 | 60 | """ | ||
454 | 61 | def __init__(self, creds=None, region=REGION_US): | ||
455 | 62 | self.creds = creds | ||
456 | 63 | self._clients = {} | ||
457 | 64 | if region == REGION_US: | ||
458 | 65 | ec2_endpoint = EC2_ENDPOINT_US | ||
459 | 66 | elif region == REGION_EU: | ||
460 | 67 | ec2_endpoint = EC2_ENDPOINT_EU | ||
461 | 68 | self.ec2_endpoint = AWSServiceEndpoint(uri=ec2_endpoint) | ||
462 | 69 | |||
463 | 70 | def get_client(self, cls, purge_cache=False, *args, **kwds): | ||
464 | 71 | """ | ||
465 | 72 | This is a general method for getting a client: if present, it is pulled | ||
466 | 73 | from the cache; if not, a new one is instantiated and then put into the | ||
467 | 74 | cache. This method should not be called directly, but rather by other | ||
468 | 75 | client-specific methods (e.g., get_ec2_client). | ||
469 | 76 | """ | ||
470 | 77 | key = str(cls) + str(args) + str(kwds) | ||
471 | 78 | instance = self._clients.get(key) | ||
472 | 79 | if purge_cache or not instance: | ||
473 | 80 | instance = cls(*args, **kwds) | ||
474 | 81 | self._clients[key] = instance | ||
475 | 82 | return instance | ||
476 | 83 | |||
477 | 84 | def get_ec2_client(self, creds=None): | ||
478 | 85 | from txaws.ec2.client import EC2Client | ||
479 | 86 | |||
480 | 87 | if creds: | ||
481 | 88 | self.creds = creds | ||
482 | 89 | return self.get_client(EC2Client, creds=creds, | ||
483 | 90 | endpoint=self.ec2_endpoint, query_factory=None) | ||
484 | 91 | |||
485 | 92 | def get_s3_client(self): | ||
486 | 93 | raise NotImplementedError | ||
487 | 94 | |||
488 | 95 | def get_simpledb_client(self): | ||
489 | 96 | raise NotImplementedError | ||
490 | 97 | |||
491 | 98 | def get_sqs_client(self): | ||
492 | 99 | raise NotImplementedError | ||
493 | 100 | |||
494 | 0 | 101 | ||
495 | === modified file 'txaws/storage/client.py' | |||
496 | --- txaws/storage/client.py 2009-08-20 12:15:12 +0000 | |||
497 | +++ txaws/storage/client.py 2009-08-21 14:50:25 +0000 | |||
498 | @@ -25,19 +25,18 @@ | |||
499 | 25 | class S3Request(object): | 25 | class S3Request(object): |
500 | 26 | 26 | ||
501 | 27 | def __init__(self, verb, bucket=None, object_name=None, data='', | 27 | def __init__(self, verb, bucket=None, object_name=None, data='', |
504 | 28 | content_type=None, | 28 | content_type=None, metadata={}, creds=None, endpoint=None): |
503 | 29 | metadata={}, root_uri='https://s3.amazonaws.com', creds=None): | ||
505 | 30 | self.verb = verb | 29 | self.verb = verb |
506 | 31 | self.bucket = bucket | 30 | self.bucket = bucket |
507 | 32 | self.object_name = object_name | 31 | self.object_name = object_name |
508 | 33 | self.data = data | 32 | self.data = data |
509 | 34 | self.content_type = content_type | 33 | self.content_type = content_type |
510 | 35 | self.metadata = metadata | 34 | self.metadata = metadata |
511 | 36 | self.root_uri = root_uri | ||
512 | 37 | self.creds = creds | 35 | self.creds = creds |
513 | 36 | self.endpoint = endpoint or self.get_uri() | ||
514 | 38 | self.date = datetimeToString() | 37 | self.date = datetimeToString() |
515 | 39 | 38 | ||
517 | 40 | def get_uri_path(self): | 39 | def get_path(self): |
518 | 41 | path = '/' | 40 | path = '/' |
519 | 42 | if self.bucket is not None: | 41 | if self.bucket is not None: |
520 | 43 | path += self.bucket | 42 | path += self.bucket |
521 | @@ -46,7 +45,8 @@ | |||
522 | 46 | return path | 45 | return path |
523 | 47 | 46 | ||
524 | 48 | def get_uri(self): | 47 | def get_uri(self): |
526 | 49 | return self.root_uri + self.get_uri_path() | 48 | self.endpoint.set_path(self.get_path()) |
527 | 49 | return self.endpoint.get_uri() | ||
528 | 50 | 50 | ||
529 | 51 | def get_headers(self): | 51 | def get_headers(self): |
530 | 52 | headers = {'Content-Length': len(self.data), | 52 | headers = {'Content-Length': len(self.data), |
531 | @@ -66,7 +66,7 @@ | |||
532 | 66 | return headers | 66 | return headers |
533 | 67 | 67 | ||
534 | 68 | def get_canonicalized_resource(self): | 68 | def get_canonicalized_resource(self): |
536 | 69 | return self.get_uri_path() | 69 | return self.get_path() |
537 | 70 | 70 | ||
538 | 71 | def get_canonicalized_amz_headers(self, headers): | 71 | def get_canonicalized_amz_headers(self, headers): |
539 | 72 | result = '' | 72 | result = '' |
540 | @@ -76,12 +76,12 @@ | |||
541 | 76 | return ''.join('%s:%s\n' % (name, value) for name, value in headers) | 76 | return ''.join('%s:%s\n' % (name, value) for name, value in headers) |
542 | 77 | 77 | ||
543 | 78 | def get_signature(self, headers): | 78 | def get_signature(self, headers): |
550 | 79 | text = self.verb + '\n' | 79 | text = (self.verb + '\n' + |
551 | 80 | text += headers.get('Content-MD5', '') + '\n' | 80 | headers.get('Content-MD5', '') + '\n' + |
552 | 81 | text += headers.get('Content-Type', '') + '\n' | 81 | headers.get('Content-Type', '') + '\n' + |
553 | 82 | text += headers.get('Date', '') + '\n' | 82 | headers.get('Date', '') + '\n' + |
554 | 83 | text += self.get_canonicalized_amz_headers(headers) | 83 | self.get_canonicalized_amz_headers(headers) + |
555 | 84 | text += self.get_canonicalized_resource() | 84 | self.get_canonicalized_resource()) |
556 | 85 | return self.creds.sign(text) | 85 | return self.creds.sign(text) |
557 | 86 | 86 | ||
558 | 87 | def submit(self): | 87 | def submit(self): |
559 | @@ -94,20 +94,21 @@ | |||
560 | 94 | 94 | ||
561 | 95 | class S3(object): | 95 | class S3(object): |
562 | 96 | 96 | ||
563 | 97 | root_uri = 'https://s3.amazonaws.com/' | ||
564 | 98 | request_factory = S3Request | 97 | request_factory = S3Request |
565 | 99 | 98 | ||
567 | 100 | def __init__(self, creds): | 99 | def __init__(self, creds, endpoint): |
568 | 101 | self.creds = creds | 100 | self.creds = creds |
569 | 101 | self.endpoint = endpoint | ||
570 | 102 | 102 | ||
571 | 103 | def make_request(self, *a, **kw): | 103 | def make_request(self, *a, **kw): |
572 | 104 | """ | 104 | """ |
573 | 105 | Create a request with the arguments passed in. | 105 | Create a request with the arguments passed in. |
574 | 106 | 106 | ||
577 | 107 | This uses the request_factory attribute, adding the credentials to the | 107 | This uses the request_factory attribute, adding the creds and endpoint |
578 | 108 | arguments passed in. | 108 | to the arguments passed in. |
579 | 109 | """ | 109 | """ |
581 | 110 | return self.request_factory(creds=self.creds, *a, **kw) | 110 | return self.request_factory(creds=self.creds, endpoint=self.endpoint, |
582 | 111 | *a, **kw) | ||
583 | 111 | 112 | ||
584 | 112 | def _parse_bucket_list(self, response): | 113 | def _parse_bucket_list(self, response): |
585 | 113 | """ | 114 | """ |
586 | 114 | 115 | ||
587 | === modified file 'txaws/storage/tests/test_client.py' | |||
588 | --- txaws/storage/tests/test_client.py 2009-08-20 12:15:12 +0000 | |||
589 | +++ txaws/storage/tests/test_client.py 2009-08-21 14:50:25 +0000 | |||
590 | @@ -5,6 +5,7 @@ | |||
591 | 5 | from twisted.internet.defer import succeed | 5 | from twisted.internet.defer import succeed |
592 | 6 | 6 | ||
593 | 7 | from txaws.credentials import AWSCredentials | 7 | from txaws.credentials import AWSCredentials |
594 | 8 | from txaws.service import AWSServiceEndpoint | ||
595 | 8 | from txaws.storage.client import S3, S3Request | 9 | from txaws.storage.client import S3, S3Request |
596 | 9 | from txaws.tests import TXAWSTestCase | 10 | from txaws.tests import TXAWSTestCase |
597 | 10 | from txaws.util import calculate_md5 | 11 | from txaws.util import calculate_md5 |
598 | @@ -18,10 +19,10 @@ | |||
599 | 18 | return succeed('') | 19 | return succeed('') |
600 | 19 | 20 | ||
601 | 20 | 21 | ||
603 | 21 | class RequestTests(TXAWSTestCase): | 22 | class RequestTestCase(TXAWSTestCase): |
604 | 22 | 23 | ||
607 | 23 | creds = AWSCredentials(access_key='0PN5J17HBGZHT7JJ3X82', | 24 | creds = AWSCredentials(access_key='fookeyid', secret_key='barsecretkey') |
608 | 24 | secret_key='uV3F3YluFJax1cknvbcGwgjvx4QpvB+leU8dUj2o') | 25 | endpoint = AWSServiceEndpoint("https://s3.amazonaws.com/") |
609 | 25 | 26 | ||
610 | 26 | def test_objectRequest(self): | 27 | def test_objectRequest(self): |
611 | 27 | """ | 28 | """ |
612 | @@ -31,18 +32,22 @@ | |||
613 | 31 | DIGEST = 'zhdB6gwvocWv/ourYUWMxA==' | 32 | DIGEST = 'zhdB6gwvocWv/ourYUWMxA==' |
614 | 32 | 33 | ||
615 | 33 | request = S3Request('PUT', 'somebucket', 'object/name/here', DATA, | 34 | request = S3Request('PUT', 'somebucket', 'object/name/here', DATA, |
617 | 34 | content_type='text/plain', metadata={'foo': 'bar'}) | 35 | content_type='text/plain', metadata={'foo': 'bar'}, |
618 | 36 | creds=self.creds, endpoint=self.endpoint) | ||
619 | 37 | request.get_signature = lambda headers: "TESTINGSIG=" | ||
620 | 35 | self.assertEqual(request.verb, 'PUT') | 38 | self.assertEqual(request.verb, 'PUT') |
621 | 36 | self.assertEqual( | 39 | self.assertEqual( |
622 | 37 | request.get_uri(), | 40 | request.get_uri(), |
623 | 38 | 'https://s3.amazonaws.com/somebucket/object/name/here') | 41 | 'https://s3.amazonaws.com/somebucket/object/name/here') |
624 | 39 | headers = request.get_headers() | 42 | headers = request.get_headers() |
625 | 40 | self.assertNotEqual(headers.pop('Date'), '') | 43 | self.assertNotEqual(headers.pop('Date'), '') |
631 | 41 | self.assertEqual(headers, | 44 | self.assertEqual( |
632 | 42 | {'Content-Type': 'text/plain', | 45 | headers, { |
633 | 43 | 'Content-Length': len(DATA), | 46 | 'Authorization': 'AWS fookeyid:TESTINGSIG=', |
634 | 44 | 'Content-MD5': DIGEST, | 47 | 'Content-Type': 'text/plain', |
635 | 45 | 'x-amz-meta-foo': 'bar'}) | 48 | 'Content-Length': len(DATA), |
636 | 49 | 'Content-MD5': DIGEST, | ||
637 | 50 | 'x-amz-meta-foo': 'bar'}) | ||
638 | 46 | self.assertEqual(request.data, 'objectData') | 51 | self.assertEqual(request.data, 'objectData') |
639 | 47 | 52 | ||
640 | 48 | def test_bucketRequest(self): | 53 | def test_bucketRequest(self): |
641 | @@ -51,22 +56,27 @@ | |||
642 | 51 | """ | 56 | """ |
643 | 52 | DIGEST = '1B2M2Y8AsgTpgAmY7PhCfg==' | 57 | DIGEST = '1B2M2Y8AsgTpgAmY7PhCfg==' |
644 | 53 | 58 | ||
646 | 54 | request = S3Request('GET', 'somebucket') | 59 | request = S3Request('GET', 'somebucket', creds=self.creds, |
647 | 60 | endpoint=self.endpoint) | ||
648 | 61 | request.get_signature = lambda headers: "TESTINGSIG=" | ||
649 | 55 | self.assertEqual(request.verb, 'GET') | 62 | self.assertEqual(request.verb, 'GET') |
650 | 56 | self.assertEqual( | 63 | self.assertEqual( |
651 | 57 | request.get_uri(), 'https://s3.amazonaws.com/somebucket') | 64 | request.get_uri(), 'https://s3.amazonaws.com/somebucket') |
652 | 58 | headers = request.get_headers() | 65 | headers = request.get_headers() |
653 | 59 | self.assertNotEqual(headers.pop('Date'), '') | 66 | self.assertNotEqual(headers.pop('Date'), '') |
657 | 60 | self.assertEqual(headers, | 67 | self.assertEqual( |
658 | 61 | {'Content-Length': 0, | 68 | headers, { |
659 | 62 | 'Content-MD5': DIGEST}) | 69 | 'Authorization': 'AWS fookeyid:TESTINGSIG=', |
660 | 70 | 'Content-Length': 0, | ||
661 | 71 | 'Content-MD5': DIGEST}) | ||
662 | 63 | self.assertEqual(request.data, '') | 72 | self.assertEqual(request.data, '') |
663 | 64 | 73 | ||
664 | 65 | def test_submit(self): | 74 | def test_submit(self): |
665 | 66 | """ | 75 | """ |
666 | 67 | Submitting the request should invoke getPage correctly. | 76 | Submitting the request should invoke getPage correctly. |
667 | 68 | """ | 77 | """ |
669 | 69 | request = StubbedS3Request('GET', 'somebucket') | 78 | request = StubbedS3Request('GET', 'somebucket', creds=self.creds, |
670 | 79 | endpoint=self.endpoint) | ||
671 | 70 | 80 | ||
672 | 71 | def _postCheck(result): | 81 | def _postCheck(result): |
673 | 72 | self.assertEqual(result, '') | 82 | self.assertEqual(result, '') |
674 | @@ -80,13 +90,14 @@ | |||
675 | 80 | return request.submit().addCallback(_postCheck) | 90 | return request.submit().addCallback(_postCheck) |
676 | 81 | 91 | ||
677 | 82 | def test_authenticationTestCases(self): | 92 | def test_authenticationTestCases(self): |
680 | 83 | req = S3Request('GET', creds=self.creds) | 93 | request = S3Request('GET', creds=self.creds, endpoint=self.endpoint) |
681 | 84 | req.date = 'Wed, 28 Mar 2007 01:29:59 +0000' | 94 | request.get_signature = lambda headers: "TESTINGSIG=" |
682 | 95 | request.date = 'Wed, 28 Mar 2007 01:29:59 +0000' | ||
683 | 85 | 96 | ||
685 | 86 | headers = req.get_headers() | 97 | headers = request.get_headers() |
686 | 87 | self.assertEqual( | 98 | self.assertEqual( |
687 | 88 | headers['Authorization'], | 99 | headers['Authorization'], |
689 | 89 | 'AWS 0PN5J17HBGZHT7JJ3X82:jF7L3z/FTV47vagZzhKupJ9oNig=') | 100 | 'AWS fookeyid:TESTINGSIG=') |
690 | 90 | 101 | ||
691 | 91 | 102 | ||
692 | 92 | class InertRequest(S3Request): | 103 | class InertRequest(S3Request): |
693 | @@ -153,16 +164,18 @@ | |||
694 | 153 | TXAWSTestCase.setUp(self) | 164 | TXAWSTestCase.setUp(self) |
695 | 154 | self.creds = AWSCredentials( | 165 | self.creds = AWSCredentials( |
696 | 155 | access_key='accessKey', secret_key='secretKey') | 166 | access_key='accessKey', secret_key='secretKey') |
698 | 156 | self.s3 = TestableS3(creds=self.creds) | 167 | self.endpoint = AWSServiceEndpoint() |
699 | 168 | self.s3 = TestableS3(creds=self.creds, endpoint=self.endpoint) | ||
700 | 157 | 169 | ||
701 | 158 | def test_make_request(self): | 170 | def test_make_request(self): |
702 | 159 | """ | 171 | """ |
704 | 160 | Test that make_request passes in the service credentials. | 172 | Test that make_request passes in the credentials object. |
705 | 161 | """ | 173 | """ |
706 | 162 | marker = object() | 174 | marker = object() |
707 | 163 | 175 | ||
708 | 164 | def _cb(*a, **kw): | 176 | def _cb(*a, **kw): |
709 | 165 | self.assertEqual(kw['creds'], self.creds) | 177 | self.assertEqual(kw['creds'], self.creds) |
710 | 178 | self.assertEqual(kw['endpoint'], self.endpoint) | ||
711 | 166 | return marker | 179 | return marker |
712 | 167 | 180 | ||
713 | 168 | self.s3.request_factory = _cb | 181 | self.s3.request_factory = _cb |
714 | 169 | 182 | ||
715 | === modified file 'txaws/tests/test_credentials.py' | |||
716 | --- txaws/tests/test_credentials.py 2009-08-20 12:15:12 +0000 | |||
717 | +++ txaws/tests/test_credentials.py 2009-08-25 14:36:20 +0000 | |||
718 | @@ -5,37 +5,49 @@ | |||
719 | 5 | 5 | ||
720 | 6 | from twisted.trial.unittest import TestCase | 6 | from twisted.trial.unittest import TestCase |
721 | 7 | 7 | ||
723 | 8 | from txaws.credentials import AWSCredentials | 8 | from txaws.credentials import AWSCredentials, ENV_ACCESS_KEY, ENV_SECRET_KEY |
724 | 9 | from txaws.tests import TXAWSTestCase | ||
725 | 10 | |||
726 | 9 | from txaws.tests import TXAWSTestCase | 11 | from txaws.tests import TXAWSTestCase |
727 | 10 | 12 | ||
728 | 11 | 13 | ||
729 | 12 | class TestCredentials(TXAWSTestCase): | 14 | class TestCredentials(TXAWSTestCase): |
730 | 13 | 15 | ||
731 | 16 | def setUp(self): | ||
732 | 17 | self.addCleanup(self.clean_environment) | ||
733 | 18 | |||
734 | 19 | def clean_environment(self): | ||
735 | 20 | if os.environ.has_key(ENV_ACCESS_KEY): | ||
736 | 21 | del os.environ[ENV_ACCESS_KEY] | ||
737 | 22 | if os.environ.has_key(ENV_SECRET_KEY): | ||
738 | 23 | del os.environ[ENV_SECRET_KEY] | ||
739 | 24 | |||
740 | 14 | def test_no_access_errors(self): | 25 | def test_no_access_errors(self): |
744 | 15 | # Without anything in os.environ, AWSCredentials() blows up | 26 | # Without anything in os.environ, AWSService() blows up |
745 | 16 | os.environ['AWS_SECRET_ACCESS_KEY'] = 'foo' | 27 | os.environ[ENV_SECRET_KEY] = "bar" |
746 | 17 | self.assertRaises(Exception, AWSCredentials) | 28 | self.assertRaises(ValueError, AWSCredentials) |
747 | 18 | 29 | ||
748 | 19 | def test_no_secret_errors(self): | 30 | def test_no_secret_errors(self): |
752 | 20 | # Without anything in os.environ, AWSCredentials() blows up | 31 | # Without anything in os.environ, AWSService() blows up |
753 | 21 | os.environ['AWS_ACCESS_KEY_ID'] = 'bar' | 32 | os.environ[ENV_ACCESS_KEY] = "foo" |
754 | 22 | self.assertRaises(Exception, AWSCredentials) | 33 | self.assertRaises(ValueError, AWSCredentials) |
755 | 23 | 34 | ||
756 | 24 | def test_found_values_used(self): | 35 | def test_found_values_used(self): |
762 | 25 | os.environ['AWS_SECRET_ACCESS_KEY'] = 'foo' | 36 | os.environ[ENV_ACCESS_KEY] = "foo" |
763 | 26 | os.environ['AWS_ACCESS_KEY_ID'] = 'bar' | 37 | os.environ[ENV_SECRET_KEY] = "bar" |
764 | 27 | creds = AWSCredentials() | 38 | service = AWSCredentials() |
765 | 28 | self.assertEqual('foo', creds.secret_key) | 39 | self.assertEqual("foo", service.access_key) |
766 | 29 | self.assertEqual('bar', creds.access_key) | 40 | self.assertEqual("bar", service.secret_key) |
767 | 41 | self.clean_environment() | ||
768 | 30 | 42 | ||
769 | 31 | def test_explicit_access_key(self): | 43 | def test_explicit_access_key(self): |
774 | 32 | os.environ['AWS_SECRET_ACCESS_KEY'] = 'foo' | 44 | os.environ[ENV_SECRET_KEY] = "foo" |
775 | 33 | creds = AWSCredentials(access_key='bar') | 45 | service = AWSCredentials(access_key="bar") |
776 | 34 | self.assertEqual('foo', creds.secret_key) | 46 | self.assertEqual("foo", service.secret_key) |
777 | 35 | self.assertEqual('bar', creds.access_key) | 47 | self.assertEqual("bar", service.access_key) |
778 | 36 | 48 | ||
779 | 37 | def test_explicit_secret_key(self): | 49 | def test_explicit_secret_key(self): |
784 | 38 | os.environ['AWS_ACCESS_KEY_ID'] = 'bar' | 50 | os.environ[ENV_ACCESS_KEY] = "bar" |
785 | 39 | creds = AWSCredentials(secret_key='foo') | 51 | service = AWSCredentials(secret_key="foo") |
786 | 40 | self.assertEqual('foo', creds.secret_key) | 52 | self.assertEqual("foo", service.secret_key) |
787 | 41 | self.assertEqual('bar', creds.access_key) | 53 | self.assertEqual("bar", service.access_key) |
788 | 42 | 54 | ||
789 | === added file 'txaws/tests/test_service.py' | |||
790 | --- txaws/tests/test_service.py 1970-01-01 00:00:00 +0000 | |||
791 | +++ txaws/tests/test_service.py 2009-08-25 16:16:22 +0000 | |||
792 | @@ -0,0 +1,116 @@ | |||
793 | 1 | # Copyright (C) 2009 Duncan McGreggor <duncan@canonical.com> | ||
794 | 2 | # Licenced under the txaws licence available at /LICENSE in the txaws source. | ||
795 | 3 | |||
796 | 4 | import os | ||
797 | 5 | |||
798 | 6 | from txaws.credentials import AWSCredentials | ||
799 | 7 | from txaws.ec2.client import EC2Client | ||
800 | 8 | from txaws.service import AWSServiceEndpoint, AWSServiceRegion, EC2_ENDPOINT_US | ||
801 | 9 | from txaws.tests import TXAWSTestCase | ||
802 | 10 | |||
803 | 11 | class AWSServiceEndpointTestCase(TXAWSTestCase): | ||
804 | 12 | |||
805 | 13 | def setUp(self): | ||
806 | 14 | self.endpoint = AWSServiceEndpoint(uri="http://my.service/da_endpoint") | ||
807 | 15 | |||
808 | 16 | def test_simple_creation(self): | ||
809 | 17 | endpoint = AWSServiceEndpoint() | ||
810 | 18 | self.assertEquals(endpoint.scheme, "http") | ||
811 | 19 | self.assertEquals(endpoint.host, "") | ||
812 | 20 | self.assertEquals(endpoint.port, 80) | ||
813 | 21 | self.assertEquals(endpoint.path, "/") | ||
814 | 22 | self.assertEquals(endpoint.method, "GET") | ||
815 | 23 | |||
816 | 24 | def test_parse_uri(self): | ||
817 | 25 | self.assertEquals(self.endpoint.scheme, "http") | ||
818 | 26 | self.assertEquals(self.endpoint.host, "my.service") | ||
819 | 27 | self.assertEquals(self.endpoint.port, 80) | ||
820 | 28 | self.assertEquals(self.endpoint.path, "/da_endpoint") | ||
821 | 29 | |||
822 | 30 | def test_parse_uri_https_and_custom_port(self): | ||
823 | 31 | endpoint = AWSServiceEndpoint(uri="https://my.service:8080/endpoint") | ||
824 | 32 | self.assertEquals(endpoint.scheme, "https") | ||
825 | 33 | self.assertEquals(endpoint.host, "my.service") | ||
826 | 34 | self.assertEquals(endpoint.port, 8080) | ||
827 | 35 | self.assertEquals(endpoint.path, "/endpoint") | ||
828 | 36 | |||
829 | 37 | def test_custom_method(self): | ||
830 | 38 | endpoint = AWSServiceEndpoint(uri="http://service/endpoint", | ||
831 | 39 | method="PUT") | ||
832 | 40 | self.assertEquals(endpoint.method, "PUT") | ||
833 | 41 | |||
834 | 42 | def test_get_uri(self): | ||
835 | 43 | uri = self.endpoint.get_uri() | ||
836 | 44 | self.assertEquals(uri, "http://my.service/da_endpoint") | ||
837 | 45 | |||
838 | 46 | def test_get_uri_custom_port(self): | ||
839 | 47 | uri = "https://my.service:8080/endpoint" | ||
840 | 48 | endpoint = AWSServiceEndpoint(uri=uri) | ||
841 | 49 | new_uri = endpoint.get_uri() | ||
842 | 50 | self.assertEquals(new_uri, uri) | ||
843 | 51 | |||
844 | 52 | def test_set_path(self): | ||
845 | 53 | original_path = self.endpoint.path | ||
846 | 54 | self.endpoint.set_path("/newpath") | ||
847 | 55 | self.assertEquals( | ||
848 | 56 | self.endpoint.get_uri(), | ||
849 | 57 | "http://my.service/newpath") | ||
850 | 58 | |||
851 | 59 | |||
852 | 60 | class AWSServiceRegionTestCase(TXAWSTestCase): | ||
853 | 61 | |||
854 | 62 | def setUp(self): | ||
855 | 63 | self.creds = AWSCredentials("foo", "bar") | ||
856 | 64 | self.region = AWSServiceRegion(creds=self.creds) | ||
857 | 65 | |||
858 | 66 | def test_simple_creation(self): | ||
859 | 67 | self.assertEquals(self.creds, self.region.creds) | ||
860 | 68 | self.assertEquals(self.region._clients, {}) | ||
861 | 69 | self.assertEquals(self.region.ec2_endpoint.get_uri(), EC2_ENDPOINT_US) | ||
862 | 70 | |||
863 | 71 | def test_get_client_with_empty_cache(self): | ||
864 | 72 | key = str(EC2Client) + str(self.creds) + str(self.region.ec2_endpoint) | ||
865 | 73 | original_client = self.region._clients.get(key) | ||
866 | 74 | new_client = self.region.get_client( | ||
867 | 75 | EC2Client, creds=self.creds, endpoint=self.region.ec2_endpoint) | ||
868 | 76 | self.assertEquals(original_client, None) | ||
869 | 77 | self.assertTrue(isinstance(new_client, EC2Client)) | ||
870 | 78 | self.assertNotEquals(original_client, new_client) | ||
871 | 79 | |||
872 | 80 | def test_get_client_from_cache(self): | ||
873 | 81 | client1 = self.region.get_client( | ||
874 | 82 | EC2Client, creds=self.creds, endpoint=self.region.ec2_endpoint) | ||
875 | 83 | client2 = self.region.get_client( | ||
876 | 84 | EC2Client, creds=self.creds, endpoint=self.region.ec2_endpoint) | ||
877 | 85 | self.assertTrue(isinstance(client1, EC2Client)) | ||
878 | 86 | self.assertTrue(isinstance(client2, EC2Client)) | ||
879 | 87 | self.assertEquals(client1, client2) | ||
880 | 88 | |||
881 | 89 | def test_get_client_from_cache_with_purge(self): | ||
882 | 90 | client1 = self.region.get_client( | ||
883 | 91 | EC2Client, creds=self.creds, endpoint=self.region.ec2_endpoint, | ||
884 | 92 | purge_cache=True) | ||
885 | 93 | client2 = self.region.get_client( | ||
886 | 94 | EC2Client, creds=self.creds, endpoint=self.region.ec2_endpoint, | ||
887 | 95 | purge_cache=True) | ||
888 | 96 | self.assertTrue(isinstance(client1, EC2Client)) | ||
889 | 97 | self.assertTrue(isinstance(client2, EC2Client)) | ||
890 | 98 | self.assertNotEquals(client1, client2) | ||
891 | 99 | |||
892 | 100 | def test_get_ec2_client_from_cache(self): | ||
893 | 101 | client1 = self.region.get_ec2_client(self.creds) | ||
894 | 102 | client2 = self.region.get_ec2_client(self.creds) | ||
895 | 103 | self.assertEquals(self.creds, self.region.creds) | ||
896 | 104 | self.assertTrue(isinstance(client1, EC2Client)) | ||
897 | 105 | self.assertTrue(isinstance(client2, EC2Client)) | ||
898 | 106 | self.assertEquals(client1, client2) | ||
899 | 107 | |||
900 | 108 | |||
901 | 109 | def test_get_s3_client(self): | ||
902 | 110 | self.assertRaises(NotImplementedError, self.region.get_s3_client) | ||
903 | 111 | |||
904 | 112 | def test_get_simpledb_client(self): | ||
905 | 113 | self.assertRaises(NotImplementedError, self.region.get_simpledb_client) | ||
906 | 114 | |||
907 | 115 | def test_get_sqs_client(self): | ||
908 | 116 | self.assertRaises(NotImplementedError, self.region.get_sqs_client) |
This branch adds support for a service object that manages host endpoints as well as authorization keys (thus obviating the need for the AWSCredential object).