Merge lp:~oubiwann/txaws/432540-more-securitygroup-methods into lp:txaws

Proposed by Duncan McGreggor
Status: Merged
Merged at revision: not available
Proposed branch: lp:~oubiwann/txaws/432540-more-securitygroup-methods
Merge into: lp:txaws
Diff against target: 1309 lines
4 files modified
txaws/ec2/client.py (+209/-30)
txaws/ec2/tests/test_client.py (+601/-261)
txaws/exception.py (+1/-0)
txaws/testing/payload.py (+42/-0)
To merge this branch: bzr merge lp:~oubiwann/txaws/432540-more-securitygroup-methods
Reviewer Review Type Date Requested Status
Thomas Herve Approve
Review via email: mp+12118@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Duncan McGreggor (oubiwann) wrote :

This one's ready for review. This branch finishes the security group support for txAWS.

Note that I've added 4 convenience functions, two each for revoking and authorizing groups. This is to provide a slightly more intuitive API than what is offered by AWS EC2. The AWS EC2 methods have also been implemented, so nothing's missing. These are just more clear to use, since they split the EC2 methods into distinct areas of functionality (both the authorize and revoke AWS EC2 methods serve dual purpose: they modify a group with another group that has IP permissions or they modify a group with IP permissions).

31. By Duncan McGreggor

Added missing newline.

Revision history for this message
Thomas Herve (therve) wrote :

[1]
+ if success and success.lower() == "true":
+ return True
+ return False

I'm not sure you need to check for the presence of success. At any rate, you could do that:

return (success is not None and success.lower() == "true")

But I would do that:

return root.findtext("return") == "true"

It would handle less cases, but it would like to see it fails when EC2 is not respecting the format of the response (we don't have to be defensive here).

[2]
+ @return: A C{Deferred} that will fire with a turth value for the

typo: truth (in 2 places).

[3] Nice factoring with _parse_truth_return. Can you use it in the other places where similar thing is done?

+1!

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

> [1]
> + if success and success.lower() == "true":
> + return True
> + return False
>
> I'm not sure you need to check for the presence of success. At any rate, you
> could do that:
>
> return (success is not None and success.lower() == "true")
>
> But I would do that:
>
> return root.findtext("return") == "true"
>
> It would handle less cases, but it would like to see it fails when EC2 is not
> respecting the format of the response (we don't have to be defensive here).

Cool. Done.

> [2]
> + @return: A C{Deferred} that will fire with a turth value for the
>
> typo: truth (in 2 places).

Yikes. There were a bunch more, too. Hopefully I got them all ;-)

> [3] Nice factoring with _parse_truth_return. Can you use it in the other
> places where similar thing is done?

Thanks! Done.

> +1!

Thanks!

d

32. By Duncan McGreggor

- Changed other functions that had custom parsing for truth returns to use the
  newly refactored method (therve 1).
- Removed debug print from unit test.

33. By Duncan McGreggor

Fixed typos (therve 2).

34. By Duncan McGreggor

Fixed a bunch more typos.

35. By Duncan McGreggor

Merged from trunk.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'txaws/ec2/client.py'
2--- txaws/ec2/client.py 2009-09-18 14:51:21 +0000
3+++ txaws/ec2/client.py 2009-09-25 05:33:10 +0000
4@@ -57,9 +57,9 @@
5 """Describe current instances."""
6 q = self.query_factory("DescribeInstances", self.creds, self.endpoint)
7 d = q.submit()
8- return d.addCallback(self._parse_instances)
9+ return d.addCallback(self._parse_describe_instances)
10
11- def _parse_instances(self, xml_bytes):
12+ def _parse_describe_instances(self, xml_bytes):
13 """
14 Parse the reservations XML payload that is returned from an AWS
15 describeInstances API call.
16@@ -165,9 +165,9 @@
17 query = self.query_factory("DescribeSecurityGroups", self.creds,
18 self.endpoint, group_names)
19 d = query.submit()
20- return d.addCallback(self._parse_security_groups)
21+ return d.addCallback(self._parse_describe_security_groups)
22
23- def _parse_security_groups(self, xml_bytes):
24+ def _parse_describe_security_groups(self, xml_bytes):
25 """Parse the XML returned by the C{DescribeSecurityGroups} function.
26
27 @param xml_bytes: XML bytes with a C{DescribeSecurityGroupsResponse}
28@@ -207,6 +207,205 @@
29 result.append(security_group)
30 return result
31
32+ def create_security_group(self, name, description):
33+ """Create security group.
34+
35+ @param name: Name of the new security group.
36+ @param description: Description of the new security group.
37+ @return: A C{Deferred} that will fire with a truth value for the
38+ success of the operation.
39+ """
40+ group_names = None
41+ parameters = {"GroupName": name, "GroupDescription": description}
42+ query = self.query_factory("CreateSecurityGroup", self.creds,
43+ self.endpoint, parameters)
44+ d = query.submit()
45+ return d.addCallback(self._parse_truth_return)
46+
47+ def _parse_truth_return(self, xml_bytes):
48+ root = XML(xml_bytes)
49+ return root.findtext("return") == "true"
50+
51+ def delete_security_group(self, name):
52+ """
53+ @param name: Name of the new security group.
54+ @return: A C{Deferred} that will fire with a truth value for the
55+ success of the operation.
56+ """
57+ parameter = {"GroupName": name}
58+ query = self.query_factory("DeleteSecurityGroup", self.creds,
59+ self.endpoint, parameter)
60+ d = query.submit()
61+ return d.addCallback(self._parse_truth_return)
62+
63+ def authorize_security_group(
64+ self, group_name, source_group_name="", source_group_owner_id="",
65+ ip_protocol="", from_port="", to_port="", cidr_ip=""):
66+ """
67+ There are two ways to use C{authorize_security_group}:
68+ 1) associate an existing group (source group) with the one that you
69+ are targeting (group_name) with an authorization update; or
70+ 2) associate a set of IP permissions with the group you are
71+ targeting with an authorization update.
72+
73+ @param group_name: The group you will be modifying with a new
74+ authorization.
75+
76+ Optionally, the following parameters:
77+ @param source_group_name: Name of security group to authorize access to
78+ when operating on a user/group pair.
79+ @param source_group_owner_id: Owner of security group to authorize
80+ access to when operating on a user/group pair.
81+
82+ If those parameters are not specified, then the following must be:
83+ @param ip_protocol: IP protocol to authorize access to when operating
84+ on a CIDR IP.
85+ @param from_port: Bottom of port range to authorize access to when
86+ operating on a CIDR IP. This contains the ICMP type if ICMP is
87+ being authorized.
88+ @param to_port: Top of port range to authorize access to when operating
89+ on a CIDR IP. This contains the ICMP code if ICMP is being
90+ authorized.
91+ @param cidr_ip: CIDR IP range to authorize access to when operating on
92+ a CIDR IP.
93+
94+ @return: A C{Deferred} that will fire with a truth value for the
95+ success of the operation.
96+ """
97+ if source_group_name and source_group_owner_id:
98+ parameters = {
99+ "SourceSecurityGroupName": source_group_name,
100+ "SourceSecurityGroupOwnerId": source_group_owner_id,
101+ }
102+ elif ip_protocol and from_port and to_port and cidr_ip:
103+ parameters = {
104+ "IpProtocol": ip_protocol,
105+ "FromPort": from_port,
106+ "ToPort": to_port,
107+ "CidrIp": cidr_ip,
108+ }
109+ else:
110+ msg = ("You must specify either both group parameters or "
111+ "all the ip parameters.")
112+ raise ValueError(msg)
113+ parameters["GroupName"] = group_name
114+ query = self.query_factory("AuthorizeSecurityGroupIngress", self.creds,
115+ self.endpoint, parameters)
116+ d = query.submit()
117+ return d.addCallback(self._parse_truth_return)
118+
119+ def authorize_group_permission(
120+ self, group_name, source_group_name, source_group_owner_id):
121+ """
122+ This is a convenience function that wraps the "authorize group"
123+ functionality of the C{authorize_security_group} method.
124+
125+ For an explanation of the parameters, see C{authorize_security_group}.
126+ """
127+ d = self.authorize_security_group(
128+ group_name,
129+ source_group_name=source_group_name,
130+ source_group_owner_id=source_group_owner_id)
131+ return d
132+
133+ def authorize_ip_permission(
134+ self, group_name, ip_protocol, from_port, to_port, cidr_ip):
135+ """
136+ This is a convenience function that wraps the "authorize ip
137+ permission" functionality of the C{authorize_security_group} method.
138+
139+ For an explanation of the parameters, see C{authorize_security_group}.
140+ """
141+ d = self.authorize_security_group(
142+ group_name,
143+ ip_protocol=ip_protocol, from_port=from_port, to_port=to_port,
144+ cidr_ip=cidr_ip)
145+ return d
146+
147+ def revoke_security_group(
148+ self, group_name, source_group_name="", source_group_owner_id="",
149+ ip_protocol="", from_port="", to_port="", cidr_ip=""):
150+ """
151+ There are two ways to use C{revoke_security_group}:
152+ 1) associate an existing group (source group) with the one that you
153+ are targeting (group_name) with the revoke update; or
154+ 2) associate a set of IP permissions with the group you are
155+ targeting with a revoke update.
156+
157+ @param group_name: The group you will be modifying with an
158+ authorization removal.
159+
160+ Optionally, the following parameters:
161+ @param source_group_name: Name of security group to revoke access from
162+ when operating on a user/group pair.
163+ @param source_group_owner_id: Owner of security group to revoke
164+ access from when operating on a user/group pair.
165+
166+ If those parameters are not specified, then the following must be:
167+ @param ip_protocol: IP protocol to revoke access from when operating
168+ on a CIDR IP.
169+ @param from_port: Bottom of port range to revoke access from when
170+ operating on a CIDR IP. This contains the ICMP type if ICMP is
171+ being revoked.
172+ @param to_port: Top of port range to revoke access from when operating
173+ on a CIDR IP. This contains the ICMP code if ICMP is being
174+ revoked.
175+ @param cidr_ip: CIDR IP range to revoke access from when operating on
176+ a CIDR IP.
177+
178+ @return: A C{Deferred} that will fire with a truth value for the
179+ success of the operation.
180+ """
181+ if source_group_name and source_group_owner_id:
182+ parameters = {
183+ "SourceSecurityGroupName": source_group_name,
184+ "SourceSecurityGroupOwnerId": source_group_owner_id,
185+ }
186+ elif ip_protocol and from_port and to_port and cidr_ip:
187+ parameters = {
188+ "IpProtocol": ip_protocol,
189+ "FromPort": from_port,
190+ "ToPort": to_port,
191+ "CidrIp": cidr_ip,
192+ }
193+ else:
194+ msg = ("You must specify either both group parameters or "
195+ "all the ip parameters.")
196+ raise ValueError(msg)
197+ parameters["GroupName"] = group_name
198+ query = self.query_factory("RevokeSecurityGroupIngress", self.creds,
199+ self.endpoint, parameters)
200+ d = query.submit()
201+ return d.addCallback(self._parse_truth_return)
202+
203+ def revoke_group_permission(
204+ self, group_name, source_group_name, source_group_owner_id):
205+ """
206+ This is a convenience function that wraps the "authorize group"
207+ functionality of the C{authorize_security_group} method.
208+
209+ For an explanation of the parameters, see C{authorize_security_group}.
210+ """
211+ d = self.revoke_security_group(
212+ group_name,
213+ source_group_name=source_group_name,
214+ source_group_owner_id=source_group_owner_id)
215+ return d
216+
217+ def revoke_ip_permission(
218+ self, group_name, ip_protocol, from_port, to_port, cidr_ip):
219+ """
220+ This is a convenience function that wraps the "authorize ip
221+ permission" functionality of the C{authorize_security_group} method.
222+
223+ For an explanation of the parameters, see C{authorize_security_group}.
224+ """
225+ d = self.revoke_security_group(
226+ group_name,
227+ ip_protocol=ip_protocol, from_port=from_port, to_port=to_port,
228+ cidr_ip=cidr_ip)
229+ return d
230+
231 def describe_volumes(self, *volume_ids):
232 """Describe available volumes."""
233 volumeset = {}
234@@ -215,9 +414,9 @@
235 q = self.query_factory(
236 "DescribeVolumes", self.creds, self.endpoint, volumeset)
237 d = q.submit()
238- return d.addCallback(self._parse_volumes)
239+ return d.addCallback(self._parse_describe_volumes)
240
241- def _parse_volumes(self, xml_bytes):
242+ def _parse_describe_volumes(self, xml_bytes):
243 root = XML(xml_bytes)
244 result = []
245 for volume_data in root.find("volumeSet"):
246@@ -274,11 +473,7 @@
247 q = self.query_factory(
248 "DeleteVolume", self.creds, self.endpoint, {"VolumeId": volume_id})
249 d = q.submit()
250- return d.addCallback(self._parse_delete_volume)
251-
252- def _parse_delete_volume(self, xml_bytes):
253- root = XML(xml_bytes)
254- return root.findtext("return") == "true"
255+ return d.addCallback(self._parse_truth_return)
256
257 def describe_snapshots(self, *snapshot_ids):
258 """Describe available snapshots."""
259@@ -334,11 +529,7 @@
260 "DeleteSnapshot", self.creds, self.endpoint,
261 {"SnapshotId": snapshot_id})
262 d = q.submit()
263- return d.addCallback(self._parse_delete_snapshot)
264-
265- def _parse_delete_snapshot(self, xml_bytes):
266- root = XML(xml_bytes)
267- return root.findtext("return") == "true"
268+ return d.addCallback(self._parse_truth_return)
269
270 def attach_volume(self, volume_id, instance_id, device):
271 """Attach the given volume to the specified instance at C{device}."""
272@@ -401,19 +592,7 @@
273 "DeleteKeyPair", self.creds, self.endpoint,
274 {"KeyName": keypair_name})
275 d = q.submit()
276- return d.addCallback(self._parse_delete_keypair)
277-
278- def _parse_delete_keypair(self, xml_bytes):
279- results = []
280- keypair_data = XML(xml_bytes)
281- result = keypair_data.findtext("return")
282- if not result:
283- result = False
284- elif result.lower() == "true":
285- result = True
286- else:
287- result = False
288- return result
289+ return d.addCallback(self._parse_truth_return)
290
291
292 class Query(object):
293@@ -471,7 +650,7 @@
294 self.params["Signature"] = self.creds.sign(self.signing_text())
295
296 def sorted_params(self):
297- """Return the query params sorted appropriately for signing."""
298+ """Return the query parameters sorted appropriately for signing."""
299 return sorted(self.params.items())
300
301 def get_page(self, url, *args, **kwds):
302
303=== modified file 'txaws/ec2/tests/test_client.py'
304--- txaws/ec2/tests/test_client.py 2009-09-15 21:12:04 +0000
305+++ txaws/ec2/tests/test_client.py 2009-09-25 05:33:10 +0000
306@@ -7,7 +7,7 @@
307 import os
308
309 from twisted.internet import reactor
310-from twisted.internet.defer import succeed
311+from twisted.internet.defer import succeed, fail
312 from twisted.python.failure import Failure
313 from twisted.python.filepath import FilePath
314 from twisted.web import server, static, util
315@@ -71,6 +71,9 @@
316 ec2 = client.EC2Client(creds=creds)
317 self.assertEqual(creds, ec2.creds)
318
319+
320+class EC2ClientInstancesTestCase(TXAWSTestCase):
321+
322 def check_parsed_instances(self, results):
323 instance = results[0]
324 # check reservations
325@@ -130,7 +133,7 @@
326 def test_parse_reservation(self):
327 creds = AWSCredentials("foo", "bar")
328 ec2 = client.EC2Client(creds=creds)
329- results = ec2._parse_instances(
330+ results = ec2._parse_describe_instances(
331 payload.sample_describe_instances_result)
332 self.check_parsed_instances(results)
333
334@@ -185,6 +188,9 @@
335 d.addCallback(check_transition)
336 return d
337
338+
339+class EC2ClientSecurityGroupsTestCase(TXAWSTestCase):
340+
341 def test_describe_security_groups(self):
342 """
343 L{EC2Client.describe_security_groups} returns a C{Deferred} that
344@@ -200,7 +206,7 @@
345 def submit(self):
346 return succeed(payload.sample_describe_security_groups_result)
347
348- def assert_security_groups(security_groups):
349+ def check_results(security_groups):
350 [security_group] = security_groups
351 self.assertEquals(security_group.owner_id,
352 "UYY3TLBUXIEON5NQVUUX6OMPWBZIQNFM")
353@@ -214,9 +220,8 @@
354
355 creds = AWSCredentials("foo", "bar")
356 ec2 = client.EC2Client(creds, query_factory=StubQuery)
357- security_groups = ec2.describe_security_groups()
358- security_groups.addCallback(assert_security_groups)
359- return security_groups
360+ d = ec2.describe_security_groups()
361+ return d.addCallback(check_results)
362
363 def test_describe_security_groups_with_multiple_results(self):
364 """
365@@ -234,7 +239,7 @@
366 return succeed(
367 payload.sample_describe_security_groups_multiple_result)
368
369- def assert_security_groups(security_groups):
370+ def check_results(security_groups):
371 self.assertEquals(len(security_groups), 2)
372
373 security_group = security_groups[0]
374@@ -263,9 +268,8 @@
375
376 creds = AWSCredentials("foo", "bar")
377 ec2 = client.EC2Client(creds, query_factory=StubQuery)
378- security_groups = ec2.describe_security_groups()
379- security_groups.addCallback(assert_security_groups)
380- return security_groups
381+ d = ec2.describe_security_groups()
382+ return d.addCallback(check_results)
383
384 def test_describe_security_groups_with_name(self):
385 """
386@@ -281,267 +285,354 @@
387 def submit(self):
388 return succeed(payload.sample_describe_security_groups_result)
389
390- def assert_security_groups(security_groups):
391+ def check_result(security_groups):
392 [security_group] = security_groups
393 self.assertEquals(security_group.name, "WebServers")
394
395 creds = AWSCredentials("foo", "bar")
396 ec2 = client.EC2Client(creds, query_factory=StubQuery)
397- security_groups = ec2.describe_security_groups("WebServers")
398- security_groups.addCallback(assert_security_groups)
399- return security_groups
400-
401-
402-class QueryTestCase(TXAWSTestCase):
403-
404- def setUp(self):
405- TXAWSTestCase.setUp(self)
406- self.creds = AWSCredentials("foo", "bar")
407- self.endpoint = AWSServiceEndpoint(uri=EC2_ENDPOINT_US)
408-
409- def test_init_minimum(self):
410- query = client.Query("DescribeInstances", self.creds, self.endpoint)
411- self.assertTrue("Timestamp" in query.params)
412- del query.params["Timestamp"]
413- self.assertEqual(
414- {"AWSAccessKeyId": "foo",
415- "Action": "DescribeInstances",
416- "SignatureMethod": "HmacSHA1",
417- "SignatureVersion": "2",
418- "Version": "2008-12-01"},
419- query.params)
420-
421- def test_init_requires_action(self):
422- self.assertRaises(TypeError, client.Query)
423-
424- def test_init_requires_creds(self):
425- self.assertRaises(TypeError, client.Query, None)
426-
427- def test_init_other_args_are_params(self):
428- query = client.Query("DescribeInstances", self.creds, self.endpoint,
429- {"InstanceId.0": "12345"},
430- time_tuple=(2007,11,12,13,14,15,0,0,0))
431- self.assertEqual(
432- {"AWSAccessKeyId": "foo",
433- "Action": "DescribeInstances",
434- "InstanceId.0": "12345",
435- "SignatureMethod": "HmacSHA1",
436- "SignatureVersion": "2",
437- "Timestamp": "2007-11-12T13:14:15Z",
438- "Version": "2008-12-01"},
439- query.params)
440-
441- def test_sorted_params(self):
442- query = client.Query("DescribeInstances", self.creds, self.endpoint,
443- {"fun": "games"},
444- time_tuple=(2007,11,12,13,14,15,0,0,0))
445- self.assertEqual([
446- ("AWSAccessKeyId", "foo"),
447- ("Action", "DescribeInstances"),
448- ("SignatureMethod", "HmacSHA1"),
449- ("SignatureVersion", "2"),
450- ("Timestamp", "2007-11-12T13:14:15Z"),
451- ("Version", "2008-12-01"),
452- ("fun", "games"),
453- ], query.sorted_params())
454-
455- def test_encode_unreserved(self):
456- all_unreserved = ("ABCDEFGHIJKLMNOPQRSTUVWXYZ"
457- "abcdefghijklmnopqrstuvwxyz0123456789-_.~")
458- query = client.Query("DescribeInstances", self.creds, self.endpoint)
459- self.assertEqual(all_unreserved, query.encode(all_unreserved))
460-
461- def test_encode_space(self):
462- """This may be just "url encode", but the AWS manual isn't clear."""
463- query = client.Query("DescribeInstances", self.creds, self.endpoint)
464- self.assertEqual("a%20space", query.encode("a space"))
465-
466- def test_canonical_query(self):
467- query = client.Query("DescribeInstances", self.creds, self.endpoint,
468- {"fu n": "g/ames", "argwithnovalue":"",
469- "InstanceId.1": "i-1234"},
470- time_tuple=(2007,11,12,13,14,15,0,0,0))
471- expected_query = ("AWSAccessKeyId=foo&Action=DescribeInstances"
472- "&InstanceId.1=i-1234"
473- "&SignatureMethod=HmacSHA1&SignatureVersion=2&"
474- "Timestamp=2007-11-12T13%3A14%3A15Z&Version=2008-12-01&"
475- "argwithnovalue=&fu%20n=g%2Fames")
476- self.assertEqual(expected_query, query.canonical_query_params())
477-
478- def test_signing_text(self):
479- query = client.Query("DescribeInstances", self.creds, self.endpoint,
480- time_tuple=(2007,11,12,13,14,15,0,0,0))
481- signing_text = ("GET\n%s\n/\n" % self.endpoint.host +
482- "AWSAccessKeyId=foo&Action=DescribeInstances&"
483- "SignatureMethod=HmacSHA1&SignatureVersion=2&"
484- "Timestamp=2007-11-12T13%3A14%3A15Z&Version=2008-12-01")
485- self.assertEqual(signing_text, query.signing_text())
486-
487- def test_sign(self):
488- query = client.Query("DescribeInstances", self.creds, self.endpoint,
489- time_tuple=(2007,11,12,13,14,15,0,0,0))
490- query.sign()
491- self.assertEqual("JuCpwFA2H4OVF3Ql/lAQs+V6iMc=",
492- query.params["Signature"])
493-
494- def test_submit_400(self):
495- """A 4xx response status from EC2 should raise a txAWS EC2Error."""
496- status = 400
497- self.addCleanup(setattr, client.Query, "get_page",
498- client.Query.get_page)
499- fake_page_getter = FakePageGetter(
500- status, payload.sample_ec2_error_message)
501- client.Query.get_page = fake_page_getter.get_page_with_exception
502+ d = ec2.describe_security_groups("WebServers")
503+ return d.addCallback(check_result)
504+
505+ def test_create_security_group(self):
506+ """
507+ L{EC2Client.create_security_group} returns a C{Deferred} that
508+ eventually fires with a true value, indicating the success of the
509+ operation.
510+ """
511+ class StubQuery(object):
512+ def __init__(stub, action, creds, endpoint, other_params=None):
513+ self.assertEqual(action, "CreateSecurityGroup")
514+ self.assertEqual(creds.access_key, "foo")
515+ self.assertEqual(creds.secret_key, "bar")
516+ self.assertEqual(other_params, {
517+ "GroupName": "WebServers",
518+ "GroupDescription": "The group for the web server farm.",
519+ })
520+ def submit(self):
521+ return succeed(payload.sample_create_security_group)
522+
523+ creds = AWSCredentials("foo", "bar")
524+ ec2 = client.EC2Client(creds, query_factory=StubQuery)
525+ d = ec2.create_security_group(
526+ "WebServers",
527+ "The group for the web server farm.")
528+ return self.assertTrue(d)
529+
530+ def test_delete_security_group(self):
531+ """
532+ L{EC2Client.delete_security_group} returns a C{Deferred} that
533+ eventually fires with a true value, indicating the success of the
534+ operation.
535+ """
536+ class StubQuery(object):
537+ def __init__(stub, action, creds, endpoint, other_params=None):
538+ self.assertEqual(action, "DeleteSecurityGroup")
539+ self.assertEqual(creds.access_key, "foo")
540+ self.assertEqual(creds.secret_key, "bar")
541+ self.assertEqual(other_params, {
542+ "GroupName": "WebServers",
543+ })
544+ def submit(self):
545+ return succeed(payload.sample_delete_security_group)
546+
547+ creds = AWSCredentials("foo", "bar")
548+ ec2 = client.EC2Client(creds, query_factory=StubQuery)
549+ d = ec2.delete_security_group("WebServers")
550+ return self.assertTrue(d)
551+
552+ def test_delete_security_group_failure(self):
553+ """
554+ L{EC2Client.delete_security_group} returns a C{Deferred} that
555+ eventually fires with a failure when EC2 is asked to delete a group
556+ that another group uses in that other group's policy.
557+ """
558+ class StubQuery(object):
559+ def __init__(stub, action, creds, endpoint, other_params=None):
560+ self.assertEqual(action, "DeleteSecurityGroup")
561+ self.assertEqual(creds.access_key, "foo")
562+ self.assertEqual(creds.secret_key, "bar")
563+ self.assertEqual(other_params, {
564+ "GroupName": "GroupReferredTo",
565+ })
566+ def submit(self):
567+ error = EC2Error(payload.sample_delete_security_group_failure)
568+ return fail(error)
569
570 def check_error(error):
571- self.assertTrue(isinstance(error, EC2Error))
572- self.assertEquals(error.get_error_codes(), "Error.Code")
573 self.assertEquals(
574- error.get_error_messages(),
575- "Message for Error.Code")
576- self.assertEquals(error.status, status)
577- self.assertEquals(error.response, payload.sample_ec2_error_message)
578-
579- query = client.Query(
580- 'BadQuery', self.creds, self.endpoint,
581- time_tuple=(2009,8,15,13,14,15,0,0,0))
582+ str(error),
583+ ("Error Message: Group groupID1:GroupReferredTo is used by "
584+ "groups: groupID2:UsingGroup"))
585
586- failure = query.submit()
587+ creds = AWSCredentials("foo", "bar")
588+ ec2 = client.EC2Client(creds, query_factory=StubQuery)
589+ failure = ec2.delete_security_group("GroupReferredTo")
590 d = self.assertFailure(failure, EC2Error)
591- d.addCallback(check_error)
592- return d
593-
594- def test_submit_500(self):
595- """
596- A 5xx response status from EC2 should raise the original Twisted
597- exception.
598- """
599- status = 500
600- self.addCleanup(setattr, client.Query, "get_page",
601- client.Query.get_page)
602- fake_page_getter = FakePageGetter(
603- status, payload.sample_ec2_error_message)
604- client.Query.get_page = fake_page_getter.get_page_with_exception
605-
606- def check_error(error):
607- self.assertFalse(isinstance(error, EC2Error))
608- self.assertEquals(error.status, status)
609- self.assertEquals(str(error), "500 There's been an error")
610-
611- query = client.Query(
612- 'BadQuery', self.creds, self.endpoint,
613- time_tuple=(2009,8,15,13,14,15,0,0,0))
614-
615- failure = query.submit()
616- d = self.assertFailure(failure, Error)
617- d.addCallback(check_error)
618- return d
619-
620-
621-class QueryPageGetterTestCase(TXAWSTestCase):
622-
623- def setUp(self):
624- TXAWSTestCase.setUp(self)
625- self.creds = AWSCredentials("foo", "bar")
626- self.endpoint = AWSServiceEndpoint(uri=EC2_ENDPOINT_US)
627- self.twisted_client_test_setup()
628- self.cleanupServerConnections = 0
629-
630- def tearDown(self):
631- """Copied from twisted.web.test.test_webclient."""
632- # If the test indicated it might leave some server-side connections
633- # around, clean them up.
634- connections = self.wrapper.protocols.keys()
635- # If there are fewer server-side connections than requested,
636- # that's okay. Some might have noticed that the client closed
637- # the connection and cleaned up after themselves.
638- for n in range(min(len(connections), self.cleanupServerConnections)):
639- proto = connections.pop()
640- #msg("Closing %r" % (proto,))
641- proto.transport.loseConnection()
642- if connections:
643- #msg("Some left-over connections; this test is probably buggy.")
644- pass
645- return self.port.stopListening()
646-
647- def _listen(self, site):
648- return reactor.listenTCP(0, site, interface="127.0.0.1")
649-
650- def twisted_client_test_setup(self):
651- name = self.mktemp()
652- os.mkdir(name)
653- FilePath(name).child("file").setContent("0123456789")
654- resource = static.File(name)
655- resource.putChild("redirect", util.Redirect("/file"))
656- self.site = server.Site(resource, timeout=None)
657- self.wrapper = WrappingFactory(self.site)
658- self.port = self._listen(self.wrapper)
659- self.portno = self.port.getHost().port
660-
661- def get_url(self, path):
662- return "http://127.0.0.1:%d/%s" % (self.portno, path)
663-
664- def test_get_page(self):
665- """Copied from twisted.web.test.test_webclient."""
666- query = client.Query(
667- 'DummyQuery', self.creds, self.endpoint,
668- time_tuple=(2009,8,17,13,14,15,0,0,0))
669- deferred = query.get_page(self.get_url("file"))
670- deferred.addCallback(self.assertEquals, "0123456789")
671- return deferred
672-
673-
674-class EC2ErrorWrapperTestCase(TXAWSTestCase):
675-
676- def setUp(self):
677- TXAWSTestCase.setUp(self)
678-
679- def get_failure(self, status=None, type=None, message=""):
680- failure = Failure(type(message))
681- failure.value.response = payload.sample_ec2_error_message
682- failure.value.status = status
683- return failure
684-
685- def test_302_error(self):
686- failure = self.get_failure(302, Exception, "found")
687- error = self.assertRaises(Exception, client.ec2_error_wrapper, failure)
688- self.assertEquals(failure.type, type(error))
689- self.assertFalse(isinstance(error, EC2Error))
690- self.assertTrue(isinstance(error, Exception))
691- self.assertEquals(error.message, "found")
692-
693- def test_400_error(self):
694- failure = self.get_failure(400, Exception)
695- error = self.assertRaises(EC2Error, client.ec2_error_wrapper, failure)
696- self.assertNotEquals(failure.type, type(error))
697- self.assertTrue(isinstance(error, EC2Error))
698- self.assertEquals(error.get_error_codes(), "Error.Code")
699- self.assertEquals(error.get_error_messages(), "Message for Error.Code")
700-
701- def test_404_error(self):
702- failure = self.get_failure(404, Exception)
703- error = self.assertRaises(EC2Error, client.ec2_error_wrapper, failure)
704- self.assertNotEquals(failure.type, type(error))
705- self.assertTrue(isinstance(error, EC2Error))
706- self.assertEquals(error.get_error_codes(), "Error.Code")
707- self.assertEquals(error.get_error_messages(), "Message for Error.Code")
708-
709- def test_500_error(self):
710- failure = self.get_failure(500, Exception, "A server error occurred")
711- error = self.assertRaises(Exception, client.ec2_error_wrapper, failure)
712- self.assertFalse(isinstance(error, EC2Error))
713- self.assertTrue(isinstance(error, Exception))
714- self.assertEquals(error.message, "A server error occurred")
715-
716- def test_timeout_error(self):
717- failure = self.get_failure(type=Exception, message="timeout")
718- error = self.assertRaises(Exception, client.ec2_error_wrapper, failure)
719- self.assertFalse(isinstance(error, EC2Error))
720- self.assertTrue(isinstance(error, Exception))
721- self.assertEquals(error.message, "timeout")
722-
723-
724-class EBSTestCase(TXAWSTestCase):
725+ return d.addCallback(check_error)
726+
727+ def test_authorize_security_group_with_user_group_pair(self):
728+ """
729+ L{EC2Client.authorize_security_group} returns a C{Deferred} that
730+ eventually fires with a true value, indicating the success of the
731+ operation. There are two ways to use the method: set another group's
732+ IP permissions or set new IP permissions; this test checks the first
733+ way.
734+ """
735+ class StubQuery(object):
736+ def __init__(stub, action, creds, endpoint, other_params=None):
737+ self.assertEqual(action, "AuthorizeSecurityGroupIngress")
738+ self.assertEqual(creds.access_key, "foo")
739+ self.assertEqual(creds.secret_key, "bar")
740+ self.assertEqual(other_params, {
741+ "GroupName": "WebServers",
742+ "SourceSecurityGroupName": "AppServers",
743+ "SourceSecurityGroupOwnerId": "123456789123",
744+ })
745+ def submit(self):
746+ return succeed(payload.sample_authorize_security_group)
747+
748+ creds = AWSCredentials("foo", "bar")
749+ ec2 = client.EC2Client(creds, query_factory=StubQuery)
750+ d = ec2.authorize_security_group(
751+ "WebServers", source_group_name="AppServers",
752+ source_group_owner_id="123456789123")
753+ return self.assertTrue(d)
754+
755+ def test_authorize_security_group_with_ip_permissions(self):
756+ """
757+ L{EC2Client.authorize_security_group} returns a C{Deferred} that
758+ eventually fires with a true value, indicating the success of the
759+ operation. There are two ways to use the method: set another group's
760+ IP permissions or set new IP permissions; this test checks the second
761+ way.
762+ """
763+ class StubQuery(object):
764+ def __init__(stub, action, creds, endpoint, other_params=None):
765+ self.assertEqual(action, "AuthorizeSecurityGroupIngress")
766+ self.assertEqual(creds.access_key, "foo")
767+ self.assertEqual(creds.secret_key, "bar")
768+ self.assertEqual(other_params, {
769+ "GroupName": "WebServers",
770+ "FromPort": "22", "ToPort": "80",
771+ "IpProtocol": "tcp", "CidrIp": "0.0.0.0/0",
772+ })
773+ def submit(self):
774+ return succeed(payload.sample_authorize_security_group)
775+
776+ creds = AWSCredentials("foo", "bar")
777+ ec2 = client.EC2Client(creds, query_factory=StubQuery)
778+ d = ec2.authorize_security_group(
779+ "WebServers", ip_protocol="tcp", from_port="22", to_port="80",
780+ cidr_ip="0.0.0.0/0")
781+ return self.assertTrue(d)
782+
783+ def test_authorize_security_group_with_missing_parameters(self):
784+ """
785+ L{EC2Client.authorize_security_group} returns a C{Deferred} that
786+ eventually fires with a true value, indicating the success of the
787+ operation. There are two ways to use the method: set another group's
788+ IP permissions or set new IP permissions. If not all group-setting
789+ parameters are set and not all IP permission parameters are set, an
790+ error is raised.
791+ """
792+ creds = AWSCredentials("foo", "bar")
793+ ec2 = client.EC2Client(creds)
794+ self.assertRaises(ValueError, ec2.authorize_security_group,
795+ "WebServers", ip_protocol="tcp", from_port="22")
796+ try:
797+ d = ec2.authorize_security_group(
798+ "WebServers", ip_protocol="tcp", from_port="22")
799+ except Exception, error:
800+ self.assertEquals(
801+ str(error),
802+ ("You must specify either both group parameters or all the "
803+ "ip parameters."))
804+
805+ def test_authorize_group_permission(self):
806+ """
807+ L{EC2Client.authorize_group_permission} returns a C{Deferred}
808+ that eventually fires with a true value, indicating the success of the
809+ operation.
810+ """
811+ class StubQuery(object):
812+ def __init__(stub, action, creds, endpoint, other_params=None):
813+ self.assertEqual(action, "AuthorizeSecurityGroupIngress")
814+ self.assertEqual(creds.access_key, "foo")
815+ self.assertEqual(creds.secret_key, "bar")
816+ self.assertEqual(other_params, {
817+ "GroupName": "WebServers",
818+ "SourceSecurityGroupName": "AppServers",
819+ "SourceSecurityGroupOwnerId": "123456789123",
820+ })
821+ def submit(self):
822+ return succeed(payload.sample_authorize_security_group)
823+
824+ creds = AWSCredentials("foo", "bar")
825+ ec2 = client.EC2Client(creds, query_factory=StubQuery)
826+ d = ec2.authorize_group_permission(
827+ "WebServers", source_group_name="AppServers",
828+ source_group_owner_id="123456789123")
829+ return self.assertTrue(d)
830+
831+ def test_authorize_ip_permission(self):
832+ """
833+ L{EC2Client.authorize_ip_permission} returns a C{Deferred} that
834+ eventually fires with a true value, indicating the success of the
835+ operation.
836+ """
837+ class StubQuery(object):
838+ def __init__(stub, action, creds, endpoint, other_params=None):
839+ self.assertEqual(action, "AuthorizeSecurityGroupIngress")
840+ self.assertEqual(creds.access_key, "foo")
841+ self.assertEqual(creds.secret_key, "bar")
842+ self.assertEqual(other_params, {
843+ "GroupName": "WebServers",
844+ "FromPort": "22", "ToPort": "80",
845+ "IpProtocol": "tcp", "CidrIp": "0.0.0.0/0",
846+ })
847+ def submit(self):
848+ return succeed(payload.sample_authorize_security_group)
849+
850+ creds = AWSCredentials("foo", "bar")
851+ ec2 = client.EC2Client(creds, query_factory=StubQuery)
852+ d = ec2.authorize_ip_permission(
853+ "WebServers", ip_protocol="tcp", from_port="22", to_port="80",
854+ cidr_ip="0.0.0.0/0")
855+ return self.assertTrue(d)
856+
857+ def test_revoke_security_group_with_user_group_pair(self):
858+ """
859+ L{EC2Client.revoke_security_group} returns a C{Deferred} that
860+ eventually fires with a true value, indicating the success of the
861+ operation. There are two ways to use the method: set another group's
862+ IP permissions or set new IP permissions; this test checks the first
863+ way.
864+ """
865+ class StubQuery(object):
866+ def __init__(stub, action, creds, endpoint, other_params=None):
867+ self.assertEqual(action, "RevokeSecurityGroupIngress")
868+ self.assertEqual(creds.access_key, "foo")
869+ self.assertEqual(creds.secret_key, "bar")
870+ self.assertEqual(other_params, {
871+ "GroupName": "WebServers",
872+ "SourceSecurityGroupName": "AppServers",
873+ "SourceSecurityGroupOwnerId": "123456789123",
874+ })
875+ def submit(self):
876+ return succeed(payload.sample_revoke_security_group)
877+
878+ creds = AWSCredentials("foo", "bar")
879+ ec2 = client.EC2Client(creds, query_factory=StubQuery)
880+ d = ec2.revoke_security_group(
881+ "WebServers", source_group_name="AppServers",
882+ source_group_owner_id="123456789123")
883+ return self.assertTrue(d)
884+
885+ def test_revoke_security_group_with_ip_permissions(self):
886+ """
887+ L{EC2Client.revoke_security_group} returns a C{Deferred} that
888+ eventually fires with a true value, indicating the success of the
889+ operation. There are two ways to use the method: set another group's
890+ IP permissions or set new IP permissions; this test checks the second
891+ way.
892+ """
893+ class StubQuery(object):
894+ def __init__(stub, action, creds, endpoint, other_params=None):
895+ self.assertEqual(action, "RevokeSecurityGroupIngress")
896+ self.assertEqual(creds.access_key, "foo")
897+ self.assertEqual(creds.secret_key, "bar")
898+ self.assertEqual(other_params, {
899+ "GroupName": "WebServers",
900+ "FromPort": "22", "ToPort": "80",
901+ "IpProtocol": "tcp", "CidrIp": "0.0.0.0/0",
902+ })
903+ def submit(self):
904+ return succeed(payload.sample_revoke_security_group)
905+
906+ creds = AWSCredentials("foo", "bar")
907+ ec2 = client.EC2Client(creds, query_factory=StubQuery)
908+ d = ec2.revoke_security_group(
909+ "WebServers", ip_protocol="tcp", from_port="22", to_port="80",
910+ cidr_ip="0.0.0.0/0")
911+ return self.assertTrue(d)
912+
913+ def test_revoke_security_group_with_missing_parameters(self):
914+ """
915+ L{EC2Client.revoke_security_group} returns a C{Deferred} that
916+ eventually fires with a true value, indicating the success of the
917+ operation. There are two ways to use the method: set another group's
918+ IP permissions or set new IP permissions. If not all group-setting
919+ parameters are set and not all IP permission parameters are set, an
920+ error is raised.
921+ """
922+ creds = AWSCredentials("foo", "bar")
923+ ec2 = client.EC2Client(creds)
924+ self.assertRaises(ValueError, ec2.authorize_security_group,
925+ "WebServers", ip_protocol="tcp", from_port="22")
926+ try:
927+ d = ec2.authorize_security_group(
928+ "WebServers", ip_protocol="tcp", from_port="22")
929+ except Exception, error:
930+ self.assertEquals(
931+ str(error),
932+ ("You must specify either both group parameters or all the "
933+ "ip parameters."))
934+
935+ def test_revoke_group_permission(self):
936+ """
937+ L{EC2Client.revoke_group_permission} returns a C{Deferred} that
938+ eventually fires with a true value, indicating the success of the
939+ operation.
940+ """
941+ class StubQuery(object):
942+ def __init__(stub, action, creds, endpoint, other_params=None):
943+ self.assertEqual(action, "RevokeSecurityGroupIngress")
944+ self.assertEqual(creds.access_key, "foo")
945+ self.assertEqual(creds.secret_key, "bar")
946+ self.assertEqual(other_params, {
947+ "GroupName": "WebServers",
948+ "SourceSecurityGroupName": "AppServers",
949+ "SourceSecurityGroupOwnerId": "123456789123",
950+ })
951+ def submit(self):
952+ return succeed(payload.sample_revoke_security_group)
953+
954+ creds = AWSCredentials("foo", "bar")
955+ ec2 = client.EC2Client(creds, query_factory=StubQuery)
956+ d = ec2.revoke_group_permission(
957+ "WebServers", source_group_name="AppServers",
958+ source_group_owner_id="123456789123")
959+ return self.assertTrue(d)
960+
961+ def test_revoke_ip_permission(self):
962+ """
963+ L{EC2Client.revoke_ip_permission} returns a C{Deferred} that
964+ eventually fires with a true value, indicating the success of the
965+ operation.
966+ """
967+ class StubQuery(object):
968+ def __init__(stub, action, creds, endpoint, other_params=None):
969+ self.assertEqual(action, "RevokeSecurityGroupIngress")
970+ self.assertEqual(creds.access_key, "foo")
971+ self.assertEqual(creds.secret_key, "bar")
972+ self.assertEqual(other_params, {
973+ "GroupName": "WebServers",
974+ "FromPort": "22", "ToPort": "80",
975+ "IpProtocol": "tcp", "CidrIp": "0.0.0.0/0",
976+ })
977+ def submit(self):
978+ return succeed(payload.sample_revoke_security_group)
979+
980+ creds = AWSCredentials("foo", "bar")
981+ ec2 = client.EC2Client(creds, query_factory=StubQuery)
982+ d = ec2.revoke_ip_permission(
983+ "WebServers", ip_protocol="tcp", from_port="22", to_port="80",
984+ cidr_ip="0.0.0.0/0")
985+ return self.assertTrue(d)
986+
987+
988+class EC2ClientEBSTestCase(TXAWSTestCase):
989
990 def setUp(self):
991 TXAWSTestCase.setUp(self)
992@@ -970,3 +1061,252 @@
993 d = ec2.delete_keypair("example-key-name")
994 d.addCallback(self.assertFalse)
995 return d
996+
997+
998+class EC2ErrorWrapperTestCase(TXAWSTestCase):
999+
1000+ def setUp(self):
1001+ TXAWSTestCase.setUp(self)
1002+
1003+ def get_failure(self, status=None, type=None, message=""):
1004+ failure = Failure(type(message))
1005+ failure.value.response = payload.sample_ec2_error_message
1006+ failure.value.status = status
1007+ return failure
1008+
1009+ def test_302_error(self):
1010+ failure = self.get_failure(302, Exception, "found")
1011+ error = self.assertRaises(Exception, client.ec2_error_wrapper, failure)
1012+ self.assertEquals(failure.type, type(error))
1013+ self.assertFalse(isinstance(error, EC2Error))
1014+ self.assertTrue(isinstance(error, Exception))
1015+ self.assertEquals(error.message, "found")
1016+
1017+ def test_400_error(self):
1018+ failure = self.get_failure(400, Exception)
1019+ error = self.assertRaises(EC2Error, client.ec2_error_wrapper, failure)
1020+ self.assertNotEquals(failure.type, type(error))
1021+ self.assertTrue(isinstance(error, EC2Error))
1022+ self.assertEquals(error.get_error_codes(), "Error.Code")
1023+ self.assertEquals(error.get_error_messages(), "Message for Error.Code")
1024+
1025+ def test_404_error(self):
1026+ failure = self.get_failure(404, Exception)
1027+ error = self.assertRaises(EC2Error, client.ec2_error_wrapper, failure)
1028+ self.assertNotEquals(failure.type, type(error))
1029+ self.assertTrue(isinstance(error, EC2Error))
1030+ self.assertEquals(error.get_error_codes(), "Error.Code")
1031+ self.assertEquals(error.get_error_messages(), "Message for Error.Code")
1032+
1033+ def test_500_error(self):
1034+ failure = self.get_failure(500, Exception, "A server error occurred")
1035+ error = self.assertRaises(Exception, client.ec2_error_wrapper, failure)
1036+ self.assertFalse(isinstance(error, EC2Error))
1037+ self.assertTrue(isinstance(error, Exception))
1038+ self.assertEquals(error.message, "A server error occurred")
1039+
1040+ def test_timeout_error(self):
1041+ failure = self.get_failure(type=Exception, message="timeout")
1042+ error = self.assertRaises(Exception, client.ec2_error_wrapper, failure)
1043+ self.assertFalse(isinstance(error, EC2Error))
1044+ self.assertTrue(isinstance(error, Exception))
1045+ self.assertEquals(error.message, "timeout")
1046+
1047+
1048+class QueryTestCase(TXAWSTestCase):
1049+
1050+ def setUp(self):
1051+ TXAWSTestCase.setUp(self)
1052+ self.creds = AWSCredentials("foo", "bar")
1053+ self.endpoint = AWSServiceEndpoint(uri=EC2_ENDPOINT_US)
1054+
1055+ def test_init_minimum(self):
1056+ query = client.Query("DescribeInstances", self.creds, self.endpoint)
1057+ self.assertTrue("Timestamp" in query.params)
1058+ del query.params["Timestamp"]
1059+ self.assertEqual(
1060+ {"AWSAccessKeyId": "foo",
1061+ "Action": "DescribeInstances",
1062+ "SignatureMethod": "HmacSHA1",
1063+ "SignatureVersion": "2",
1064+ "Version": "2008-12-01"},
1065+ query.params)
1066+
1067+ def test_init_requires_action(self):
1068+ self.assertRaises(TypeError, client.Query)
1069+
1070+ def test_init_requires_creds(self):
1071+ self.assertRaises(TypeError, client.Query, None)
1072+
1073+ def test_init_other_args_are_params(self):
1074+ query = client.Query("DescribeInstances", self.creds, self.endpoint,
1075+ {"InstanceId.0": "12345"},
1076+ time_tuple=(2007,11,12,13,14,15,0,0,0))
1077+ self.assertEqual(
1078+ {"AWSAccessKeyId": "foo",
1079+ "Action": "DescribeInstances",
1080+ "InstanceId.0": "12345",
1081+ "SignatureMethod": "HmacSHA1",
1082+ "SignatureVersion": "2",
1083+ "Timestamp": "2007-11-12T13:14:15Z",
1084+ "Version": "2008-12-01"},
1085+ query.params)
1086+
1087+ def test_sorted_params(self):
1088+ query = client.Query("DescribeInstances", self.creds, self.endpoint,
1089+ {"fun": "games"},
1090+ time_tuple=(2007,11,12,13,14,15,0,0,0))
1091+ self.assertEqual([
1092+ ("AWSAccessKeyId", "foo"),
1093+ ("Action", "DescribeInstances"),
1094+ ("SignatureMethod", "HmacSHA1"),
1095+ ("SignatureVersion", "2"),
1096+ ("Timestamp", "2007-11-12T13:14:15Z"),
1097+ ("Version", "2008-12-01"),
1098+ ("fun", "games"),
1099+ ], query.sorted_params())
1100+
1101+ def test_encode_unreserved(self):
1102+ all_unreserved = ("ABCDEFGHIJKLMNOPQRSTUVWXYZ"
1103+ "abcdefghijklmnopqrstuvwxyz0123456789-_.~")
1104+ query = client.Query("DescribeInstances", self.creds, self.endpoint)
1105+ self.assertEqual(all_unreserved, query.encode(all_unreserved))
1106+
1107+ def test_encode_space(self):
1108+ """This may be just "url encode", but the AWS manual isn't clear."""
1109+ query = client.Query("DescribeInstances", self.creds, self.endpoint)
1110+ self.assertEqual("a%20space", query.encode("a space"))
1111+
1112+ def test_canonical_query(self):
1113+ query = client.Query("DescribeInstances", self.creds, self.endpoint,
1114+ {"fu n": "g/ames", "argwithnovalue":"",
1115+ "InstanceId.1": "i-1234"},
1116+ time_tuple=(2007,11,12,13,14,15,0,0,0))
1117+ expected_query = ("AWSAccessKeyId=foo&Action=DescribeInstances"
1118+ "&InstanceId.1=i-1234"
1119+ "&SignatureMethod=HmacSHA1&SignatureVersion=2&"
1120+ "Timestamp=2007-11-12T13%3A14%3A15Z&Version=2008-12-01&"
1121+ "argwithnovalue=&fu%20n=g%2Fames")
1122+ self.assertEqual(expected_query, query.canonical_query_params())
1123+
1124+ def test_signing_text(self):
1125+ query = client.Query("DescribeInstances", self.creds, self.endpoint,
1126+ time_tuple=(2007,11,12,13,14,15,0,0,0))
1127+ signing_text = ("GET\n%s\n/\n" % self.endpoint.host +
1128+ "AWSAccessKeyId=foo&Action=DescribeInstances&"
1129+ "SignatureMethod=HmacSHA1&SignatureVersion=2&"
1130+ "Timestamp=2007-11-12T13%3A14%3A15Z&Version=2008-12-01")
1131+ self.assertEqual(signing_text, query.signing_text())
1132+
1133+ def test_sign(self):
1134+ query = client.Query("DescribeInstances", self.creds, self.endpoint,
1135+ time_tuple=(2007,11,12,13,14,15,0,0,0))
1136+ query.sign()
1137+ self.assertEqual("JuCpwFA2H4OVF3Ql/lAQs+V6iMc=",
1138+ query.params["Signature"])
1139+
1140+ def test_submit_400(self):
1141+ """A 4xx response status from EC2 should raise a txAWS EC2Error."""
1142+ status = 400
1143+ self.addCleanup(setattr, client.Query, "get_page",
1144+ client.Query.get_page)
1145+ fake_page_getter = FakePageGetter(
1146+ status, payload.sample_ec2_error_message)
1147+ client.Query.get_page = fake_page_getter.get_page_with_exception
1148+
1149+ def check_error(error):
1150+ self.assertTrue(isinstance(error, EC2Error))
1151+ self.assertEquals(error.get_error_codes(), "Error.Code")
1152+ self.assertEquals(
1153+ error.get_error_messages(),
1154+ "Message for Error.Code")
1155+ self.assertEquals(error.status, status)
1156+ self.assertEquals(error.response, payload.sample_ec2_error_message)
1157+
1158+ query = client.Query(
1159+ 'BadQuery', self.creds, self.endpoint,
1160+ time_tuple=(2009,8,15,13,14,15,0,0,0))
1161+
1162+ failure = query.submit()
1163+ d = self.assertFailure(failure, EC2Error)
1164+ d.addCallback(check_error)
1165+ return d
1166+
1167+ def test_submit_500(self):
1168+ """
1169+ A 5xx response status from EC2 should raise the original Twisted
1170+ exception.
1171+ """
1172+ status = 500
1173+ self.addCleanup(setattr, client.Query, "get_page",
1174+ client.Query.get_page)
1175+ fake_page_getter = FakePageGetter(
1176+ status, payload.sample_ec2_error_message)
1177+ client.Query.get_page = fake_page_getter.get_page_with_exception
1178+
1179+ def check_error(error):
1180+ self.assertFalse(isinstance(error, EC2Error))
1181+ self.assertEquals(error.status, status)
1182+ self.assertEquals(str(error), "500 There's been an error")
1183+
1184+ query = client.Query(
1185+ 'BadQuery', self.creds, self.endpoint,
1186+ time_tuple=(2009,8,15,13,14,15,0,0,0))
1187+
1188+ failure = query.submit()
1189+ d = self.assertFailure(failure, Error)
1190+ d.addCallback(check_error)
1191+ return d
1192+
1193+
1194+class QueryPageGetterTestCase(TXAWSTestCase):
1195+
1196+ def setUp(self):
1197+ TXAWSTestCase.setUp(self)
1198+ self.creds = AWSCredentials("foo", "bar")
1199+ self.endpoint = AWSServiceEndpoint(uri=EC2_ENDPOINT_US)
1200+ self.twisted_client_test_setup()
1201+ self.cleanupServerConnections = 0
1202+
1203+ def tearDown(self):
1204+ """Copied from twisted.web.test.test_webclient."""
1205+ # If the test indicated it might leave some server-side connections
1206+ # around, clean them up.
1207+ connections = self.wrapper.protocols.keys()
1208+ # If there are fewer server-side connections than requested,
1209+ # that's okay. Some might have noticed that the client closed
1210+ # the connection and cleaned up after themselves.
1211+ for n in range(min(len(connections), self.cleanupServerConnections)):
1212+ proto = connections.pop()
1213+ #msg("Closing %r" % (proto,))
1214+ proto.transport.loseConnection()
1215+ if connections:
1216+ #msg("Some left-over connections; this test is probably buggy.")
1217+ pass
1218+ return self.port.stopListening()
1219+
1220+ def _listen(self, site):
1221+ return reactor.listenTCP(0, site, interface="127.0.0.1")
1222+
1223+ def twisted_client_test_setup(self):
1224+ name = self.mktemp()
1225+ os.mkdir(name)
1226+ FilePath(name).child("file").setContent("0123456789")
1227+ resource = static.File(name)
1228+ resource.putChild("redirect", util.Redirect("/file"))
1229+ self.site = server.Site(resource, timeout=None)
1230+ self.wrapper = WrappingFactory(self.site)
1231+ self.port = self._listen(self.wrapper)
1232+ self.portno = self.port.getHost().port
1233+
1234+ def get_url(self, path):
1235+ return "http://127.0.0.1:%d/%s" % (self.portno, path)
1236+
1237+ def test_get_page(self):
1238+ """Copied from twisted.web.test.test_webclient."""
1239+ query = client.Query(
1240+ 'DummyQuery', self.creds, self.endpoint,
1241+ time_tuple=(2009,8,17,13,14,15,0,0,0))
1242+ deferred = query.get_page(self.get_url("file"))
1243+ deferred.addCallback(self.assertEquals, "0123456789")
1244+ return deferred
1245
1246=== modified file 'txaws/exception.py'
1247--- txaws/exception.py 2009-09-10 00:32:37 +0000
1248+++ txaws/exception.py 2009-09-25 05:33:10 +0000
1249@@ -3,6 +3,7 @@
1250
1251 from twisted.web.error import Error
1252
1253+
1254 class AWSError(Error):
1255 """
1256 A base class for txAWS errors.
1257
1258=== modified file 'txaws/testing/payload.py'
1259--- txaws/testing/payload.py 2009-09-15 22:19:14 +0000
1260+++ txaws/testing/payload.py 2009-09-25 05:33:10 +0000
1261@@ -202,6 +202,48 @@
1262 """ % (aws_api,)
1263
1264
1265+sample_create_security_group = """\
1266+<CreateSecurityGroupResponse xmlns="http://ec2.amazonaws.com/doc/%s/">
1267+ <return>true</return>
1268+</CreateSecurityGroupResponse>
1269+""" % (aws_api,)
1270+
1271+
1272+sample_delete_security_group = """\
1273+<DeleteSecurityGroupResponse xmlns="http://ec2.amazonaws.com/doc/%s/">
1274+ <return>true</return>
1275+</DeleteSecurityGroupResponse>
1276+""" % (aws_api,)
1277+
1278+
1279+sample_delete_security_group_failure = """\
1280+<?xml version="1.0"?>
1281+<Response>
1282+ <Errors>
1283+ <Error>
1284+ <Code>InvalidGroup.InUse</Code>
1285+ <Message>Group groupID1:GroupReferredTo is used by groups: groupID2:UsingGroup</Message>
1286+ </Error>
1287+ </Errors>
1288+ <RequestID>9a6df05f-9c27-47aa-81d8-6619689210cc</RequestID>
1289+</Response>
1290+"""
1291+
1292+
1293+sample_authorize_security_group = """\
1294+<AuthorizeSecurityGroupIngressResponse xmlns="http://ec2.amazonaws.com/doc/%s/">
1295+ <return>true</return>
1296+</AuthorizeSecurityGroupIngressResponse>
1297+""" % (aws_api,)
1298+
1299+
1300+sample_revoke_security_group = """\
1301+<RevokeSecurityGroupIngressResponse xmlns="http://ec2.amazonaws.com/doc/%s/">
1302+ <return>true</return>
1303+</RevokeSecurityGroupIngressResponse>
1304+""" % (aws_api,)
1305+
1306+
1307 sample_describe_volumes_result = """\
1308 <?xml version="1.0"?>
1309 <DescribeVolumesResponse xmlns="http://ec2.amazonaws.com/doc/%s/">

Subscribers

People subscribed via source and target branches

to all changes: