Merge lp:~fwierzbicki/txaws/break-out-ec2-parser into lp:txaws

Proposed by Frank Wierzbicki
Status: Merged
Approved by: Thomas Herve
Approved revision: 87
Merged at revision: 85
Proposed branch: lp:~fwierzbicki/txaws/break-out-ec2-parser
Merge into: lp:txaws
Diff against target: 975 lines (+405/-318)
5 files modified
txaws/client/base.py (+4/-1)
txaws/client/tests/test_client.py (+2/-1)
txaws/ec2/client.py (+396/-314)
txaws/ec2/tests/test_client.py (+1/-1)
txaws/testing/ec2.py (+2/-1)
To merge this branch: bzr merge lp:~fwierzbicki/txaws/break-out-ec2-parser
Reviewer Review Type Date Requested Status
Thomas Herve Approve
Duncan McGreggor Approve
Review via email: mp+58623@code.launchpad.net

Description of the change

This branch breaks out the private parsing methods from EC2Client into a separate Parser class with public methods so that they can be safely overridden. It also adds docstrings to the parser functions that lacked them.

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

Man, I can't tell you how badly I've wanted this... since about the second or third week Thomas and I were working on the ec2 client support!

1) Overall, looks awesome -- nice work, and thanks :-)

2) The Parser class seems to have a superfluous __init__; I'd just recommend removing it.

3) This is a nit... totally up to you, but now that the parse_* methods are in their own Parse class, I'd just remove the "parse_" from each method name. With them in there, it looks like C code ;-)

All tests pass, +1 for merge; points #2 and #3 are up to you.

review: Approve
Revision history for this message
Frank Wierzbicki (fwierzbicki) wrote :

> Man, I can't tell you how badly I've wanted this... since about the second or
> third week Thomas and I were working on the ec2 client support!
>
> 1) Overall, looks awesome -- nice work, and thanks :-)
No problem! It will make my cloud deck work nicer :)

> 2) The Parser class seems to have a superfluous __init__; I'd just recommend
> removing it.
Fixed.

> 3) This is a nit... totally up to you, but now that the parse_* methods are in
> their own Parse class, I'd just remove the "parse_" from each method name.
> With them in there, it looks like C code ;-)
Fixed.

85. By Frank Wierzbicki

Remove parse_ from Parser methods, remove unneeded __init__.

86. By Frank Wierzbicki

Merge with trunk.

87. By Frank Wierzbicki

Add parser parameter to FakeEC2Client.

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

[1] Please update the __all__ attribute of client.py with Query and Parser.

It would have been nice to have direct testing of the Parser class, but it will do it for now. Nice branch, +1!

review: Approve
88. By Frank Wierzbicki

Update __all__

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'txaws/client/base.py'
--- txaws/client/base.py 2011-04-21 14:59:23 +0000
+++ txaws/client/base.py 2011-04-26 16:30:59 +0000
@@ -57,8 +57,10 @@
57 @param endpoint: The service endpoint URI.57 @param endpoint: The service endpoint URI.
58 @param query_factory: The class or function that produces a query58 @param query_factory: The class or function that produces a query
59 object for making requests to the EC2 service.59 object for making requests to the EC2 service.
60 @param parser: A parser object for parsing responses from the EC2 service.
60 """61 """
61 def __init__(self, creds=None, endpoint=None, query_factory=None):62 def __init__(self, creds=None, endpoint=None, query_factory=None,
63 parser=None):
62 if creds is None:64 if creds is None:
63 creds = AWSCredentials()65 creds = AWSCredentials()
64 if endpoint is None:66 if endpoint is None:
@@ -66,6 +68,7 @@
66 self.creds = creds68 self.creds = creds
67 self.endpoint = endpoint69 self.endpoint = endpoint
68 self.query_factory = query_factory70 self.query_factory = query_factory
71 self.parser = parser
6972
7073
71class BaseQuery(object):74class BaseQuery(object):
7275
=== modified file 'txaws/client/tests/test_client.py'
--- txaws/client/tests/test_client.py 2011-04-21 16:40:58 +0000
+++ txaws/client/tests/test_client.py 2011-04-26 16:30:59 +0000
@@ -53,10 +53,11 @@
53class BaseClientTestCase(TXAWSTestCase):53class BaseClientTestCase(TXAWSTestCase):
5454
55 def test_creation(self):55 def test_creation(self):
56 client = BaseClient("creds", "endpoint", "query factory")56 client = BaseClient("creds", "endpoint", "query factory", "parser")
57 self.assertEquals(client.creds, "creds")57 self.assertEquals(client.creds, "creds")
58 self.assertEquals(client.endpoint, "endpoint")58 self.assertEquals(client.endpoint, "endpoint")
59 self.assertEquals(client.query_factory, "query factory")59 self.assertEquals(client.query_factory, "query factory")
60 self.assertEquals(client.parser, "parser")
6061
6162
62class BaseQueryTestCase(TXAWSTestCase):63class BaseQueryTestCase(TXAWSTestCase):
6364
=== modified file 'txaws/ec2/client.py'
--- txaws/ec2/client.py 2011-04-21 13:10:37 +0000
+++ txaws/ec2/client.py 2011-04-26 16:30:59 +0000
@@ -16,7 +16,7 @@
16from txaws.util import iso8601time, XML16from txaws.util import iso8601time, XML
1717
1818
19__all__ = ["EC2Client"]19__all__ = ["EC2Client", "Query", "Parser"]
2020
2121
22def ec2_error_wrapper(error):22def ec2_error_wrapper(error):
@@ -26,10 +26,13 @@
26class EC2Client(BaseClient):26class EC2Client(BaseClient):
27 """A client for EC2."""27 """A client for EC2."""
2828
29 def __init__(self, creds=None, endpoint=None, query_factory=None):29 def __init__(self, creds=None, endpoint=None, query_factory=None,
30 parser=None):
30 if query_factory is None:31 if query_factory is None:
31 query_factory = Query32 query_factory = Query
32 super(EC2Client, self).__init__(creds, endpoint, query_factory)33 if parser is None:
34 parser = Parser()
35 super(EC2Client, self).__init__(creds, endpoint, query_factory, parser)
3336
34 def describe_instances(self, *instance_ids):37 def describe_instances(self, *instance_ids):
35 """Describe current instances."""38 """Describe current instances."""
@@ -40,91 +43,7 @@
40 action="DescribeInstances", creds=self.creds,43 action="DescribeInstances", creds=self.creds,
41 endpoint=self.endpoint, other_params=instances)44 endpoint=self.endpoint, other_params=instances)
42 d = query.submit()45 d = query.submit()
43 return d.addCallback(self._parse_describe_instances)46 return d.addCallback(self.parser.describe_instances)
44
45 def _parse_instances_set(self, root, reservation):
46 """Parse instance data out of an XML payload.
47
48 @param root: The root node of the XML payload.
49 @param reservation: The L{Reservation} associated with the instances
50 from the response.
51 @return: A C{list} of L{Instance}s.
52 """
53 instances = []
54 for instance_data in root.find("instancesSet"):
55 instances.append(self._parse_instance(instance_data, reservation))
56 return instances
57
58 def _parse_instance(self, instance_data, reservation):
59 """Parse instance data out of an XML payload.
60
61 @param instance_data: An XML node containing instance data.
62 @param reservation: The L{Reservation} associated with the instance.
63 @return: An L{Instance}.
64 """
65 instance_id = instance_data.findtext("instanceId")
66 instance_state = instance_data.find(
67 "instanceState").findtext("name")
68 instance_type = instance_data.findtext("instanceType")
69 image_id = instance_data.findtext("imageId")
70 private_dns_name = instance_data.findtext("privateDnsName")
71 dns_name = instance_data.findtext("dnsName")
72 key_name = instance_data.findtext("keyName")
73 ami_launch_index = instance_data.findtext("amiLaunchIndex")
74 launch_time = instance_data.findtext("launchTime")
75 placement = instance_data.find("placement").findtext(
76 "availabilityZone")
77 products = []
78 product_codes = instance_data.find("productCodes")
79 if product_codes is not None:
80 for product_data in instance_data.find("productCodes"):
81 products.append(product_data.text)
82 kernel_id = instance_data.findtext("kernelId")
83 ramdisk_id = instance_data.findtext("ramdiskId")
84 instance = model.Instance(
85 instance_id, instance_state, instance_type, image_id,
86 private_dns_name, dns_name, key_name, ami_launch_index,
87 launch_time, placement, products, kernel_id, ramdisk_id,
88 reservation=reservation)
89 return instance
90
91 def _parse_describe_instances(self, xml_bytes):
92 """
93 Parse the reservations XML payload that is returned from an AWS
94 describeInstances API call.
95
96 Instead of returning the reservations as the "top-most" object, we
97 return the object that most developers and their code will be
98 interested in: the instances. In instances reservation is available on
99 the instance object.
100
101 The following instance attributes are optional:
102 * ami_launch_index
103 * key_name
104 * kernel_id
105 * product_codes
106 * ramdisk_id
107 * reason
108 """
109 root = XML(xml_bytes)
110 results = []
111 # May be a more elegant way to do this:
112 for reservation_data in root.find("reservationSet"):
113 # Get the security group information.
114 groups = []
115 for group_data in reservation_data.find("groupSet"):
116 group_id = group_data.findtext("groupId")
117 groups.append(group_id)
118 # Create a reservation object with the parsed data.
119 reservation = model.Reservation(
120 reservation_id=reservation_data.findtext("reservationId"),
121 owner_id=reservation_data.findtext("ownerId"),
122 groups=groups)
123 # Get the list of instances.
124 instances = self._parse_instances_set(
125 reservation_data, reservation)
126 results.extend(instances)
127 return results
12847
129 def run_instances(self, image_id, min_count, max_count,48 def run_instances(self, image_id, min_count, max_count,
130 security_groups=None, key_name=None, instance_type=None,49 security_groups=None, key_name=None, instance_type=None,
@@ -152,27 +71,7 @@
152 action="RunInstances", creds=self.creds, endpoint=self.endpoint,71 action="RunInstances", creds=self.creds, endpoint=self.endpoint,
153 other_params=params)72 other_params=params)
154 d = query.submit()73 d = query.submit()
155 return d.addCallback(self._parse_run_instances)74 return d.addCallback(self.parser.run_instances)
156
157 def _parse_run_instances(self, xml_bytes):
158 """
159 Parse the reservations XML payload that is returned from an AWS
160 RunInstances API call.
161 """
162 root = XML(xml_bytes)
163 # Get the security group information.
164 groups = []
165 for group_data in root.find("groupSet"):
166 group_id = group_data.findtext("groupId")
167 groups.append(group_id)
168 # Create a reservation object with the parsed data.
169 reservation = model.Reservation(
170 reservation_id=root.findtext("reservationId"),
171 owner_id=root.findtext("ownerId"),
172 groups=groups)
173 # Get the list of instances.
174 instances = self._parse_instances_set(root, reservation)
175 return instances
17675
177 def terminate_instances(self, *instance_ids):76 def terminate_instances(self, *instance_ids):
178 """Terminate some instances.77 """Terminate some instances.
@@ -188,20 +87,7 @@
188 action="TerminateInstances", creds=self.creds,87 action="TerminateInstances", creds=self.creds,
189 endpoint=self.endpoint, other_params=instances)88 endpoint=self.endpoint, other_params=instances)
190 d = query.submit()89 d = query.submit()
191 return d.addCallback(self._parse_terminate_instances)90 return d.addCallback(self.parser.terminate_instances)
192
193 def _parse_terminate_instances(self, xml_bytes):
194 root = XML(xml_bytes)
195 result = []
196 # May be a more elegant way to do this:
197 for instance in root.find("instancesSet"):
198 instanceId = instance.findtext("instanceId")
199 previousState = instance.find("previousState").findtext(
200 "name")
201 shutdownState = instance.find("shutdownState").findtext(
202 "name")
203 result.append((instanceId, previousState, shutdownState))
204 return result
20591
206 def describe_security_groups(self, *names):92 def describe_security_groups(self, *names):
207 """Describe security groups.93 """Describe security groups.
@@ -219,50 +105,7 @@
219 action="DescribeSecurityGroups", creds=self.creds,105 action="DescribeSecurityGroups", creds=self.creds,
220 endpoint=self.endpoint, other_params=group_names)106 endpoint=self.endpoint, other_params=group_names)
221 d = query.submit()107 d = query.submit()
222 return d.addCallback(self._parse_describe_security_groups)108 return d.addCallback(self.parser.describe_security_groups)
223
224 def _parse_describe_security_groups(self, xml_bytes):
225 """Parse the XML returned by the C{DescribeSecurityGroups} function.
226
227 @param xml_bytes: XML bytes with a C{DescribeSecurityGroupsResponse}
228 root element.
229 @return: A list of L{SecurityGroup} instances.
230 """
231 root = XML(xml_bytes)
232 result = []
233 for group_info in root.findall("securityGroupInfo/item"):
234 name = group_info.findtext("groupName")
235 description = group_info.findtext("groupDescription")
236 owner_id = group_info.findtext("ownerId")
237 allowed_groups = []
238 allowed_ips = []
239 ip_permissions = group_info.find("ipPermissions")
240 if ip_permissions is None:
241 ip_permissions = ()
242 for ip_permission in ip_permissions:
243 ip_protocol = ip_permission.findtext("ipProtocol")
244 from_port = int(ip_permission.findtext("fromPort"))
245 to_port = int(ip_permission.findtext("toPort"))
246 for groups in ip_permission.findall("groups/item") or ():
247 user_id = groups.findtext("userId")
248 group_name = groups.findtext("groupName")
249 if user_id and group_name:
250 if (user_id, group_name) not in allowed_groups:
251 allowed_groups.append((user_id, group_name))
252 for ip_ranges in ip_permission.findall("ipRanges/item") or ():
253 cidr_ip = ip_ranges.findtext("cidrIp")
254 allowed_ips.append(
255 model.IPPermission(
256 ip_protocol, from_port, to_port, cidr_ip))
257
258 allowed_groups = [model.UserIDGroupPair(user_id, group_name)
259 for user_id, group_name in allowed_groups]
260
261 security_group = model.SecurityGroup(
262 name, description, owner_id=owner_id,
263 groups=allowed_groups, ips=allowed_ips)
264 result.append(security_group)
265 return result
266109
267 def create_security_group(self, name, description):110 def create_security_group(self, name, description):
268 """Create security group.111 """Create security group.
@@ -277,11 +120,7 @@
277 action="CreateSecurityGroup", creds=self.creds,120 action="CreateSecurityGroup", creds=self.creds,
278 endpoint=self.endpoint, other_params=parameters)121 endpoint=self.endpoint, other_params=parameters)
279 d = query.submit()122 d = query.submit()
280 return d.addCallback(self._parse_truth_return)123 return d.addCallback(self.parser.truth_return)
281
282 def _parse_truth_return(self, xml_bytes):
283 root = XML(xml_bytes)
284 return root.findtext("return") == "true"
285124
286 def delete_security_group(self, name):125 def delete_security_group(self, name):
287 """126 """
@@ -294,7 +133,7 @@
294 action="DeleteSecurityGroup", creds=self.creds,133 action="DeleteSecurityGroup", creds=self.creds,
295 endpoint=self.endpoint, other_params=parameter)134 endpoint=self.endpoint, other_params=parameter)
296 d = query.submit()135 d = query.submit()
297 return d.addCallback(self._parse_truth_return)136 return d.addCallback(self.parser.truth_return)
298137
299 def authorize_security_group(138 def authorize_security_group(
300 self, group_name, source_group_name="", source_group_owner_id="",139 self, group_name, source_group_name="", source_group_owner_id="",
@@ -351,7 +190,7 @@
351 action="AuthorizeSecurityGroupIngress", creds=self.creds,190 action="AuthorizeSecurityGroupIngress", creds=self.creds,
352 endpoint=self.endpoint, other_params=parameters)191 endpoint=self.endpoint, other_params=parameters)
353 d = query.submit()192 d = query.submit()
354 return d.addCallback(self._parse_truth_return)193 return d.addCallback(self.parser.truth_return)
355194
356 def authorize_group_permission(195 def authorize_group_permission(
357 self, group_name, source_group_name, source_group_owner_id):196 self, group_name, source_group_name, source_group_owner_id):
@@ -436,7 +275,7 @@
436 action="RevokeSecurityGroupIngress", creds=self.creds,275 action="RevokeSecurityGroupIngress", creds=self.creds,
437 endpoint=self.endpoint, other_params=parameters)276 endpoint=self.endpoint, other_params=parameters)
438 d = query.submit()277 d = query.submit()
439 return d.addCallback(self._parse_truth_return)278 return d.addCallback(self.parser.truth_return)
440279
441 def revoke_group_permission(280 def revoke_group_permission(
442 self, group_name, source_group_name, source_group_owner_id):281 self, group_name, source_group_name, source_group_owner_id):
@@ -475,35 +314,7 @@
475 action="DescribeVolumes", creds=self.creds, endpoint=self.endpoint,314 action="DescribeVolumes", creds=self.creds, endpoint=self.endpoint,
476 other_params=volumeset)315 other_params=volumeset)
477 d = query.submit()316 d = query.submit()
478 return d.addCallback(self._parse_describe_volumes)317 return d.addCallback(self.parser.describe_volumes)
479
480 def _parse_describe_volumes(self, xml_bytes):
481 root = XML(xml_bytes)
482 result = []
483 for volume_data in root.find("volumeSet"):
484 volume_id = volume_data.findtext("volumeId")
485 size = int(volume_data.findtext("size"))
486 status = volume_data.findtext("status")
487 availability_zone = volume_data.findtext("availabilityZone")
488 snapshot_id = volume_data.findtext("snapshotId")
489 create_time = volume_data.findtext("createTime")
490 create_time = datetime.strptime(
491 create_time[:19], "%Y-%m-%dT%H:%M:%S")
492 volume = model.Volume(
493 volume_id, size, status, create_time, availability_zone,
494 snapshot_id)
495 result.append(volume)
496 for attachment_data in volume_data.find("attachmentSet"):
497 instance_id = attachment_data.findtext("instanceId")
498 status = attachment_data.findtext("status")
499 device = attachment_data.findtext("device")
500 attach_time = attachment_data.findtext("attachTime")
501 attach_time = datetime.strptime(
502 attach_time[:19], "%Y-%m-%dT%H:%M:%S")
503 attachment = model.Attachment(
504 instance_id, device, status, attach_time)
505 volume.attachments.append(attachment)
506 return result
507318
508 def create_volume(self, availability_zone, size=None, snapshot_id=None):319 def create_volume(self, availability_zone, size=None, snapshot_id=None):
509 """Create a new volume."""320 """Create a new volume."""
@@ -519,29 +330,14 @@
519 action="CreateVolume", creds=self.creds, endpoint=self.endpoint,330 action="CreateVolume", creds=self.creds, endpoint=self.endpoint,
520 other_params=params)331 other_params=params)
521 d = query.submit()332 d = query.submit()
522 return d.addCallback(self._parse_create_volume)333 return d.addCallback(self.parser.create_volume)
523
524 def _parse_create_volume(self, xml_bytes):
525 root = XML(xml_bytes)
526 volume_id = root.findtext("volumeId")
527 size = int(root.findtext("size"))
528 status = root.findtext("status")
529 create_time = root.findtext("createTime")
530 availability_zone = root.findtext("availabilityZone")
531 snapshot_id = root.findtext("snapshotId")
532 create_time = datetime.strptime(
533 create_time[:19], "%Y-%m-%dT%H:%M:%S")
534 volume = model.Volume(
535 volume_id, size, status, create_time, availability_zone,
536 snapshot_id)
537 return volume
538334
539 def delete_volume(self, volume_id):335 def delete_volume(self, volume_id):
540 query = self.query_factory(336 query = self.query_factory(
541 action="DeleteVolume", creds=self.creds, endpoint=self.endpoint,337 action="DeleteVolume", creds=self.creds, endpoint=self.endpoint,
542 other_params={"VolumeId": volume_id})338 other_params={"VolumeId": volume_id})
543 d = query.submit()339 d = query.submit()
544 return d.addCallback(self._parse_truth_return)340 return d.addCallback(self.parser.truth_return)
545341
546 def describe_snapshots(self, *snapshot_ids):342 def describe_snapshots(self, *snapshot_ids):
547 """Describe available snapshots."""343 """Describe available snapshots."""
@@ -552,24 +348,7 @@
552 action="DescribeSnapshots", creds=self.creds,348 action="DescribeSnapshots", creds=self.creds,
553 endpoint=self.endpoint, other_params=snapshot_set)349 endpoint=self.endpoint, other_params=snapshot_set)
554 d = query.submit()350 d = query.submit()
555 return d.addCallback(self._parse_snapshots)351 return d.addCallback(self.parser.snapshots)
556
557 def _parse_snapshots(self, xml_bytes):
558 root = XML(xml_bytes)
559 result = []
560 for snapshot_data in root.find("snapshotSet"):
561 snapshot_id = snapshot_data.findtext("snapshotId")
562 volume_id = snapshot_data.findtext("volumeId")
563 status = snapshot_data.findtext("status")
564 start_time = snapshot_data.findtext("startTime")
565 start_time = datetime.strptime(
566 start_time[:19], "%Y-%m-%dT%H:%M:%S")
567 progress = snapshot_data.findtext("progress")[:-1]
568 progress = float(progress or "0") / 100.
569 snapshot = model.Snapshot(
570 snapshot_id, volume_id, status, start_time, progress)
571 result.append(snapshot)
572 return result
573352
574 def create_snapshot(self, volume_id):353 def create_snapshot(self, volume_id):
575 """Create a new snapshot of an existing volume."""354 """Create a new snapshot of an existing volume."""
@@ -577,20 +356,7 @@
577 action="CreateSnapshot", creds=self.creds, endpoint=self.endpoint,356 action="CreateSnapshot", creds=self.creds, endpoint=self.endpoint,
578 other_params={"VolumeId": volume_id})357 other_params={"VolumeId": volume_id})
579 d = query.submit()358 d = query.submit()
580 return d.addCallback(self._parse_create_snapshot)359 return d.addCallback(self.parser.create_snapshot)
581
582 def _parse_create_snapshot(self, xml_bytes):
583 root = XML(xml_bytes)
584 snapshot_id = root.findtext("snapshotId")
585 volume_id = root.findtext("volumeId")
586 status = root.findtext("status")
587 start_time = root.findtext("startTime")
588 start_time = datetime.strptime(
589 start_time[:19], "%Y-%m-%dT%H:%M:%S")
590 progress = root.findtext("progress")[:-1]
591 progress = float(progress or "0") / 100.
592 return model.Snapshot(
593 snapshot_id, volume_id, status, start_time, progress)
594360
595 def delete_snapshot(self, snapshot_id):361 def delete_snapshot(self, snapshot_id):
596 """Remove a previously created snapshot."""362 """Remove a previously created snapshot."""
@@ -598,7 +364,7 @@
598 action="DeleteSnapshot", creds=self.creds, endpoint=self.endpoint,364 action="DeleteSnapshot", creds=self.creds, endpoint=self.endpoint,
599 other_params={"SnapshotId": snapshot_id})365 other_params={"SnapshotId": snapshot_id})
600 d = query.submit()366 d = query.submit()
601 return d.addCallback(self._parse_truth_return)367 return d.addCallback(self.parser.truth_return)
602368
603 def attach_volume(self, volume_id, instance_id, device):369 def attach_volume(self, volume_id, instance_id, device):
604 """Attach the given volume to the specified instance at C{device}."""370 """Attach the given volume to the specified instance at C{device}."""
@@ -607,15 +373,7 @@
607 other_params={"VolumeId": volume_id, "InstanceId": instance_id,373 other_params={"VolumeId": volume_id, "InstanceId": instance_id,
608 "Device": device})374 "Device": device})
609 d = query.submit()375 d = query.submit()
610 return d.addCallback(self._parse_attach_volume)376 return d.addCallback(self.parser.attach_volume)
611
612 def _parse_attach_volume(self, xml_bytes):
613 root = XML(xml_bytes)
614 status = root.findtext("status")
615 attach_time = root.findtext("attachTime")
616 attach_time = datetime.strptime(
617 attach_time[:19], "%Y-%m-%dT%H:%M:%S")
618 return {"status": status, "attach_time": attach_time}
619377
620 def describe_keypairs(self, *keypair_names):378 def describe_keypairs(self, *keypair_names):
621 """Returns information about key pairs available."""379 """Returns information about key pairs available."""
@@ -626,19 +384,7 @@
626 action="DescribeKeyPairs", creds=self.creds,384 action="DescribeKeyPairs", creds=self.creds,
627 endpoint=self.endpoint, other_params=keypairs)385 endpoint=self.endpoint, other_params=keypairs)
628 d = query.submit()386 d = query.submit()
629 return d.addCallback(self._parse_describe_keypairs)387 return d.addCallback(self.parser.describe_keypairs)
630
631 def _parse_describe_keypairs(self, xml_bytes):
632 results = []
633 root = XML(xml_bytes)
634 keypairs = root.find("keySet")
635 if keypairs is None:
636 return results
637 for keypair_data in keypairs:
638 key_name = keypair_data.findtext("keyName")
639 key_fingerprint = keypair_data.findtext("keyFingerprint")
640 results.append(model.Keypair(key_name, key_fingerprint))
641 return results
642388
643 def create_keypair(self, keypair_name):389 def create_keypair(self, keypair_name):
644 """390 """
@@ -649,14 +395,7 @@
649 action="CreateKeyPair", creds=self.creds, endpoint=self.endpoint,395 action="CreateKeyPair", creds=self.creds, endpoint=self.endpoint,
650 other_params={"KeyName": keypair_name})396 other_params={"KeyName": keypair_name})
651 d = query.submit()397 d = query.submit()
652 return d.addCallback(self._parse_create_keypair)398 return d.addCallback(self.parser.create_keypair)
653
654 def _parse_create_keypair(self, xml_bytes):
655 keypair_data = XML(xml_bytes)
656 key_name = keypair_data.findtext("keyName")
657 key_fingerprint = keypair_data.findtext("keyFingerprint")
658 key_material = keypair_data.findtext("keyMaterial")
659 return model.Keypair(key_name, key_fingerprint, key_material)
660399
661 def delete_keypair(self, keypair_name):400 def delete_keypair(self, keypair_name):
662 """Delete a given keypair."""401 """Delete a given keypair."""
@@ -664,7 +403,7 @@
664 action="DeleteKeyPair", creds=self.creds, endpoint=self.endpoint,403 action="DeleteKeyPair", creds=self.creds, endpoint=self.endpoint,
665 other_params={"KeyName": keypair_name})404 other_params={"KeyName": keypair_name})
666 d = query.submit()405 d = query.submit()
667 return d.addCallback(self._parse_truth_return)406 return d.addCallback(self.parser.truth_return)
668407
669 def import_keypair(self, keypair_name, key_material):408 def import_keypair(self, keypair_name, key_material):
670 """409 """
@@ -685,14 +424,7 @@
685 other_params={"KeyName": keypair_name,424 other_params={"KeyName": keypair_name,
686 "PublicKeyMaterial": b64encode(key_material)})425 "PublicKeyMaterial": b64encode(key_material)})
687 d = query.submit()426 d = query.submit()
688 return d.addCallback(self._parse_import_keypair, key_material)427 return d.addCallback(self.parser.import_keypair, key_material)
689
690 def _parse_import_keypair(self, xml_bytes, key_material):
691 """Extract the key name and the fingerprint from the result."""
692 keypair_data = XML(xml_bytes)
693 key_name = keypair_data.findtext("keyName")
694 key_fingerprint = keypair_data.findtext("keyFingerprint")
695 return model.Keypair(key_name, key_fingerprint, key_material)
696428
697 def allocate_address(self):429 def allocate_address(self):
698 """430 """
@@ -706,11 +438,7 @@
706 action="AllocateAddress", creds=self.creds, endpoint=self.endpoint,438 action="AllocateAddress", creds=self.creds, endpoint=self.endpoint,
707 other_params={})439 other_params={})
708 d = query.submit()440 d = query.submit()
709 return d.addCallback(self._parse_allocate_address)441 return d.addCallback(self.parser.allocate_address)
710
711 def _parse_allocate_address(self, xml_bytes):
712 address_data = XML(xml_bytes)
713 return address_data.findtext("publicIp")
714442
715 def release_address(self, address):443 def release_address(self, address):
716 """444 """
@@ -722,7 +450,7 @@
722 action="ReleaseAddress", creds=self.creds, endpoint=self.endpoint,450 action="ReleaseAddress", creds=self.creds, endpoint=self.endpoint,
723 other_params={"PublicIp": address})451 other_params={"PublicIp": address})
724 d = query.submit()452 d = query.submit()
725 return d.addCallback(self._parse_truth_return)453 return d.addCallback(self.parser.truth_return)
726454
727 def associate_address(self, instance_id, address):455 def associate_address(self, instance_id, address):
728 """456 """
@@ -736,7 +464,7 @@
736 endpoint=self.endpoint,464 endpoint=self.endpoint,
737 other_params={"InstanceId": instance_id, "PublicIp": address})465 other_params={"InstanceId": instance_id, "PublicIp": address})
738 d = query.submit()466 d = query.submit()
739 return d.addCallback(self._parse_truth_return)467 return d.addCallback(self.parser.truth_return)
740468
741 def disassociate_address(self, address):469 def disassociate_address(self, address):
742 """470 """
@@ -748,7 +476,7 @@
748 action="DisassociateAddress", creds=self.creds,476 action="DisassociateAddress", creds=self.creds,
749 endpoint=self.endpoint, other_params={"PublicIp": address})477 endpoint=self.endpoint, other_params={"PublicIp": address})
750 d = query.submit()478 d = query.submit()
751 return d.addCallback(self._parse_truth_return)479 return d.addCallback(self.parser.truth_return)
752480
753 def describe_addresses(self, *addresses):481 def describe_addresses(self, *addresses):
754 """482 """
@@ -766,16 +494,7 @@
766 action="DescribeAddresses", creds=self.creds,494 action="DescribeAddresses", creds=self.creds,
767 endpoint=self.endpoint, other_params=address_set)495 endpoint=self.endpoint, other_params=address_set)
768 d = query.submit()496 d = query.submit()
769 return d.addCallback(self._parse_describe_addresses)497 return d.addCallback(self.parser.describe_addresses)
770
771 def _parse_describe_addresses(self, xml_bytes):
772 results = []
773 root = XML(xml_bytes)
774 for address_data in root.find("addressesSet"):
775 address = address_data.findtext("publicIp")
776 instance_id = address_data.findtext("instanceId")
777 results.append((address, instance_id))
778 return results
779498
780 def describe_availability_zones(self, names=None):499 def describe_availability_zones(self, names=None):
781 zone_names = None500 zone_names = None
@@ -786,9 +505,372 @@
786 action="DescribeAvailabilityZones", creds=self.creds,505 action="DescribeAvailabilityZones", creds=self.creds,
787 endpoint=self.endpoint, other_params=zone_names)506 endpoint=self.endpoint, other_params=zone_names)
788 d = query.submit()507 d = query.submit()
789 return d.addCallback(self._parse_describe_availability_zones)508 return d.addCallback(self.parser.describe_availability_zones)
790509
791 def _parse_describe_availability_zones(self, xml_bytes):510
511class Parser(object):
512 """A parser for EC2 responses"""
513
514 def instances_set(self, root, reservation):
515 """Parse instance data out of an XML payload.
516
517 @param root: The root node of the XML payload.
518 @param reservation: The L{Reservation} associated with the instances
519 from the response.
520 @return: A C{list} of L{Instance}s.
521 """
522 instances = []
523 for instance_data in root.find("instancesSet"):
524 instances.append(self.instance(instance_data, reservation))
525 return instances
526
527 def instance(self, instance_data, reservation):
528 """Parse instance data out of an XML payload.
529
530 @param instance_data: An XML node containing instance data.
531 @param reservation: The L{Reservation} associated with the instance.
532 @return: An L{Instance}.
533 """
534 instance_id = instance_data.findtext("instanceId")
535 instance_state = instance_data.find(
536 "instanceState").findtext("name")
537 instance_type = instance_data.findtext("instanceType")
538 image_id = instance_data.findtext("imageId")
539 private_dns_name = instance_data.findtext("privateDnsName")
540 dns_name = instance_data.findtext("dnsName")
541 key_name = instance_data.findtext("keyName")
542 ami_launch_index = instance_data.findtext("amiLaunchIndex")
543 launch_time = instance_data.findtext("launchTime")
544 placement = instance_data.find("placement").findtext(
545 "availabilityZone")
546 products = []
547 product_codes = instance_data.find("productCodes")
548 if product_codes is not None:
549 for product_data in instance_data.find("productCodes"):
550 products.append(product_data.text)
551 kernel_id = instance_data.findtext("kernelId")
552 ramdisk_id = instance_data.findtext("ramdiskId")
553 instance = model.Instance(
554 instance_id, instance_state, instance_type, image_id,
555 private_dns_name, dns_name, key_name, ami_launch_index,
556 launch_time, placement, products, kernel_id, ramdisk_id,
557 reservation=reservation)
558 return instance
559
560 def describe_instances(self, xml_bytes):
561 """
562 Parse the reservations XML payload that is returned from an AWS
563 describeInstances API call.
564
565 Instead of returning the reservations as the "top-most" object, we
566 return the object that most developers and their code will be
567 interested in: the instances. In instances reservation is available on
568 the instance object.
569
570 The following instance attributes are optional:
571 * ami_launch_index
572 * key_name
573 * kernel_id
574 * product_codes
575 * ramdisk_id
576 * reason
577
578 @param xml_bytes: raw XML payload from AWS.
579 """
580 root = XML(xml_bytes)
581 results = []
582 # May be a more elegant way to do this:
583 for reservation_data in root.find("reservationSet"):
584 # Get the security group information.
585 groups = []
586 for group_data in reservation_data.find("groupSet"):
587 group_id = group_data.findtext("groupId")
588 groups.append(group_id)
589 # Create a reservation object with the parsed data.
590 reservation = model.Reservation(
591 reservation_id=reservation_data.findtext("reservationId"),
592 owner_id=reservation_data.findtext("ownerId"),
593 groups=groups)
594 # Get the list of instances.
595 instances = self.instances_set(
596 reservation_data, reservation)
597 results.extend(instances)
598 return results
599
600 def run_instances(self, xml_bytes):
601 """
602 Parse the reservations XML payload that is returned from an AWS
603 RunInstances API call.
604
605 @param xml_bytes: raw XML payload from AWS.
606 """
607 root = XML(xml_bytes)
608 # Get the security group information.
609 groups = []
610 for group_data in root.find("groupSet"):
611 group_id = group_data.findtext("groupId")
612 groups.append(group_id)
613 # Create a reservation object with the parsed data.
614 reservation = model.Reservation(
615 reservation_id=root.findtext("reservationId"),
616 owner_id=root.findtext("ownerId"),
617 groups=groups)
618 # Get the list of instances.
619 instances = self.instances_set(root, reservation)
620 return instances
621
622 def terminate_instances(self, xml_bytes):
623 """Parse the XML returned by the C{TerminateInstances} function.
624
625 @param xml_bytes: XML bytes with a C{TerminateInstancesResponse} root
626 element.
627 @return: An iterable of C{tuple} of (instanceId, previousState,
628 shutdownState) for the ec2 instances that where terminated.
629 """
630 root = XML(xml_bytes)
631 result = []
632 # May be a more elegant way to do this:
633 for instance in root.find("instancesSet"):
634 instanceId = instance.findtext("instanceId")
635 previousState = instance.find("previousState").findtext(
636 "name")
637 shutdownState = instance.find("shutdownState").findtext(
638 "name")
639 result.append((instanceId, previousState, shutdownState))
640 return result
641
642 def describe_security_groups(self, xml_bytes):
643 """Parse the XML returned by the C{DescribeSecurityGroups} function.
644
645 @param xml_bytes: XML bytes with a C{DescribeSecurityGroupsResponse}
646 root element.
647 @return: A list of L{SecurityGroup} instances.
648 """
649 root = XML(xml_bytes)
650 result = []
651 for group_info in root.findall("securityGroupInfo/item"):
652 name = group_info.findtext("groupName")
653 description = group_info.findtext("groupDescription")
654 owner_id = group_info.findtext("ownerId")
655 allowed_groups = []
656 allowed_ips = []
657 ip_permissions = group_info.find("ipPermissions")
658 if ip_permissions is None:
659 ip_permissions = ()
660 for ip_permission in ip_permissions:
661 ip_protocol = ip_permission.findtext("ipProtocol")
662 from_port = int(ip_permission.findtext("fromPort"))
663 to_port = int(ip_permission.findtext("toPort"))
664 for groups in ip_permission.findall("groups/item") or ():
665 user_id = groups.findtext("userId")
666 group_name = groups.findtext("groupName")
667 if user_id and group_name:
668 if (user_id, group_name) not in allowed_groups:
669 allowed_groups.append((user_id, group_name))
670 for ip_ranges in ip_permission.findall("ipRanges/item") or ():
671 cidr_ip = ip_ranges.findtext("cidrIp")
672 allowed_ips.append(
673 model.IPPermission(
674 ip_protocol, from_port, to_port, cidr_ip))
675
676 allowed_groups = [model.UserIDGroupPair(user_id, group_name)
677 for user_id, group_name in allowed_groups]
678
679 security_group = model.SecurityGroup(
680 name, description, owner_id=owner_id,
681 groups=allowed_groups, ips=allowed_ips)
682 result.append(security_group)
683 return result
684
685 def truth_return(self, xml_bytes):
686 """Parse the XML for a truth value.
687
688 @param xml_bytes: XML bytes.
689 @return: True if the node contains "return" otherwise False.
690 """
691 root = XML(xml_bytes)
692 return root.findtext("return") == "true"
693
694 def describe_volumes(self, xml_bytes):
695 """Parse the XML returned by the C{DescribeVolumes} function.
696
697 @param xml_bytes: XML bytes with a C{DescribeVolumesResponse} root
698 element.
699 @return: A list of L{Volume} instances.
700 """
701 root = XML(xml_bytes)
702 result = []
703 for volume_data in root.find("volumeSet"):
704 volume_id = volume_data.findtext("volumeId")
705 size = int(volume_data.findtext("size"))
706 status = volume_data.findtext("status")
707 availability_zone = volume_data.findtext("availabilityZone")
708 snapshot_id = volume_data.findtext("snapshotId")
709 create_time = volume_data.findtext("createTime")
710 create_time = datetime.strptime(
711 create_time[:19], "%Y-%m-%dT%H:%M:%S")
712 volume = model.Volume(
713 volume_id, size, status, create_time, availability_zone,
714 snapshot_id)
715 result.append(volume)
716 for attachment_data in volume_data.find("attachmentSet"):
717 instance_id = attachment_data.findtext("instanceId")
718 status = attachment_data.findtext("status")
719 device = attachment_data.findtext("device")
720 attach_time = attachment_data.findtext("attachTime")
721 attach_time = datetime.strptime(
722 attach_time[:19], "%Y-%m-%dT%H:%M:%S")
723 attachment = model.Attachment(
724 instance_id, device, status, attach_time)
725 volume.attachments.append(attachment)
726 return result
727
728 def create_volume(self, xml_bytes):
729 """Parse the XML returned by the C{CreateVolume} function.
730
731 @param xml_bytes: XML bytes with a C{CreateVolumeResponse} root
732 element.
733 @return: The L{Volume} instance created.
734 """
735 root = XML(xml_bytes)
736 volume_id = root.findtext("volumeId")
737 size = int(root.findtext("size"))
738 status = root.findtext("status")
739 create_time = root.findtext("createTime")
740 availability_zone = root.findtext("availabilityZone")
741 snapshot_id = root.findtext("snapshotId")
742 create_time = datetime.strptime(
743 create_time[:19], "%Y-%m-%dT%H:%M:%S")
744 volume = model.Volume(
745 volume_id, size, status, create_time, availability_zone,
746 snapshot_id)
747 return volume
748
749 def snapshots(self, xml_bytes):
750 """Parse the XML returned by the C{DescribeSnapshots} function.
751
752 @param xml_bytes: XML bytes with a C{DescribeSnapshotsResponse} root
753 element.
754 @return: A list of L{Snapshot} instances.
755 """
756 root = XML(xml_bytes)
757 result = []
758 for snapshot_data in root.find("snapshotSet"):
759 snapshot_id = snapshot_data.findtext("snapshotId")
760 volume_id = snapshot_data.findtext("volumeId")
761 status = snapshot_data.findtext("status")
762 start_time = snapshot_data.findtext("startTime")
763 start_time = datetime.strptime(
764 start_time[:19], "%Y-%m-%dT%H:%M:%S")
765 progress = snapshot_data.findtext("progress")[:-1]
766 progress = float(progress or "0") / 100.
767 snapshot = model.Snapshot(
768 snapshot_id, volume_id, status, start_time, progress)
769 result.append(snapshot)
770 return result
771
772 def create_snapshot(self, xml_bytes):
773 """Parse the XML returned by the C{CreateSnapshot} function.
774
775 @param xml_bytes: XML bytes with a C{CreateSnapshotResponse} root
776 element.
777 @return: The L{Snapshot} instance created.
778 """
779 root = XML(xml_bytes)
780 snapshot_id = root.findtext("snapshotId")
781 volume_id = root.findtext("volumeId")
782 status = root.findtext("status")
783 start_time = root.findtext("startTime")
784 start_time = datetime.strptime(
785 start_time[:19], "%Y-%m-%dT%H:%M:%S")
786 progress = root.findtext("progress")[:-1]
787 progress = float(progress or "0") / 100.
788 return model.Snapshot(
789 snapshot_id, volume_id, status, start_time, progress)
790
791 def attach_volume(self, xml_bytes):
792 """Parse the XML returned by the C{AttachVolume} function.
793
794 @param xml_bytes: XML bytes with a C{AttachVolumeResponse} root
795 element.
796 @return: a C{dict} with status and attach_time keys.
797 """
798 root = XML(xml_bytes)
799 status = root.findtext("status")
800 attach_time = root.findtext("attachTime")
801 attach_time = datetime.strptime(
802 attach_time[:19], "%Y-%m-%dT%H:%M:%S")
803 return {"status": status, "attach_time": attach_time}
804
805 def describe_keypairs(self, xml_bytes):
806 """Parse the XML returned by the C{DescribeKeyPairs} function.
807
808 @param xml_bytes: XML bytes with a C{DescribeKeyPairsResponse} root
809 element.
810 @return: a C{list} of L{Keypair}.
811 """
812 results = []
813 root = XML(xml_bytes)
814 keypairs = root.find("keySet")
815 if keypairs is None:
816 return results
817 for keypair_data in keypairs:
818 key_name = keypair_data.findtext("keyName")
819 key_fingerprint = keypair_data.findtext("keyFingerprint")
820 results.append(model.Keypair(key_name, key_fingerprint))
821 return results
822
823 def create_keypair(self, xml_bytes):
824 """Parse the XML returned by the C{CreateKeyPair} function.
825
826 @param xml_bytes: XML bytes with a C{CreateKeyPairResponse} root
827 element.
828 @return: The L{Keypair} instance created.
829 """
830 keypair_data = XML(xml_bytes)
831 key_name = keypair_data.findtext("keyName")
832 key_fingerprint = keypair_data.findtext("keyFingerprint")
833 key_material = keypair_data.findtext("keyMaterial")
834 return model.Keypair(key_name, key_fingerprint, key_material)
835
836 def import_keypair(self, xml_bytes, key_material):
837 """Extract the key name and the fingerprint from the result."""
838 keypair_data = XML(xml_bytes)
839 key_name = keypair_data.findtext("keyName")
840 key_fingerprint = keypair_data.findtext("keyFingerprint")
841 return model.Keypair(key_name, key_fingerprint, key_material)
842
843 def allocate_address(self, xml_bytes):
844 """Parse the XML returned by the C{AllocateAddress} function.
845
846 @param xml_bytes: XML bytes with a C{AllocateAddress} root element.
847 @return: The public ip address as a string.
848 """
849 address_data = XML(xml_bytes)
850 return address_data.findtext("publicIp")
851
852 def describe_addresses(self, xml_bytes):
853 """Parse the XML returned by the C{DescribeAddresses} function.
854
855 @param xml_bytes: XML bytes with a C{DescribeAddressesResponse} root
856 element.
857 @return: a C{list} of L{tuple} of (publicIp, instancId).
858 """
859 results = []
860 root = XML(xml_bytes)
861 for address_data in root.find("addressesSet"):
862 address = address_data.findtext("publicIp")
863 instance_id = address_data.findtext("instanceId")
864 results.append((address, instance_id))
865 return results
866
867 def describe_availability_zones(self, xml_bytes):
868 """Parse the XML returned by the C{DescribeAvailibilityZones} function.
869
870 @param xml_bytes: XML bytes with a C{DescribeAvailibilityZonesResponse}
871 root element.
872 @return: a C{list} of L{AvailabilityZone}.
873 """
792 results = []874 results = []
793 root = XML(xml_bytes)875 root = XML(xml_bytes)
794 for zone_data in root.find("availabilityZoneInfo"):876 for zone_data in root.find("availabilityZoneInfo"):
795877
=== modified file 'txaws/ec2/tests/test_client.py'
--- txaws/ec2/tests/test_client.py 2011-04-21 16:14:58 +0000
+++ txaws/ec2/tests/test_client.py 2011-04-26 16:30:59 +0000
@@ -217,7 +217,7 @@
217 def test_parse_reservation(self):217 def test_parse_reservation(self):
218 creds = AWSCredentials("foo", "bar")218 creds = AWSCredentials("foo", "bar")
219 ec2 = client.EC2Client(creds=creds)219 ec2 = client.EC2Client(creds=creds)
220 results = ec2._parse_describe_instances(220 results = ec2.parser.describe_instances(
221 payload.sample_describe_instances_result)221 payload.sample_describe_instances_result)
222 self.check_parsed_instances(results)222 self.check_parsed_instances(results)
223223
224224
=== modified file 'txaws/testing/ec2.py'
--- txaws/testing/ec2.py 2011-04-19 16:26:26 +0000
+++ txaws/testing/ec2.py 2011-04-26 16:30:59 +0000
@@ -16,11 +16,12 @@
16 def __init__(self, creds, endpoint, instances=None, keypairs=None,16 def __init__(self, creds, endpoint, instances=None, keypairs=None,
17 volumes=None, key_material="", security_groups=None,17 volumes=None, key_material="", security_groups=None,
18 snapshots=None, addresses=None, availability_zones=None,18 snapshots=None, addresses=None, availability_zones=None,
19 query_factory=None):19 query_factory=None, parser=None):
2020
21 self.creds = creds21 self.creds = creds
22 self.endpoint = endpoint22 self.endpoint = endpoint
23 self.query_factory = query_factory23 self.query_factory = query_factory
24 self.parser = parser
2425
25 self.instances = instances or []26 self.instances = instances or []
26 self.keypairs = keypairs or []27 self.keypairs = keypairs or []

Subscribers

People subscribed via source and target branches