Merge lp:~oubiwann/txaws/416109-arbitrary-endpoints into lp:~txawsteam/txaws/trunk

Proposed by Duncan McGreggor
Status: Superseded
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
Reviewer Review Type Date Requested Status
Original txAWS Team Pending
Review via email: mp+10477@code.launchpad.net

This proposal has been superseded by a proposal from 2009-08-23.

To post a comment you must log in.
Revision history for this message
Duncan McGreggor (oubiwann) wrote :

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).

Revision history for this message
Robert Collins (lifeless) wrote :

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_region = #...

At runtime then, one would ask a region for a client of a particular
service, using some credentials.

AWS_US1.make_ec2_client(my_creds)
AWS_US1.make_sqs_client(my_creds)

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_client(self, creds=None):
        return EC2Client(creds=creds, service_endpoint=self.ec2_endpoint)

Also a bit of detail review - 'default_schema = https' - in URL terms
(see http://www.ietf.org/rfc/rfc3986.txt) that is a _scheme_, not a
_schema_.

review needsfixing

Revision history for this message
Duncan McGreggor (oubiwann) wrote :

> 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_region = #...
>
> At runtime then, one would ask a region for a client of a particular
> service, using some credentials.
>
> AWS_US1.make_ec2_client(my_creds)
> AWS_US1.make_sqs_client(my_creds)
>
> 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_client(self, creds=None):
> return EC2Client(creds=creds, service_endpoint=self.ec2_endpoint)
>
> Also a bit of detail review - 'default_schema = https' - in URL terms
> (see http://www.ietf.org/rfc/rfc3986.txt) that is a _scheme_, not a
> _schema_.
>
> review needsfixing

+1 on these suggestions. I'll give it another go with this in mind.

18. By Duncan McGreggor

A couple tiny tweaks.

19. By Duncan McGreggor

Added missing service file.

20. By Duncan McGreggor

Added credentials back.

21. By Duncan McGreggor

Merged from trunk and resolved conflicts.

Revision history for this message
Duncan McGreggor (oubiwann) wrote :

> 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_region = #...
>
> At runtime then, one would ask a region for a client of a particular
> service, using some credentials.
>
> AWS_US1.make_ec2_client(my_creds)
> AWS_US1.make_sqs_client(my_creds)
>
> 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_client(self, creds=None):
> return EC2Client(creds=creds, service_endpoint=self.ec2_endpoint)

[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://www.ietf.org/rfc/rfc3986.txt) that is a _scheme_, not a
> _schema_.

[4]

Ugh, thanks. The first place I wrote it was good, then I copied a typo everywhere else. Fixed.

22. By Duncan McGreggor

- Updated the AWSCredentials with the refactored code that had been written in
  the AWService class.
- Renamed the AWSService class to AWSServiceEndpoint (lifeless 1)
- Created and AWSServiceRegion object that acts as a client factory (lifeless 3)
- Added a placeholder for the service unit tests.
- Removed storage service.

23. By Duncan McGreggor

Added a TODO comment for tests.

24. By Duncan McGreggor

Added missing test for set_path.

Revision history for this message
Duncan McGreggor (oubiwann) wrote :

Okay! Just pushed up the latest code for the missing unit tests. It's ready for another review :-)

25. By Duncan McGreggor

Added missing tests for the AWS service region object.

26. By Duncan McGreggor

Added another check for client caching.

27. By Duncan McGreggor

Removed new cred files.

28. By Duncan McGreggor

Reverted to original cred files (-r13..12) in an effort to fix some weirdness
in this branch with those files.

29. By Duncan McGreggor

Reapplied the recent changes to the cred files.

30. By Duncan McGreggor

- Changed the gtk client to use creds and service region instead of the
  no-longer-supported service object.
- Added a cache-purging keyword parameter to the AWS service region's
  get_client method.
- Added a docstring.
- Added region string objects to service.__all__.
- Tweaked the gtk code to check for an already-installed gtk Twisted reactor.
- Cleaned up some remaining references to the service object in the client and
  replaced those with endpoint references.
- Fixed a stub query signature in a unit test to include a parameter for an
  endpoint object.

31. By Duncan McGreggor

- Removed old service unit test file.
- Added unit test for purge client option.
- Fixed typo in client check unit tests.

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.

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).

40. By Duncan McGreggor

Removed unneeded try/except block in gtk client (therve 2).

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'txaws/client/gui/gtk.py'
--- txaws/client/gui/gtk.py 2009-08-18 22:53:53 +0000
+++ txaws/client/gui/gtk.py 2009-08-20 19:14:32 +0000
@@ -8,7 +8,7 @@
8import gobject8import gobject
9import gtk9import gtk
1010
11from txaws.credentials import AWSCredentials11from txaws.ec2.service import EC2Service
1212
1313
14__all__ = ['main']14__all__ = ['main']
@@ -27,10 +27,10 @@
27 # Nested import because otherwise we get 'reactor already installed'.27 # Nested import because otherwise we get 'reactor already installed'.
28 self.password_dialog = None28 self.password_dialog = None
29 try:29 try:
30 creds = AWSCredentials()30 service = AWSService()
31 except ValueError:31 except ValueError:
32 creds = self.from_gnomekeyring()32 service = self.from_gnomekeyring()
33 self.create_client(creds)33 self.create_client(service)
34 menu = '''34 menu = '''
35 <ui>35 <ui>
36 <menubar name="Menubar">36 <menubar name="Menubar">
@@ -54,10 +54,10 @@
54 '/Menubar/Menu/Stop instances').props.parent54 '/Menubar/Menu/Stop instances').props.parent
55 self.connect('popup-menu', self.on_popup_menu)55 self.connect('popup-menu', self.on_popup_menu)
5656
57 def create_client(self, creds):57 def create_client(self, service):
58 from txaws.ec2.client import EC2Client58 from txaws.ec2.client import EC2Client
59 if creds is not None:59 if service is not None:
60 self.client = EC2Client(creds=creds)60 self.client = EC2Client(service=service)
61 self.on_activate(None)61 self.on_activate(None)
62 else:62 else:
63 # waiting on user entered credentials.63 # waiting on user entered credentials.
@@ -65,7 +65,7 @@
6565
66 def from_gnomekeyring(self):66 def from_gnomekeyring(self):
67 # Try for gtk gui specific credentials.67 # Try for gtk gui specific credentials.
68 creds = None68 service = None
69 try:69 try:
70 items = gnomekeyring.find_items_sync(70 items = gnomekeyring.find_items_sync(
71 gnomekeyring.ITEM_GENERIC_SECRET,71 gnomekeyring.ITEM_GENERIC_SECRET,
@@ -78,7 +78,7 @@
78 return None78 return None
79 else:79 else:
80 key_id, secret_key = items[0].secret.split(':')80 key_id, secret_key = items[0].secret.split(':')
81 return AWSCredentials(access_key=key_id, secret_key=secret_key)81 return EC2Service(access_key=key_id, secret_key=secret_key)
8282
83 def show_a_password_dialog(self):83 def show_a_password_dialog(self):
84 self.password_dialog = gtk.Dialog(84 self.password_dialog = gtk.Dialog(
@@ -133,8 +133,8 @@
133 content = self.password_dialog.get_content_area()133 content = self.password_dialog.get_content_area()
134 key_id = content.get_children()[0].get_children()[1].get_text()134 key_id = content.get_children()[0].get_children()[1].get_text()
135 secret_key = content.get_children()[1].get_children()[1].get_text()135 secret_key = content.get_children()[1].get_children()[1].get_text()
136 creds = AWSCredentials(access_key=key_id, secret_key=secret_key)136 service = EC2Service(access_key=key_id, secret_key=secret_key)
137 self.create_client(creds)137 self.create_client(service)
138 gnomekeyring.item_create_sync(138 gnomekeyring.item_create_sync(
139 None,139 None,
140 gnomekeyring.ITEM_GENERIC_SECRET,140 gnomekeyring.ITEM_GENERIC_SECRET,
141141
=== removed file 'txaws/credentials.py'
--- txaws/credentials.py 2009-08-17 11:18:56 +0000
+++ txaws/credentials.py 1970-01-01 00:00:00 +0000
@@ -1,37 +0,0 @@
1# Copyright (C) 2009 Robert Collins <robertc@robertcollins.net>
2# Licenced under the txaws licence available at /LICENSE in the txaws source.
3
4"""Credentials for accessing AWS services."""
5
6import os
7
8from txaws.util import *
9
10
11__all__ = ['AWSCredentials']
12
13
14class AWSCredentials(object):
15
16 def __init__(self, access_key=None, secret_key=None):
17 """Create an AWSCredentials object.
18
19 :param access_key: The access key to use. If None the environment
20 variable AWS_ACCESS_KEY_ID is consulted.
21 :param secret_key: The secret key to use. If None the environment
22 variable AWS_SECRET_ACCESS_KEY is consulted.
23 """
24 self.secret_key = secret_key
25 if self.secret_key is None:
26 self.secret_key = os.environ.get('AWS_SECRET_ACCESS_KEY')
27 if self.secret_key is None:
28 raise ValueError('Could not find AWS_SECRET_ACCESS_KEY')
29 self.access_key = access_key
30 if self.access_key is None:
31 self.access_key = os.environ.get('AWS_ACCESS_KEY_ID')
32 if self.access_key is None:
33 raise ValueError('Could not find AWS_ACCESS_KEY_ID')
34
35 def sign(self, bytes):
36 """Sign some bytes."""
37 return hmac_sha1(self.secret_key, bytes)
380
=== modified file 'txaws/ec2/client.py'
--- txaws/ec2/client.py 2009-08-18 21:56:36 +0000
+++ txaws/ec2/client.py 2009-08-20 16:47:54 +0000
@@ -8,7 +8,7 @@
88
9from twisted.web.client import getPage9from twisted.web.client import getPage
1010
11from txaws import credentials11from txaws.ec2.service import EC2Service
12from txaws.util import iso8601time, XML12from txaws.util import iso8601time, XML
1313
1414
@@ -46,16 +46,15 @@
4646
47 name_space = '{http://ec2.amazonaws.com/doc/2008-12-01/}'47 name_space = '{http://ec2.amazonaws.com/doc/2008-12-01/}'
4848
49 def __init__(self, creds=None, query_factory=None):49 def __init__(self, service=None, query_factory=None):
50 """Create an EC2Client.50 """Create an EC2Client.
5151
52 @param creds: Explicit credentials to use. If None, credentials are52 @param service: Explicit service to use.
53 inferred as per txaws.credentials.AWSCredentials.
54 """53 """
55 if creds is None:54 if service is None:
56 self.creds = credentials.AWSCredentials()55 self.service = EC2Service()
57 else:56 else:
58 self.creds = creds57 self.service = service
59 if query_factory is None:58 if query_factory is None:
60 self.query_factory = Query59 self.query_factory = Query
61 else:60 else:
@@ -63,7 +62,7 @@
6362
64 def describe_instances(self):63 def describe_instances(self):
65 """Describe current instances."""64 """Describe current instances."""
66 q = self.query_factory('DescribeInstances', self.creds)65 q = self.query_factory('DescribeInstances', self.service)
67 d = q.submit()66 d = q.submit()
68 return d.addCallback(self._parse_instances)67 return d.addCallback(self._parse_instances)
6968
@@ -119,7 +118,7 @@
119 instanceset = {}118 instanceset = {}
120 for pos, instance_id in enumerate(instance_ids):119 for pos, instance_id in enumerate(instance_ids):
121 instanceset["InstanceId.%d" % (pos+1)] = instance_id120 instanceset["InstanceId.%d" % (pos+1)] = instance_id
122 q = self.query_factory('TerminateInstances', self.creds, instanceset)121 q = self.query_factory('TerminateInstances', self.service, instanceset)
123 d = q.submit()122 d = q.submit()
124 return d.addCallback(self._parse_terminate_instances)123 return d.addCallback(self._parse_terminate_instances)
125124
@@ -142,7 +141,7 @@
142class Query(object):141class Query(object):
143 """A query that may be submitted to EC2."""142 """A query that may be submitted to EC2."""
144143
145 def __init__(self, action, creds, other_params=None, time_tuple=None):144 def __init__(self, action, service, other_params=None, time_tuple=None):
146 """Create a Query to submit to EC2."""145 """Create a Query to submit to EC2."""
147 # Require params (2008-12-01 API):146 # Require params (2008-12-01 API):
148 # Version, SignatureVersion, SignatureMethod, Action, AWSAccessKeyId,147 # Version, SignatureVersion, SignatureMethod, Action, AWSAccessKeyId,
@@ -151,15 +150,12 @@
151 'SignatureVersion': '2',150 'SignatureVersion': '2',
152 'SignatureMethod': 'HmacSHA1',151 'SignatureMethod': 'HmacSHA1',
153 'Action': action,152 'Action': action,
154 'AWSAccessKeyId': creds.access_key,153 'AWSAccessKeyId': service.access_key,
155 'Timestamp': iso8601time(time_tuple),154 'Timestamp': iso8601time(time_tuple),
156 }155 }
157 if other_params:156 if other_params:
158 self.params.update(other_params)157 self.params.update(other_params)
159 self.method = 'GET'158 self.service = service
160 self.host = 'ec2.amazonaws.com'
161 self.uri = '/'
162 self.creds = creds
163159
164 def canonical_query_params(self):160 def canonical_query_params(self):
165 """Return the canonical query params (used in signing)."""161 """Return the canonical query params (used in signing)."""
@@ -178,18 +174,19 @@
178174
179 def signing_text(self):175 def signing_text(self):
180 """Return the text to be signed when signing the query."""176 """Return the text to be signed when signing the query."""
181 result = "%s\n%s\n%s\n%s" % (self.method, self.host, self.uri,177 result = "%s\n%s\n%s\n%s" % (self.service.method, self.service.host,
182 self.canonical_query_params())178 self.service.endpoint,
179 self.canonical_query_params())
183 return result180 return result
184181
185 def sign(self):182 def sign(self):
186 """Sign this query using its built in credentials.183 """Sign this query using its built in service.
187 184
188 This prepares it to be sent, and should be done as the last step before185 This prepares it to be sent, and should be done as the last step before
189 submitting the query. Signing is done automatically - this is a public186 submitting the query. Signing is done automatically - this is a public
190 method to facilitate testing.187 method to facilitate testing.
191 """188 """
192 self.params['Signature'] = self.creds.sign(self.signing_text())189 self.params['Signature'] = self.service.sign(self.signing_text())
193190
194 def sorted_params(self):191 def sorted_params(self):
195 """Return the query params sorted appropriately for signing."""192 """Return the query params sorted appropriately for signing."""
@@ -198,9 +195,8 @@
198 def submit(self):195 def submit(self):
199 """Submit this query.196 """Submit this query.
200197
201 :return: A deferred from twisted.web.client.getPage198 @return: A deferred from twisted.web.client.getPage
202 """199 """
203 self.sign()200 self.sign()
204 url = 'http://%s%s?%s' % (self.host, self.uri,201 url = "%s?%s" % (self.service.get_url(), self.canonical_query_params())
205 self.canonical_query_params())202 return getPage(url, method=self.service.method)
206 return getPage(url, method=self.method)
207203
=== modified file 'txaws/ec2/tests/test_client.py'
--- txaws/ec2/tests/test_client.py 2009-08-18 21:56:36 +0000
+++ txaws/ec2/tests/test_client.py 2009-08-20 16:47:54 +0000
@@ -5,8 +5,8 @@
55
6from twisted.internet.defer import succeed6from twisted.internet.defer import succeed
77
8from txaws.credentials import AWSCredentials
9from txaws.ec2 import client8from txaws.ec2 import client
9from txaws.ec2.service import EC2Service, US_EC2_HOST
10from txaws.tests import TXAWSTestCase10from txaws.tests import TXAWSTestCase
1111
1212
@@ -91,21 +91,21 @@
91 self.assertEquals(reservation.groups, ["one", "two"])91 self.assertEquals(reservation.groups, ["one", "two"])
9292
9393
94class TestEC2Client(TXAWSTestCase):94class EC2ClientTestCase(TXAWSTestCase):
95 95
96 def test_init_no_creds(self):96 def test_init_no_creds(self):
97 os.environ['AWS_SECRET_ACCESS_KEY'] = 'foo'97 os.environ['AWS_SECRET_ACCESS_KEY'] = 'foo'
98 os.environ['AWS_ACCESS_KEY_ID'] = 'bar'98 os.environ['AWS_ACCESS_KEY_ID'] = 'bar'
99 ec2 = client.EC2Client()99 ec2 = client.EC2Client()
100 self.assertNotEqual(None, ec2.creds)100 self.assertNotEqual(None, ec2.service)
101101
102 def test_init_no_creds_non_available_errors(self):102 def test_init_no_creds_non_available_errors(self):
103 self.assertRaises(ValueError, client.EC2Client)103 self.assertRaises(ValueError, client.EC2Client)
104104
105 def test_init_explicit_creds(self):105 def test_init_explicit_service(self):
106 creds = 'foo'106 service = EC2Service("foo", "bar")
107 ec2 = client.EC2Client(creds=creds)107 ec2 = client.EC2Client(service=service)
108 self.assertEqual(creds, ec2.creds)108 self.assertEqual(service, ec2.service)
109109
110 def check_parsed_instances(self, results):110 def check_parsed_instances(self, results):
111 instance = results[0]111 instance = results[0]
@@ -118,33 +118,38 @@
118 self.assertEquals(group, "default")118 self.assertEquals(group, "default")
119119
120 def test_parse_reservation(self):120 def test_parse_reservation(self):
121 ec2 = client.EC2Client(creds='foo')121 service = EC2Service("foo", "bar")
122 ec2 = client.EC2Client(service=service)
122 results = ec2._parse_instances(sample_describe_instances_result)123 results = ec2._parse_instances(sample_describe_instances_result)
123 self.check_parsed_instances(results)124 self.check_parsed_instances(results)
124125
125 def test_describe_instances(self):126 def test_describe_instances(self):
126 class StubQuery(object):127 class StubQuery(object):
127 def __init__(stub, action, creds):128 def __init__(stub, action, service):
128 self.assertEqual(action, 'DescribeInstances')129 self.assertEqual(action, 'DescribeInstances')
129 self.assertEqual('foo', creds)130 self.assertEqual(service.access_key, "foo")
131 self.assertEqual(service.secret_key, "bar")
130 def submit(self):132 def submit(self):
131 return succeed(sample_describe_instances_result)133 return succeed(sample_describe_instances_result)
132 ec2 = client.EC2Client(creds='foo', query_factory=StubQuery)134 service = EC2Service("foo", "bar")
135 ec2 = client.EC2Client(service, query_factory=StubQuery)
133 d = ec2.describe_instances()136 d = ec2.describe_instances()
134 d.addCallback(self.check_parsed_instances)137 d.addCallback(self.check_parsed_instances)
135 return d138 return d
136139
137 def test_terminate_instances(self):140 def test_terminate_instances(self):
138 class StubQuery(object):141 class StubQuery(object):
139 def __init__(stub, action, creds, other_params):142 def __init__(stub, action, service, other_params):
140 self.assertEqual(action, 'TerminateInstances')143 self.assertEqual(action, 'TerminateInstances')
141 self.assertEqual('foo', creds)144 self.assertEqual(service.access_key, "foo")
145 self.assertEqual(service.secret_key, "bar")
142 self.assertEqual(146 self.assertEqual(
143 {'InstanceId.1': 'i-1234', 'InstanceId.2': 'i-5678'},147 {'InstanceId.1': 'i-1234', 'InstanceId.2': 'i-5678'},
144 other_params)148 other_params)
145 def submit(self):149 def submit(self):
146 return succeed(sample_terminate_instances_result)150 return succeed(sample_terminate_instances_result)
147 ec2 = client.EC2Client(creds='foo', query_factory=StubQuery)151 service = EC2Service("foo", "bar")
152 ec2 = client.EC2Client(service=service, query_factory=StubQuery)
148 d = ec2.terminate_instances('i-1234', 'i-5678')153 d = ec2.terminate_instances('i-1234', 'i-5678')
149 def check_transition(changes):154 def check_transition(changes):
150 self.assertEqual([('i-1234', 'running', 'shutting-down'),155 self.assertEqual([('i-1234', 'running', 'shutting-down'),
@@ -152,14 +157,14 @@
152 return d157 return d
153158
154159
155class TestQuery(TXAWSTestCase):160class QueryTestCase(TXAWSTestCase):
156161
157 def setUp(self):162 def setUp(self):
158 TXAWSTestCase.setUp(self)163 TXAWSTestCase.setUp(self)
159 self.creds = AWSCredentials('foo', 'bar')164 self.service = EC2Service('foo', 'bar')
160165
161 def test_init_minimum(self):166 def test_init_minimum(self):
162 query = client.Query('DescribeInstances', self.creds)167 query = client.Query('DescribeInstances', self.service)
163 self.assertTrue('Timestamp' in query.params)168 self.assertTrue('Timestamp' in query.params)
164 del query.params['Timestamp']169 del query.params['Timestamp']
165 self.assertEqual(170 self.assertEqual(
@@ -173,11 +178,11 @@
173 def test_init_requires_action(self):178 def test_init_requires_action(self):
174 self.assertRaises(TypeError, client.Query)179 self.assertRaises(TypeError, client.Query)
175180
176 def test_init_requires_creds(self):181 def test_init_requires_service_with_creds(self):
177 self.assertRaises(TypeError, client.Query, None)182 self.assertRaises(TypeError, client.Query, None)
178183
179 def test_init_other_args_are_params(self):184 def test_init_other_args_are_params(self):
180 query = client.Query('DescribeInstances', self.creds,185 query = client.Query('DescribeInstances', self.service,
181 {'InstanceId.0': '12345'},186 {'InstanceId.0': '12345'},
182 time_tuple=(2007,11,12,13,14,15,0,0,0))187 time_tuple=(2007,11,12,13,14,15,0,0,0))
183 self.assertEqual(188 self.assertEqual(
@@ -191,7 +196,7 @@
191 query.params)196 query.params)
192197
193 def test_sorted_params(self):198 def test_sorted_params(self):
194 query = client.Query('DescribeInstances', self.creds,199 query = client.Query('DescribeInstances', self.service,
195 {'fun': 'games'},200 {'fun': 'games'},
196 time_tuple=(2007,11,12,13,14,15,0,0,0))201 time_tuple=(2007,11,12,13,14,15,0,0,0))
197 self.assertEqual([202 self.assertEqual([
@@ -207,16 +212,16 @@
207 def test_encode_unreserved(self):212 def test_encode_unreserved(self):
208 all_unreserved = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'213 all_unreserved = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
209 'abcdefghijklmnopqrstuvwxyz0123456789-_.~')214 'abcdefghijklmnopqrstuvwxyz0123456789-_.~')
210 query = client.Query('DescribeInstances', self.creds)215 query = client.Query('DescribeInstances', self.service)
211 self.assertEqual(all_unreserved, query.encode(all_unreserved))216 self.assertEqual(all_unreserved, query.encode(all_unreserved))
212217
213 def test_encode_space(self):218 def test_encode_space(self):
214 """This may be just 'url encode', but the AWS manual isn't clear."""219 """This may be just 'url encode', but the AWS manual isn't clear."""
215 query = client.Query('DescribeInstances', self.creds)220 query = client.Query('DescribeInstances', self.service)
216 self.assertEqual('a%20space', query.encode('a space'))221 self.assertEqual('a%20space', query.encode('a space'))
217222
218 def test_canonical_query(self):223 def test_canonical_query(self):
219 query = client.Query('DescribeInstances', self.creds,224 query = client.Query('DescribeInstances', self.service,
220 {'fu n': 'g/ames', 'argwithnovalue':'',225 {'fu n': 'g/ames', 'argwithnovalue':'',
221 'InstanceId.1': 'i-1234'},226 'InstanceId.1': 'i-1234'},
222 time_tuple=(2007,11,12,13,14,15,0,0,0))227 time_tuple=(2007,11,12,13,14,15,0,0,0))
@@ -228,17 +233,17 @@
228 self.assertEqual(expected_query, query.canonical_query_params())233 self.assertEqual(expected_query, query.canonical_query_params())
229234
230 def test_signing_text(self):235 def test_signing_text(self):
231 query = client.Query('DescribeInstances', self.creds,236 query = client.Query('DescribeInstances', self.service,
232 time_tuple=(2007,11,12,13,14,15,0,0,0))237 time_tuple=(2007,11,12,13,14,15,0,0,0))
233 signing_text = ('GET\nec2.amazonaws.com\n/\n'238 signing_text = ('GET\n%s\n/\n' % US_EC2_HOST +
234 'AWSAccessKeyId=foo&Action=DescribeInstances&'239 'AWSAccessKeyId=foo&Action=DescribeInstances&'
235 'SignatureMethod=HmacSHA1&SignatureVersion=2&'240 'SignatureMethod=HmacSHA1&SignatureVersion=2&'
236 'Timestamp=2007-11-12T13%3A14%3A15Z&Version=2008-12-01')241 'Timestamp=2007-11-12T13%3A14%3A15Z&Version=2008-12-01')
237 self.assertEqual(signing_text, query.signing_text())242 self.assertEqual(signing_text, query.signing_text())
238243
239 def test_sign(self):244 def test_sign(self):
240 query = client.Query('DescribeInstances', self.creds,245 query = client.Query('DescribeInstances', self.service,
241 time_tuple=(2007,11,12,13,14,15,0,0,0))246 time_tuple=(2007,11,12,13,14,15,0,0,0))
242 query.sign()247 query.sign()
243 self.assertEqual('4hEtLuZo9i6kuG3TOXvRQNOrE/U=',248 self.assertEqual('JuCpwFA2H4OVF3Ql/lAQs+V6iMc=',
244 query.params['Signature'])249 query.params['Signature'])
245250
=== added file 'txaws/service.py'
--- txaws/service.py 1970-01-01 00:00:00 +0000
+++ txaws/service.py 2009-08-20 19:09:56 +0000
@@ -0,0 +1,70 @@
1# Copyright (C) 2009 Duncan McGreggor <duncan@canonical.com>
2# Copyright (C) 2009 Robert Collins <robertc@robertcollins.net>
3# Licenced under the txaws licence available at /LICENSE in the txaws source.
4
5import os
6
7from twisted.web.client import _parse
8
9from txaws.util import hmac_sha1
10
11DEFAULT_PORT = 80
12ENV_ACCESS_KEY = "AWS_ACCESS_KEY_ID"
13ENV_SECRET_KEY = "AWS_SECRET_ACCESS_KEY"
14
15class AWSService(object):
16 """
17 @param access_key: The access key to use. If None the environment
18 variable AWS_ACCESS_KEY_ID is consulted.
19 @param secret_key: The secret key to use. If None the environment
20 variable AWS_SECRET_ACCESS_KEY is consulted.
21 @param uri: The URL for the service.
22 @param method: The HTTP method used when accessing a service.
23 """
24 default_host = ""
25 default_schema = "https"
26
27 def __init__(self, access_key="", secret_key="", uri="", method="GET"):
28 self.access_key = access_key
29 self.secret_key = secret_key
30 self.schema = ""
31 self.host = ""
32 self.port = DEFAULT_PORT
33 self.endpoint = "/"
34 self.method = method
35 self._process_creds()
36 self._parse_uri(uri)
37 if not self.host:
38 self.host = self.default_host
39 if not self.schema:
40 self.schema = self.default_schema
41
42 def _process_creds(self):
43 # perform checks for access key
44 if not self.access_key:
45 self.access_key = os.environ.get(ENV_ACCESS_KEY)
46 if not self.access_key:
47 raise ValueError("Could not find %s" % ENV_ACCESS_KEY)
48 # perform checks for secret key
49 if not self.secret_key:
50 self.secret_key = os.environ.get(ENV_SECRET_KEY)
51 if not self.secret_key:
52 raise ValueError("Could not find %s" % ENV_SECRET_KEY)
53
54 def _parse_uri(self, uri):
55 scheme, host, port, endpoint = _parse(uri, defaultPort=DEFAULT_PORT)
56 self.schema = scheme
57 self.host = host
58 self.port = port
59 self.endpoint = endpoint
60
61 def get_uri(self):
62 """Get a URL representation of the service."""
63 uri = "%s://%s" % (self.schema, self.host)
64 if self.port and self.port != DEFAULT_PORT:
65 uri = "%s:%s" % (uri, self.port)
66 return uri + self.endpoint
67
68 def sign(self, bytes):
69 """Sign some bytes."""
70 return hmac_sha1(self.secret_key, bytes)
071
=== modified file 'txaws/storage/client.py'
--- txaws/storage/client.py 2009-08-17 11:18:56 +0000
+++ txaws/storage/client.py 2009-08-20 19:09:56 +0000
@@ -10,178 +10,170 @@
10from hashlib import md510from hashlib import md5
11from base64 import b64encode11from base64 import b64encode
1212
13
14from epsilon.extime import Time13from epsilon.extime import Time
1514
16from twisted.web.client import getPage15from twisted.web.client import getPage
17from twisted.web.http import datetimeToString16from twisted.web.http import datetimeToString
1817
19from txaws.credentials import AWSCredentials18from txaws.util import XML, calculate_md5
20from txaws.util import XML19from txaws.service import AWSService
2120
2221
23def calculateMD5(data):22name_space = '{http://s3.amazonaws.com/doc/2006-03-01/}'
24 digest = md5(data).digest()
25 return b64encode(digest)
2623
2724
28class S3Request(object):25class S3Request(object):
29 def __init__(self, verb, bucket=None, objectName=None, data='',26
30 contentType=None, metadata={}, rootURI='https://s3.amazonaws.com',27 def __init__(self, verb, bucket=None, object_name=None, data='',
31 creds=None):28 content_type=None, metadata={}, service=None):
32 self.verb = verb29 self.verb = verb
33 self.bucket = bucket30 self.bucket = bucket
34 self.objectName = objectName31 self.object_name = object_name
35 self.data = data32 self.data = data
36 self.contentType = contentType33 self.content_type = content_type
37 self.metadata = metadata34 self.metadata = metadata
38 self.rootURI = rootURI35 self.service = service
39 self.creds = creds36 self.service.endpoint = self.get_path()
40 self.date = datetimeToString()37 self.date = datetimeToString()
4138
42 def getURIPath(self):39 def get_path(self):
43 path = '/'40 path = '/'
44 if self.bucket is not None:41 if self.bucket is not None:
45 path += self.bucket42 path += self.bucket
46 if self.objectName is not None:43 if self.object_name is not None:
47 path += '/' + self.objectName44 path += '/' + self.object_name
48 return path45 return path
4946
50 def getURI(self):47 def get_uri(self):
51 return self.rootURI + self.getURIPath()48 return self.service.get_uri()
5249
53 def getHeaders(self):50 def get_headers(self):
54 headers = {'Content-Length': len(self.data),51 headers = {'Content-Length': len(self.data),
55 'Content-MD5': calculateMD5(self.data),52 'Content-MD5': calculate_md5(self.data),
56 'Date': self.date}53 'Date': self.date}
5754
58 for key, value in self.metadata.iteritems():55 for key, value in self.metadata.iteritems():
59 headers['x-amz-meta-' + key] = value56 headers['x-amz-meta-' + key] = value
6057
61 if self.contentType is not None:58 if self.content_type is not None:
62 headers['Content-Type'] = self.contentType59 headers['Content-Type'] = self.content_type
6360
64 if self.creds is not None:61 if self.service is not None:
65 signature = self.getSignature(headers)62 signature = self.get_signature(headers)
66 headers['Authorization'] = 'AWS %s:%s' % (63 headers['Authorization'] = 'AWS %s:%s' % (
67 self.creds.access_key, signature)64 self.service.access_key, signature)
68
69 return headers65 return headers
7066
71 def getCanonicalizedResource(self):67 def get_canonicalized_resource(self):
72 return self.getURIPath()68 return self.get_path()
7369
74 def getCanonicalizedAmzHeaders(self, headers):70 def get_canonicalized_amz_headers(self, headers):
75 result = ''71 result = ''
76 headers = [(name.lower(), value) for name, value in headers.iteritems()72 headers = [(name.lower(), value) for name, value in headers.iteritems()
77 if name.lower().startswith('x-amz-')]73 if name.lower().startswith('x-amz-')]
78 headers.sort()74 headers.sort()
79 return ''.join('%s:%s\n' % (name, value) for name, value in headers)75 return ''.join('%s:%s\n' % (name, value) for name, value in headers)
8076
81 def getSignature(self, headers):77 def get_signature(self, headers):
82 text = self.verb + '\n'78 text = (self.verb + '\n' +
83 text += headers.get('Content-MD5', '') + '\n'79 headers.get('Content-MD5', '') + '\n' +
84 text += headers.get('Content-Type', '') + '\n'80 headers.get('Content-Type', '') + '\n' +
85 text += headers.get('Date', '') + '\n'81 headers.get('Date', '') + '\n' +
86 text += self.getCanonicalizedAmzHeaders(headers)82 self.get_canonicalized_amz_headers(headers) +
87 text += self.getCanonicalizedResource()83 self.get_canonicalized_resource())
88 return self.creds.sign(text)84 return self.service.sign(text)
8985
90 def submit(self):86 def submit(self):
91 return self.getPage(87 return self.get_page(url=self.get_uri(), method=self.verb,
92 url=self.getURI(), method=self.verb, postdata=self.data,88 postdata=self.data, headers=self.get_headers())
93 headers=self.getHeaders())
9489
95 def getPage(self, *a, **kw):90 def get_page(self, *a, **kw):
96 return getPage(*a, **kw)91 return getPage(*a, **kw)
9792
9893
99NS = '{http://s3.amazonaws.com/doc/2006-03-01/}'
100
101
102class S3(object):94class S3(object):
103 rootURI = 'https://s3.amazonaws.com/'95
104 requestFactory = S3Request96 request_factory = S3Request
10597
106 def __init__(self, creds):98 def __init__(self, service):
107 self.creds = creds99 self.service = service
108100
109 def makeRequest(self, *a, **kw):101 def make_request(self, *a, **kw):
110 """102 """
111 Create a request with the arguments passed in.103 Create a request with the arguments passed in.
112104
113 This uses the requestFactory attribute, adding the credentials to the105 This uses the request_factory attribute, adding the service to the
114 arguments passed in.106 arguments passed in.
115 """107 """
116 return self.requestFactory(creds=self.creds, *a, **kw)108 return self.request_factory(service=self.service, *a, **kw)
117109
118 def _parseBucketList(self, response):110 def _parse_bucket_list(self, response):
119 """111 """
120 Parse XML bucket list response.112 Parse XML bucket list response.
121 """113 """
122 root = XML(response)114 root = XML(response)
123 for bucket in root.find(NS + 'Buckets'):115 for bucket in root.find(name_space + 'Buckets'):
124 timeText = bucket.findtext(NS + 'CreationDate')116 timeText = bucket.findtext(name_space + 'CreationDate')
125 yield {117 yield {
126 'name': bucket.findtext(NS + 'Name'),118 'name': bucket.findtext(name_space + 'Name'),
127 'created': Time.fromISO8601TimeAndDate(timeText),119 'created': Time.fromISO8601TimeAndDate(timeText),
128 }120 }
129121
130 def listBuckets(self):122 def list_buckets(self):
131 """123 """
132 List all buckets.124 List all buckets.
133125
134 Returns a list of all the buckets owned by the authenticated sender of126 Returns a list of all the buckets owned by the authenticated sender of
135 the request.127 the request.
136 """128 """
137 d = self.makeRequest('GET').submit()129 deferred = self.make_request('GET').submit()
138 d.addCallback(self._parseBucketList)130 deferred.addCallback(self._parse_bucket_list)
139 return d131 return deferred
140132
141 def createBucket(self, bucket):133 def create_bucket(self, bucket):
142 """134 """
143 Create a new bucket.135 Create a new bucket.
144 """136 """
145 return self.makeRequest('PUT', bucket).submit()137 return self.make_request('PUT', bucket).submit()
146138
147 def deleteBucket(self, bucket):139 def delete_bucket(self, bucket):
148 """140 """
149 Delete a bucket.141 Delete a bucket.
150142
151 The bucket must be empty before it can be deleted.143 The bucket must be empty before it can be deleted.
152 """144 """
153 return self.makeRequest('DELETE', bucket).submit()145 return self.make_request('DELETE', bucket).submit()
154146
155 def putObject(self, bucket, objectName, data, contentType=None,147 def put_object(self, bucket, object_name, data, content_type=None,
156 metadata={}):148 metadata={}):
157 """149 """
158 Put an object in a bucket.150 Put an object in a bucket.
159151
160 Any existing object of the same name will be replaced.152 Any existing object of the same name will be replaced.
161 """153 """
162 return self.makeRequest(154 return self.make_request('PUT', bucket, object_name, data,
163 'PUT', bucket, objectName, data, contentType, metadata).submit()155 content_type, metadata).submit()
164156
165 def getObject(self, bucket, objectName):157 def get_object(self, bucket, object_name):
166 """158 """
167 Get an object from a bucket.159 Get an object from a bucket.
168 """160 """
169 return self.makeRequest('GET', bucket, objectName).submit()161 return self.make_request('GET', bucket, object_name).submit()
170162
171 def headObject(self, bucket, objectName):163 def head_object(self, bucket, object_name):
172 """164 """
173 Retrieve object metadata only.165 Retrieve object metadata only.
174166
175 This is like getObject, but the object's content is not retrieved.167 This is like get_object, but the object's content is not retrieved.
176 Currently the metadata is not returned to the caller either, so this168 Currently the metadata is not returned to the caller either, so this
177 method is mostly useless, and only provided for completeness.169 method is mostly useless, and only provided for completeness.
178 """170 """
179 return self.makeRequest('HEAD', bucket, objectName).submit()171 return self.make_request('HEAD', bucket, object_name).submit()
180172
181 def deleteObject(self, bucket, objectName):173 def delete_object(self, bucket, object_name):
182 """174 """
183 Delete an object from a bucket.175 Delete an object from a bucket.
184176
185 Once deleted, there is no method to restore or undelete an object.177 Once deleted, there is no method to restore or undelete an object.
186 """178 """
187 return self.makeRequest('DELETE', bucket, objectName).submit()179 return self.make_request('DELETE', bucket, object_name).submit()
188180
=== added file 'txaws/storage/service.py'
--- txaws/storage/service.py 1970-01-01 00:00:00 +0000
+++ txaws/storage/service.py 2009-08-20 19:09:56 +0000
@@ -0,0 +1,19 @@
1# Copyright (C) 2009 Duncan McGreggor <duncan@canonical.com>
2# Licenced under the txaws licence available at /LICENSE in the txaws source.
3
4from txaws.service import AWSService
5
6
7S3_HOST = "s3.amazonaws.com"
8
9
10class S3Service(AWSService):
11 """
12 This service uses the standard S3 host defined with S3_HOST by default. To
13 override this behaviour, simply pass the desired value in the "host"
14 keyword parameter.
15
16 For more details, see txaws.service.AWSService.
17 """
18 default_host = S3_HOST
19 default_schema = "https"
020
=== modified file 'txaws/storage/test/test_client.py'
--- txaws/storage/test/test_client.py 2009-08-15 03:28:45 +0000
+++ txaws/storage/test/test_client.py 2009-08-20 19:09:56 +0000
@@ -4,20 +4,23 @@
44
5from twisted.internet.defer import succeed5from twisted.internet.defer import succeed
66
7from txaws.credentials import AWSCredentials7from txaws.util import calculate_md5
8from txaws.tests import TXAWSTestCase8from txaws.tests import TXAWSTestCase
9from txaws.storage.client import S3, S3Request, calculateMD59from txaws.storage.service import S3Service
10from txaws.storage.client import S3, S3Request
11
1012
1113
12class StubbedS3Request(S3Request):14class StubbedS3Request(S3Request):
13 def getPage(self, url, method, postdata, headers):15
16 def get_page(self, url, method, postdata, headers):
14 self.getPageArgs = (url, method, postdata, headers)17 self.getPageArgs = (url, method, postdata, headers)
15 return succeed('')18 return succeed('')
1619
1720
18class RequestTests(TXAWSTestCase):21class RequestTestCase(TXAWSTestCase):
19 creds = AWSCredentials(access_key='0PN5J17HBGZHT7JJ3X82',22
20 secret_key='uV3F3YluFJax1cknvbcGwgjvx4QpvB+leU8dUj2o')23 service = S3Service(access_key='fookeyid', secret_key='barsecretkey')
2124
22 def test_objectRequest(self):25 def test_objectRequest(self):
23 """26 """
@@ -26,20 +29,23 @@
26 DATA = 'objectData'29 DATA = 'objectData'
27 DIGEST = 'zhdB6gwvocWv/ourYUWMxA=='30 DIGEST = 'zhdB6gwvocWv/ourYUWMxA=='
2831
29 request = S3Request(32 request = S3Request('PUT', 'somebucket', 'object/name/here', DATA,
30 'PUT', 'somebucket', 'object/name/here', DATA, 33 content_type='text/plain', metadata={'foo': 'bar'},
31 contentType='text/plain', metadata={'foo': 'bar'})34 service=self.service)
35 request.get_signature = lambda headers: "TESTINGSIG="
32 self.assertEqual(request.verb, 'PUT')36 self.assertEqual(request.verb, 'PUT')
33 self.assertEqual(37 self.assertEqual(
34 request.getURI(),38 request.get_uri(),
35 'https://s3.amazonaws.com/somebucket/object/name/here')39 'https://s3.amazonaws.com/somebucket/object/name/here')
36 headers = request.getHeaders()40 headers = request.get_headers()
37 self.assertNotEqual(headers.pop('Date'), '')41 self.assertNotEqual(headers.pop('Date'), '')
38 self.assertEqual(headers,42 self.assertEqual(
39 {'Content-Type': 'text/plain',43 headers, {
40 'Content-Length': len(DATA),44 'Authorization': 'AWS fookeyid:TESTINGSIG=',
41 'Content-MD5': DIGEST,45 'Content-Type': 'text/plain',
42 'x-amz-meta-foo': 'bar'})46 'Content-Length': len(DATA),
47 'Content-MD5': DIGEST,
48 'x-amz-meta-foo': 'bar'})
43 self.assertEqual(request.data, 'objectData')49 self.assertEqual(request.data, 'objectData')
4450
45 def test_bucketRequest(self):51 def test_bucketRequest(self):
@@ -48,42 +54,46 @@
48 """54 """
49 DIGEST = '1B2M2Y8AsgTpgAmY7PhCfg=='55 DIGEST = '1B2M2Y8AsgTpgAmY7PhCfg=='
5056
51 request = S3Request('GET', 'somebucket')57 request = S3Request('GET', 'somebucket', service=self.service)
58 request.get_signature = lambda headers: "TESTINGSIG="
52 self.assertEqual(request.verb, 'GET')59 self.assertEqual(request.verb, 'GET')
53 self.assertEqual(60 self.assertEqual(
54 request.getURI(), 'https://s3.amazonaws.com/somebucket')61 request.get_uri(), 'https://s3.amazonaws.com/somebucket')
55 headers = request.getHeaders()62 headers = request.get_headers()
56 self.assertNotEqual(headers.pop('Date'), '')63 self.assertNotEqual(headers.pop('Date'), '')
57 self.assertEqual(headers,64 self.assertEqual(
58 {'Content-Length': 0,65 headers, {
59 'Content-MD5': DIGEST})66 'Authorization': 'AWS fookeyid:TESTINGSIG=',
67 'Content-Length': 0,
68 'Content-MD5': DIGEST})
60 self.assertEqual(request.data, '')69 self.assertEqual(request.data, '')
6170
62 def test_submit(self):71 def test_submit(self):
63 """72 """
64 Submitting the request should invoke getPage correctly.73 Submitting the request should invoke getPage correctly.
65 """74 """
66 request = StubbedS3Request('GET', 'somebucket')75 request = StubbedS3Request('GET', 'somebucket', service=self.service)
6776
68 def _postCheck(result):77 def _postCheck(result):
69 self.assertEqual(result, '')78 self.assertEqual(result, '')
7079
71 url, method, postdata, headers = request.getPageArgs80 url, method, postdata, headers = request.getPageArgs
72 self.assertEqual(url, request.getURI())81 self.assertEqual(url, request.get_uri())
73 self.assertEqual(method, request.verb)82 self.assertEqual(method, request.verb)
74 self.assertEqual(postdata, request.data)83 self.assertEqual(postdata, request.data)
75 self.assertEqual(headers, request.getHeaders())84 self.assertEqual(headers, request.get_headers())
7685
77 return request.submit().addCallback(_postCheck)86 return request.submit().addCallback(_postCheck)
7887
79 def test_authenticationTestCases(self):88 def test_authenticationTestCases(self):
80 req = S3Request('GET', creds=self.creds)89 request = S3Request('GET', service=self.service)
81 req.date = 'Wed, 28 Mar 2007 01:29:59 +0000'90 request.get_signature = lambda headers: "TESTINGSIG="
91 request.date = 'Wed, 28 Mar 2007 01:29:59 +0000'
8292
83 headers = req.getHeaders()93 headers = request.get_headers()
84 self.assertEqual(94 self.assertEqual(
85 headers['Authorization'],95 headers['Authorization'],
86 'AWS 0PN5J17HBGZHT7JJ3X82:jF7L3z/FTV47vagZzhKupJ9oNig=')96 'AWS fookeyid:TESTINGSIG=')
8797
8898
89class InertRequest(S3Request):99class InertRequest(S3Request):
@@ -110,13 +120,13 @@
110 """120 """
111 Testable version of S3.121 Testable version of S3.
112122
113 This subclass stubs requestFactory to use InertRequest, making it easy to123 This subclass stubs request_factory to use InertRequest, making it easy to
114 assert things about the requests that are created in response to various124 assert things about the requests that are created in response to various
115 operations.125 operations.
116 """126 """
117 response = None127 response = None
118128
119 def requestFactory(self, *a, **kw):129 def request_factory(self, *a, **kw):
120 req = InertRequest(response=self.response, *a, **kw)130 req = InertRequest(response=self.response, *a, **kw)
121 self._lastRequest = req131 self._lastRequest = req
122 return req132 return req
@@ -148,34 +158,34 @@
148158
149 def setUp(self):159 def setUp(self):
150 TXAWSTestCase.setUp(self)160 TXAWSTestCase.setUp(self)
151 self.creds = AWSCredentials(161 self.service = S3Service(
152 access_key='accessKey', secret_key='secretKey')162 access_key='accessKey', secret_key='secretKey')
153 self.s3 = TestableS3(creds=self.creds)163 self.s3 = TestableS3(service=self.service)
154164
155 def test_makeRequest(self):165 def test_make_request(self):
156 """166 """
157 Test that makeRequest passes in the service credentials.167 Test that make_request passes in the service object.
158 """168 """
159 marker = object()169 marker = object()
160170
161 def _cb(*a, **kw):171 def _cb(*a, **kw):
162 self.assertEqual(kw['creds'], self.creds)172 self.assertEqual(kw['service'], self.service)
163 return marker173 return marker
164174
165 self.s3.requestFactory = _cb175 self.s3.request_factory = _cb
166 self.assertIdentical(self.s3.makeRequest('GET'), marker)176 self.assertIdentical(self.s3.make_request('GET'), marker)
167177
168 def test_listBuckets(self):178 def test_list_buckets(self):
169 self.s3.response = samples['ListAllMyBucketsResult']179 self.s3.response = samples['ListAllMyBucketsResult']
170 d = self.s3.listBuckets()180 d = self.s3.list_buckets()
171181
172 req = self.s3._lastRequest182 req = self.s3._lastRequest
173 self.assertTrue(req.submitted)183 self.assertTrue(req.submitted)
174 self.assertEqual(req.verb, 'GET')184 self.assertEqual(req.verb, 'GET')
175 self.assertEqual(req.bucket, None)185 self.assertEqual(req.bucket, None)
176 self.assertEqual(req.objectName, None)186 self.assertEqual(req.object_name, None)
177187
178 def _checkResult(buckets):188 def _check_result(buckets):
179 self.assertEqual(189 self.assertEqual(
180 list(buckets),190 list(buckets),
181 [{'name': u'quotes',191 [{'name': u'quotes',
@@ -184,61 +194,61 @@
184 {'name': u'samples',194 {'name': u'samples',
185 'created': Time.fromDatetime(195 'created': Time.fromDatetime(
186 datetime(2006, 2, 3, 16, 41, 58))}])196 datetime(2006, 2, 3, 16, 41, 58))}])
187 return d.addCallback(_checkResult)197 return d.addCallback(_check_result)
188198
189 def test_createBucket(self):199 def test_create_bucket(self):
190 self.s3.createBucket('foo')200 self.s3.create_bucket('foo')
191 req = self.s3._lastRequest201 req = self.s3._lastRequest
192 self.assertTrue(req.submitted)202 self.assertTrue(req.submitted)
193 self.assertEqual(req.verb, 'PUT')203 self.assertEqual(req.verb, 'PUT')
194 self.assertEqual(req.bucket, 'foo')204 self.assertEqual(req.bucket, 'foo')
195 self.assertEqual(req.objectName, None)205 self.assertEqual(req.object_name, None)
196206
197 def test_deleteBucket(self):207 def test_delete_bucket(self):
198 self.s3.deleteBucket('foo')208 self.s3.delete_bucket('foo')
199 req = self.s3._lastRequest209 req = self.s3._lastRequest
200 self.assertTrue(req.submitted)210 self.assertTrue(req.submitted)
201 self.assertEqual(req.verb, 'DELETE')211 self.assertEqual(req.verb, 'DELETE')
202 self.assertEqual(req.bucket, 'foo')212 self.assertEqual(req.bucket, 'foo')
203 self.assertEqual(req.objectName, None)213 self.assertEqual(req.object_name, None)
204214
205 def test_putObject(self):215 def test_put_object(self):
206 self.s3.putObject(216 self.s3.put_object(
207 'foobucket', 'foo', 'data', 'text/plain', {'foo': 'bar'})217 'foobucket', 'foo', 'data', 'text/plain', {'foo': 'bar'})
208 req = self.s3._lastRequest218 req = self.s3._lastRequest
209 self.assertTrue(req.submitted)219 self.assertTrue(req.submitted)
210 self.assertEqual(req.verb, 'PUT')220 self.assertEqual(req.verb, 'PUT')
211 self.assertEqual(req.bucket, 'foobucket')221 self.assertEqual(req.bucket, 'foobucket')
212 self.assertEqual(req.objectName, 'foo')222 self.assertEqual(req.object_name, 'foo')
213 self.assertEqual(req.data, 'data')223 self.assertEqual(req.data, 'data')
214 self.assertEqual(req.contentType, 'text/plain')224 self.assertEqual(req.content_type, 'text/plain')
215 self.assertEqual(req.metadata, {'foo': 'bar'})225 self.assertEqual(req.metadata, {'foo': 'bar'})
216226
217 def test_getObject(self):227 def test_get_object(self):
218 self.s3.getObject('foobucket', 'foo')228 self.s3.get_object('foobucket', 'foo')
219 req = self.s3._lastRequest229 req = self.s3._lastRequest
220 self.assertTrue(req.submitted)230 self.assertTrue(req.submitted)
221 self.assertEqual(req.verb, 'GET')231 self.assertEqual(req.verb, 'GET')
222 self.assertEqual(req.bucket, 'foobucket')232 self.assertEqual(req.bucket, 'foobucket')
223 self.assertEqual(req.objectName, 'foo')233 self.assertEqual(req.object_name, 'foo')
224234
225 def test_headObject(self):235 def test_head_object(self):
226 self.s3.headObject('foobucket', 'foo')236 self.s3.head_object('foobucket', 'foo')
227 req = self.s3._lastRequest237 req = self.s3._lastRequest
228 self.assertTrue(req.submitted)238 self.assertTrue(req.submitted)
229 self.assertEqual(req.verb, 'HEAD')239 self.assertEqual(req.verb, 'HEAD')
230 self.assertEqual(req.bucket, 'foobucket')240 self.assertEqual(req.bucket, 'foobucket')
231 self.assertEqual(req.objectName, 'foo')241 self.assertEqual(req.object_name, 'foo')
232242
233 def test_deleteObject(self):243 def test_delete_object(self):
234 self.s3.deleteObject('foobucket', 'foo')244 self.s3.delete_object('foobucket', 'foo')
235 req = self.s3._lastRequest245 req = self.s3._lastRequest
236 self.assertTrue(req.submitted)246 self.assertTrue(req.submitted)
237 self.assertEqual(req.verb, 'DELETE')247 self.assertEqual(req.verb, 'DELETE')
238 self.assertEqual(req.bucket, 'foobucket')248 self.assertEqual(req.bucket, 'foobucket')
239 self.assertEqual(req.objectName, 'foo')249 self.assertEqual(req.object_name, 'foo')
240250
241251
242class MiscellaneousTests(TXAWSTestCase):252class MiscellaneousTests(TXAWSTestCase):
243 def test_contentMD5(self):253 def test_contentMD5(self):
244 self.assertEqual(calculateMD5('somedata'), 'rvr3UC1SmUw7AZV2NqPN0g==')254 self.assertEqual(calculate_md5('somedata'), 'rvr3UC1SmUw7AZV2NqPN0g==')
245255
=== removed file 'txaws/tests/test_credentials.py'
--- txaws/tests/test_credentials.py 2009-08-17 11:18:56 +0000
+++ txaws/tests/test_credentials.py 1970-01-01 00:00:00 +0000
@@ -1,41 +0,0 @@
1# Copyright (C) 2009 Robert Collins <robertc@robertcollins.net>
2# Licenced under the txaws licence available at /LICENSE in the txaws source.
3
4import os
5
6from twisted.trial.unittest import TestCase
7from txaws.tests import TXAWSTestCase
8
9from txaws.credentials import AWSCredentials
10
11
12class TestCredentials(TXAWSTestCase):
13
14 def test_no_access_errors(self):
15 # Without anything in os.environ, AWSCredentials() blows up
16 os.environ['AWS_SECRET_ACCESS_KEY'] = 'foo'
17 self.assertRaises(Exception, AWSCredentials)
18
19 def test_no_secret_errors(self):
20 # Without anything in os.environ, AWSCredentials() blows up
21 os.environ['AWS_ACCESS_KEY_ID'] = 'bar'
22 self.assertRaises(Exception, AWSCredentials)
23
24 def test_found_values_used(self):
25 os.environ['AWS_SECRET_ACCESS_KEY'] = 'foo'
26 os.environ['AWS_ACCESS_KEY_ID'] = 'bar'
27 creds = AWSCredentials()
28 self.assertEqual('foo', creds.secret_key)
29 self.assertEqual('bar', creds.access_key)
30
31 def test_explicit_access_key(self):
32 os.environ['AWS_SECRET_ACCESS_KEY'] = 'foo'
33 creds = AWSCredentials(access_key='bar')
34 self.assertEqual('foo', creds.secret_key)
35 self.assertEqual('bar', creds.access_key)
36
37 def test_explicit_secret_key(self):
38 os.environ['AWS_ACCESS_KEY_ID'] = 'bar'
39 creds = AWSCredentials(secret_key='foo')
40 self.assertEqual('foo', creds.secret_key)
41 self.assertEqual('bar', creds.access_key)
420
=== added file 'txaws/tests/test_service.py'
--- txaws/tests/test_service.py 1970-01-01 00:00:00 +0000
+++ txaws/tests/test_service.py 2009-08-20 19:09:56 +0000
@@ -0,0 +1,88 @@
1# Copyright (C) 2009 Duncan McGreggor <duncan@canonical.com>
2# Licenced under the txaws licence available at /LICENSE in the txaws source.
3
4import os
5
6from txaws.service import AWSService, ENV_ACCESS_KEY, ENV_SECRET_KEY
7from txaws.tests import TXAWSTestCase
8
9
10class AWSServiceTestCase(TXAWSTestCase):
11
12 def setUp(self):
13 self.service = AWSService("fookeyid", "barsecretkey",
14 "http://my.service/da_endpoint")
15 self.addCleanup(self.clean_environment)
16
17 def clean_environment(self):
18 if os.environ.has_key(ENV_ACCESS_KEY):
19 del os.environ[ENV_ACCESS_KEY]
20 if os.environ.has_key(ENV_SECRET_KEY):
21 del os.environ[ENV_SECRET_KEY]
22
23 def test_simple_creation(self):
24 service = AWSService("fookeyid", "barsecretkey")
25 self.assertEquals(service.access_key, "fookeyid")
26 self.assertEquals(service.secret_key, "barsecretkey")
27 self.assertEquals(service.schema, "https")
28 self.assertEquals(service.host, "")
29 self.assertEquals(service.port, 80)
30 self.assertEquals(service.endpoint, "/")
31 self.assertEquals(service.method, "GET")
32
33 def test_no_access_errors(self):
34 # Without anything in os.environ, AWSService() blows up
35 os.environ[ENV_SECRET_KEY] = "bar"
36 self.assertRaises(ValueError, AWSService)
37
38 def test_no_secret_errors(self):
39 # Without anything in os.environ, AWSService() blows up
40 os.environ[ENV_ACCESS_KEY] = "foo"
41 self.assertRaises(ValueError, AWSService)
42
43 def test_found_values_used(self):
44 os.environ[ENV_ACCESS_KEY] = "foo"
45 os.environ[ENV_SECRET_KEY] = "bar"
46 service = AWSService()
47 self.assertEqual("foo", service.access_key)
48 self.assertEqual("bar", service.secret_key)
49 self.clean_environment()
50
51 def test_explicit_access_key(self):
52 os.environ[ENV_SECRET_KEY] = "foo"
53 service = AWSService(access_key="bar")
54 self.assertEqual("foo", service.secret_key)
55 self.assertEqual("bar", service.access_key)
56
57 def test_explicit_secret_key(self):
58 os.environ[ENV_ACCESS_KEY] = "bar"
59 service = AWSService(secret_key="foo")
60 self.assertEqual("foo", service.secret_key)
61 self.assertEqual("bar", service.access_key)
62
63 def test_parse_uri(self):
64 self.assertEquals(self.service.schema, "http")
65 self.assertEquals(self.service.host, "my.service")
66 self.assertEquals(self.service.port, 80)
67 self.assertEquals(self.service.endpoint, "/da_endpoint")
68
69 def test_parse_uri_https_and_custom_port(self):
70 service = AWSService("foo", "bar", "https://my.service:8080/endpoint")
71 self.assertEquals(service.schema, "https")
72 self.assertEquals(service.host, "my.service")
73 self.assertEquals(service.port, 8080)
74 self.assertEquals(service.endpoint, "/endpoint")
75
76 def test_custom_method(self):
77 service = AWSService("foo", "bar", "http://service/endpoint", "PUT")
78 self.assertEquals(service.method, "PUT")
79
80 def test_get_uri(self):
81 uri = self.service.get_uri()
82 self.assertEquals(uri, "http://my.service/da_endpoint")
83
84 def test_get_uri_custom_port(self):
85 uri = "https://my.service:8080/endpoint"
86 service = AWSService("foo", "bar", uri)
87 new_uri = service.get_uri()
88 self.assertEquals(new_uri, uri)
089
=== modified file 'txaws/util.py'
--- txaws/util.py 2009-08-17 11:18:56 +0000
+++ txaws/util.py 2009-08-18 20:48:59 +0000
@@ -4,10 +4,10 @@
4services.4services.
5"""5"""
66
7import time
8import hmac
9from hashlib import sha1, md5
7from base64 import b64encode10from base64 import b64encode
8from hashlib import sha1
9import hmac
10import time
1111
12# Import XML from somwhere; here in one place to prevent duplication.12# Import XML from somwhere; here in one place to prevent duplication.
13try:13try:
@@ -19,6 +19,11 @@
19__all__ = ['hmac_sha1', 'iso8601time']19__all__ = ['hmac_sha1', 'iso8601time']
2020
2121
22def calculate_md5(data):
23 digest = md5(data).digest()
24 return b64encode(digest)
25
26
22def hmac_sha1(secret, data):27def hmac_sha1(secret, data):
23 digest = hmac.new(secret, data, sha1).digest()28 digest = hmac.new(secret, data, sha1).digest()
24 return b64encode(digest)29 return b64encode(digest)

Subscribers

People subscribed via source and target branches