Merge lp:~therve/txaws/ebs-support into lp:~txawsteam/txaws/trunk

Proposed by Thomas Herve
Status: Merged
Merged at revision: not available
Proposed branch: lp:~therve/txaws/ebs-support
Merge into: lp:~txawsteam/txaws/trunk
Diff against target: None lines
To merge this branch: bzr merge lp:~therve/txaws/ebs-support
Reviewer Review Type Date Requested Status
Duncan McGreggor Approve
Review via email: mp+10742@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Thomas Herve (therve) wrote :

This is ready to review in the attached branch. I also took the opportunity to remove the need of namespaces, as it feels useless to me.

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

Looks great -- +1 for merge. Thanks for this new functionality!

review: Approve
lp:~therve/txaws/ebs-support updated
13. By Thomas Herve

Merge separate branches for EBS support.

14. By Thomas Herve

Add parameter support to describe volumes

15. By Thomas Herve

Add parameter support to describe snapshots

16. By Thomas Herve

Merge 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-08-21 03:26:35 +0000
3+++ txaws/ec2/client.py 2009-08-26 13:50:25 +0000
4@@ -3,7 +3,7 @@
5
6 """EC2 client support."""
7
8-from base64 import b64encode
9+from datetime import datetime
10 from urllib import quote
11
12 from twisted.web.client import getPage
13@@ -19,7 +19,7 @@
14 """An Amazon EC2 Reservation.
15
16 @attrib reservation_id: Unique ID of the reservation.
17- @attrib owner_id: AWS Access Key ID of the user who owns the reservation.
18+ @attrib owner_id: AWS Access Key ID of the user who owns the reservation.
19 @attrib groups: A list of security groups.
20 """
21 def __init__(self, reservation_id, owner_id, groups=None):
22@@ -72,11 +72,32 @@
23 self.reservation = reservation
24
25
26+class Volume(object):
27+ """An EBS volume instance."""
28+
29+ def __init__(self, id, size, status, create_time):
30+ self.id = id
31+ self.size = size
32+ self.status = status
33+ self.create_time = create_time
34+ self.attachments = []
35+
36+
37+class Attachment(object):
38+ """An attachment of a L{Volume}."""
39+
40+ def __init__(self, instance_id, snapshot_id, availability_zone, status,
41+ attach_time):
42+ self.instance_id = instance_id
43+ self.snapshot_id = snapshot_id
44+ self.availability_zone = availability_zone
45+ self.status = status
46+ self.attach_time = attach_time
47+
48+
49 class EC2Client(object):
50 """A client for EC2."""
51
52- name_space = '{http://ec2.amazonaws.com/doc/2008-12-01/}'
53-
54 def __init__(self, creds=None, query_factory=None):
55 """Create an EC2Client.
56
57@@ -102,7 +123,7 @@
58 """
59 Parse the reservations XML payload that is returned from an AWS
60 describeInstances API call.
61-
62+
63 Instead of returning the reservations as the "top-most" object, we
64 return the object that most developers and their code will be
65 interested in: the instances. In instances reservation is available on
66@@ -111,53 +132,38 @@
67 root = XML(xml_bytes)
68 results = []
69 # May be a more elegant way to do this:
70- for reservation_data in root.find(self.name_space + 'reservationSet'):
71+ for reservation_data in root.find("reservationSet"):
72 # Get the security group information.
73 groups = []
74- for group_data in reservation_data.find(
75- self.name_space + 'groupSet'):
76- group_id = group_data.findtext(self.name_space + 'groupId')
77+ for group_data in reservation_data.find("groupSet"):
78+ group_id = group_data.findtext("groupId")
79 groups.append(group_id)
80 # Create a reservation object with the parsed data.
81 reservation = Reservation(
82- reservation_id=reservation_data.findtext(
83- self.name_space + 'reservationId'),
84- owner_id=reservation_data.findtext(
85- self.name_space + 'ownerId'),
86+ reservation_id=reservation_data.findtext("reservationId"),
87+ owner_id=reservation_data.findtext("ownerId"),
88 groups=groups)
89 # Get the list of instances.
90 instances = []
91- for instance_data in reservation_data.find(
92- self.name_space + 'instancesSet'):
93- instance_id = instance_data.findtext(
94- self.name_space + 'instanceId')
95+ for instance_data in reservation_data.find("instancesSet"):
96+ instance_id = instance_data.findtext("instanceId")
97 instance_state = instance_data.find(
98- self.name_space + 'instanceState').findtext(
99- self.name_space + 'name')
100- instance_type = instance_data.findtext(
101- self.name_space + 'instanceType')
102- image_id = instance_data.findtext(self.name_space + 'imageId')
103- private_dns_name = instance_data.findtext(
104- self.name_space + 'privateDnsName')
105- dns_name = instance_data.findtext(self.name_space + 'dnsName')
106- key_name = instance_data.findtext(self.name_space + 'keyName')
107- ami_launch_index = instance_data.findtext(
108- self.name_space + 'amiLaunchIndex')
109- launch_time = instance_data.findtext(
110- self.name_space + 'launchTime')
111- placement = instance_data.find(
112- self.name_space + 'placement').findtext(
113- self.name_space + 'availabilityZone')
114+ "instanceState").findtext("name")
115+ instance_type = instance_data.findtext("instanceType")
116+ image_id = instance_data.findtext("imageId")
117+ private_dns_name = instance_data.findtext("privateDnsName")
118+ dns_name = instance_data.findtext("dnsName")
119+ key_name = instance_data.findtext("keyName")
120+ ami_launch_index = instance_data.findtext("amiLaunchIndex")
121+ launch_time = instance_data.findtext("launchTime")
122+ placement = instance_data.find("placement").findtext(
123+ "availabilityZone")
124 products = []
125- for product_data in instance_data.find(
126- self.name_space + 'productCodesSet'):
127- product_code = product_data.findtext(
128- self.name_space + 'productCode')
129+ for product_data in instance_data.find("productCodesSet"):
130+ product_code = product_data.findtext("productCode")
131 products.append(product_code)
132- kernel_id = instance_data.findtext(
133- self.name_space + 'kernelId')
134- ramdisk_id = instance_data.findtext(
135- self.name_space + 'ramdiskId')
136+ kernel_id = instance_data.findtext("kernelId")
137+ ramdisk_id = instance_data.findtext("ramdiskId")
138 instance = Instance(
139 instance_id, instance_state, instance_type, image_id,
140 private_dns_name, dns_name, key_name, ami_launch_index,
141@@ -169,7 +175,7 @@
142
143 def terminate_instances(self, *instance_ids):
144 """Terminate some instances.
145-
146+
147 @param instance_ids: The ids of the instances to terminate.
148 @return: A deferred which on success gives an iterable of
149 (id, old-state, new-state) tuples.
150@@ -185,17 +191,48 @@
151 root = XML(xml_bytes)
152 result = []
153 # May be a more elegant way to do this:
154- for instance in root.find(self.name_space + 'instancesSet'):
155- instanceId = instance.findtext(self.name_space + 'instanceId')
156- previousState = instance.find(
157- self.name_space + 'previousState').findtext(
158- self.name_space + 'name')
159- shutdownState = instance.find(
160- self.name_space + 'shutdownState').findtext(
161- self.name_space + 'name')
162+ for instance in root.find("instancesSet"):
163+ instanceId = instance.findtext("instanceId")
164+ previousState = instance.find("previousState").findtext(
165+ "name")
166+ shutdownState = instance.find("shutdownState").findtext(
167+ "name")
168 result.append((instanceId, previousState, shutdownState))
169 return result
170
171+ def describe_volumes(self):
172+ """Describe available volumes."""
173+ q = self.query_factory("DescribeVolumes", self.creds)
174+ d = q.submit()
175+ return d.addCallback(self._parse_volumes)
176+
177+ def _parse_volumes(self, xml_bytes):
178+ root = XML(xml_bytes)
179+ result = []
180+ for volume_data in root.find("volumeSet"):
181+ volume_id = volume_data.findtext("volumeId")
182+ size = int(volume_data.findtext("size"))
183+ status = volume_data.findtext("status")
184+ create_time = volume_data.findtext("createTime")
185+ create_time = datetime.strptime(
186+ create_time[:19], "%Y-%m-%dT%H:%M:%S")
187+ volume = Volume(volume_id, size, status, create_time)
188+ result.append(volume)
189+ for attachment_data in volume_data.find("attachmentSet"):
190+ instance_id = attachment_data.findtext("instanceId")
191+ snapshot_id = attachment_data.findtext("snapshotId")
192+ availability_zone = attachment_data.findtext(
193+ "availabilityZone")
194+ status = attachment_data.findtext("status")
195+ attach_time = attachment_data.findtext("attachTime")
196+ attach_time = datetime.strptime(
197+ attach_time[:19], "%Y-%m-%dT%H:%M:%S")
198+ attachment = Attachment(
199+ instance_id, snapshot_id, availability_zone, status,
200+ attach_time)
201+ volume.attachments.append(attachment)
202+ return result
203+
204
205 class Query(object):
206 """A query that may be submitted to EC2."""
207@@ -204,7 +241,7 @@
208 """Create a Query to submit to EC2."""
209 # Require params (2008-12-01 API):
210 # Version, SignatureVersion, SignatureMethod, Action, AWSAccessKeyId,
211- # Timestamp || Expires, Signature,
212+ # Timestamp || Expires, Signature,
213 self.params = {'Version': '2008-12-01',
214 'SignatureVersion': '2',
215 'SignatureMethod': 'HmacSHA1',
216@@ -242,7 +279,7 @@
217
218 def sign(self):
219 """Sign this query using its built in credentials.
220-
221+
222 This prepares it to be sent, and should be done as the last step before
223 submitting the query. Signing is done automatically - this is a public
224 method to facilitate testing.
225
226=== modified file 'txaws/ec2/tests/test_client.py'
227--- txaws/ec2/tests/test_client.py 2009-08-21 03:26:35 +0000
228+++ txaws/ec2/tests/test_client.py 2009-08-26 14:31:01 +0000
229@@ -1,6 +1,7 @@
230 # Copyright (C) 2009 Robert Collins <robertc@robertcollins.net>
231 # Licenced under the txaws licence available at /LICENSE in the txaws source.
232
233+from datetime import datetime
234 import os
235
236 from twisted.internet.defer import succeed
237@@ -86,6 +87,31 @@
238 """
239
240
241+sample_describe_volumes_result = """<?xml version="1.0"?>
242+<DescribeVolumesResponse xmlns="http://ec2.amazonaws.com/doc/2008-12-01/">
243+ <volumeSet>
244+ <item>
245+ <volumeId>vol-4282672b</volumeId>
246+ <size>800</size>
247+ <status>in-use</status>
248+ <createTime>2008-05-07T11:51:50.000Z</createTime>
249+ <attachmentSet>
250+ <item>
251+ <volumeId>vol-4282672b</volumeId>
252+ <instanceId>i-6058a509</instanceId>
253+ <size>800</size>
254+ <snapshotId>snap-12345678</snapshotId>
255+ <availabilityZone>us-east-1a</availabilityZone>
256+ <status>attached</status>
257+ <attachTime>2008-05-07T12:51:50.000Z</attachTime>
258+ </item>
259+ </attachmentSet>
260+ </item>
261+ </volumeSet>
262+</DescribeVolumesResponse>
263+"""
264+
265+
266 class ReservationTestCase(TXAWSTestCase):
267
268 def test_reservation_creation(self):
269@@ -118,7 +144,7 @@
270
271
272 class TestEC2Client(TXAWSTestCase):
273-
274+
275 def test_init_no_creds(self):
276 os.environ['AWS_SECRET_ACCESS_KEY'] = 'foo'
277 os.environ['AWS_ACCESS_KEY_ID'] = 'bar'
278@@ -160,7 +186,6 @@
279 self.assertEquals(instance.kernel_id, "aki-b51cf9dc")
280 self.assertEquals(instance.ramdisk_id, "ari-b31cf9da")
281
282-
283 def test_parse_reservation(self):
284 ec2 = client.EC2Client(creds='foo')
285 results = ec2._parse_instances(sample_describe_instances_result)
286@@ -286,3 +311,38 @@
287 query.sign()
288 self.assertEqual('4hEtLuZo9i6kuG3TOXvRQNOrE/U=',
289 query.params['Signature'])
290+
291+
292+class TestEBS(TXAWSTestCase):
293+
294+ def test_describe_volumes(self):
295+
296+ class StubQuery(object):
297+ def __init__(stub, action, creds):
298+ self.assertEqual(action, "DescribeVolumes")
299+ self.assertEqual("foo", creds)
300+
301+ def submit(self):
302+ return succeed(sample_describe_volumes_result)
303+
304+ def check_parsed_volumes(volumes):
305+ self.assertEquals(len(volumes), 1)
306+ volume = volumes[0]
307+ self.assertEquals(volume.id, "vol-4282672b")
308+ self.assertEquals(volume.size, 800)
309+ self.assertEquals(volume.status, "in-use")
310+ create_time = datetime(2008, 05, 07, 11, 51, 50)
311+ self.assertEquals(volume.create_time, create_time)
312+ self.assertEquals(len(volume.attachments), 1)
313+ attachment = volume.attachments[0]
314+ self.assertEquals(attachment.instance_id, "i-6058a509")
315+ self.assertEquals(attachment.snapshot_id, "snap-12345678")
316+ self.assertEquals(attachment.availability_zone, "us-east-1a")
317+ self.assertEquals(attachment.status, "attached")
318+ attach_time = datetime(2008, 05, 07, 12, 51, 50)
319+ self.assertEquals(attachment.attach_time, attach_time)
320+
321+ ec2 = client.EC2Client(creds="foo", query_factory=StubQuery)
322+ d = ec2.describe_volumes()
323+ d.addCallback(check_parsed_volumes)
324+ return d
325
326=== modified file 'txaws/storage/client.py'
327--- txaws/storage/client.py 2009-08-20 12:15:12 +0000
328+++ txaws/storage/client.py 2009-08-26 13:50:25 +0000
329@@ -19,9 +19,6 @@
330 from txaws.util import XML, calculate_md5
331
332
333-name_space = '{http://s3.amazonaws.com/doc/2006-03-01/}'
334-
335-
336 class S3Request(object):
337
338 def __init__(self, verb, bucket=None, object_name=None, data='',
339@@ -114,10 +111,10 @@
340 Parse XML bucket list response.
341 """
342 root = XML(response)
343- for bucket in root.find(name_space + 'Buckets'):
344- timeText = bucket.findtext(name_space + 'CreationDate')
345+ for bucket in root.find("Buckets"):
346+ timeText = bucket.findtext("CreationDate")
347 yield {
348- 'name': bucket.findtext(name_space + 'Name'),
349+ 'name': bucket.findtext("Name"),
350 'created': Time.fromISO8601TimeAndDate(timeText),
351 }
352
353
354=== modified file 'txaws/util.py'
355--- txaws/util.py 2009-08-20 12:15:12 +0000
356+++ txaws/util.py 2009-08-26 13:50:25 +0000
357@@ -9,14 +9,14 @@
358 import hmac
359 import time
360
361-# Import XML from somwhere; here in one place to prevent duplication.
362+# Import XMLTreeBuilder from somwhere; here in one place to prevent duplication.
363 try:
364- from xml.etree.ElementTree import XML
365+ from xml.etree.ElementTree import XMLTreeBuilder
366 except ImportError:
367- from elementtree.ElementTree import XML
368-
369-
370-__all__ = ['hmac_sha1', 'iso8601time']
371+ from elementtree.ElementTree import XMLTreeBuilder
372+
373+
374+__all__ = ["hmac_sha1", "iso8601time", "XML"]
375
376
377 def calculate_md5(data):
378@@ -38,3 +38,17 @@
379 return time.strftime("%Y-%m-%dT%H:%M:%SZ", time_tuple)
380 else:
381 return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
382+
383+
384+class NamespaceFixXmlTreeBuilder(XMLTreeBuilder):
385+
386+ def _fixname(self, key):
387+ if "}" in key:
388+ key = key.split("}", 1)[1]
389+ return key
390+
391+
392+def XML(text):
393+ parser = NamespaceFixXmlTreeBuilder()
394+ parser.feed(text)
395+ return parser.close()

Subscribers

People subscribed via source and target branches