Merge lp:~fwierzbicki/txaws/break-out-ec2-parser into lp:txaws
- break-out-ec2-parser
- Merge into trunk
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 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Thomas Herve | Approve | ||
Duncan McGreggor | Approve | ||
Review via email: mp+58623@code.launchpad.net |
Commit message
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.
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.
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!
- 88. By Frank Wierzbicki
-
Update __all__
Preview Diff
1 | === modified file 'txaws/client/base.py' | |||
2 | --- txaws/client/base.py 2011-04-21 14:59:23 +0000 | |||
3 | +++ txaws/client/base.py 2011-04-26 16:30:59 +0000 | |||
4 | @@ -57,8 +57,10 @@ | |||
5 | 57 | @param endpoint: The service endpoint URI. | 57 | @param endpoint: The service endpoint URI. |
6 | 58 | @param query_factory: The class or function that produces a query | 58 | @param query_factory: The class or function that produces a query |
7 | 59 | object for making requests to the EC2 service. | 59 | object for making requests to the EC2 service. |
8 | 60 | @param parser: A parser object for parsing responses from the EC2 service. | ||
9 | 60 | """ | 61 | """ |
11 | 61 | def __init__(self, creds=None, endpoint=None, query_factory=None): | 62 | def __init__(self, creds=None, endpoint=None, query_factory=None, |
12 | 63 | parser=None): | ||
13 | 62 | if creds is None: | 64 | if creds is None: |
14 | 63 | creds = AWSCredentials() | 65 | creds = AWSCredentials() |
15 | 64 | if endpoint is None: | 66 | if endpoint is None: |
16 | @@ -66,6 +68,7 @@ | |||
17 | 66 | self.creds = creds | 68 | self.creds = creds |
18 | 67 | self.endpoint = endpoint | 69 | self.endpoint = endpoint |
19 | 68 | self.query_factory = query_factory | 70 | self.query_factory = query_factory |
20 | 71 | self.parser = parser | ||
21 | 69 | 72 | ||
22 | 70 | 73 | ||
23 | 71 | class BaseQuery(object): | 74 | class BaseQuery(object): |
24 | 72 | 75 | ||
25 | === modified file 'txaws/client/tests/test_client.py' | |||
26 | --- txaws/client/tests/test_client.py 2011-04-21 16:40:58 +0000 | |||
27 | +++ txaws/client/tests/test_client.py 2011-04-26 16:30:59 +0000 | |||
28 | @@ -53,10 +53,11 @@ | |||
29 | 53 | class BaseClientTestCase(TXAWSTestCase): | 53 | class BaseClientTestCase(TXAWSTestCase): |
30 | 54 | 54 | ||
31 | 55 | def test_creation(self): | 55 | def test_creation(self): |
33 | 56 | client = BaseClient("creds", "endpoint", "query factory") | 56 | client = BaseClient("creds", "endpoint", "query factory", "parser") |
34 | 57 | self.assertEquals(client.creds, "creds") | 57 | self.assertEquals(client.creds, "creds") |
35 | 58 | self.assertEquals(client.endpoint, "endpoint") | 58 | self.assertEquals(client.endpoint, "endpoint") |
36 | 59 | self.assertEquals(client.query_factory, "query factory") | 59 | self.assertEquals(client.query_factory, "query factory") |
37 | 60 | self.assertEquals(client.parser, "parser") | ||
38 | 60 | 61 | ||
39 | 61 | 62 | ||
40 | 62 | class BaseQueryTestCase(TXAWSTestCase): | 63 | class BaseQueryTestCase(TXAWSTestCase): |
41 | 63 | 64 | ||
42 | === modified file 'txaws/ec2/client.py' | |||
43 | --- txaws/ec2/client.py 2011-04-21 13:10:37 +0000 | |||
44 | +++ txaws/ec2/client.py 2011-04-26 16:30:59 +0000 | |||
45 | @@ -16,7 +16,7 @@ | |||
46 | 16 | from txaws.util import iso8601time, XML | 16 | from txaws.util import iso8601time, XML |
47 | 17 | 17 | ||
48 | 18 | 18 | ||
50 | 19 | __all__ = ["EC2Client"] | 19 | __all__ = ["EC2Client", "Query", "Parser"] |
51 | 20 | 20 | ||
52 | 21 | 21 | ||
53 | 22 | def ec2_error_wrapper(error): | 22 | def ec2_error_wrapper(error): |
54 | @@ -26,10 +26,13 @@ | |||
55 | 26 | class EC2Client(BaseClient): | 26 | class EC2Client(BaseClient): |
56 | 27 | """A client for EC2.""" | 27 | """A client for EC2.""" |
57 | 28 | 28 | ||
59 | 29 | def __init__(self, creds=None, endpoint=None, query_factory=None): | 29 | def __init__(self, creds=None, endpoint=None, query_factory=None, |
60 | 30 | parser=None): | ||
61 | 30 | if query_factory is None: | 31 | if query_factory is None: |
62 | 31 | query_factory = Query | 32 | query_factory = Query |
64 | 32 | super(EC2Client, self).__init__(creds, endpoint, query_factory) | 33 | if parser is None: |
65 | 34 | parser = Parser() | ||
66 | 35 | super(EC2Client, self).__init__(creds, endpoint, query_factory, parser) | ||
67 | 33 | 36 | ||
68 | 34 | def describe_instances(self, *instance_ids): | 37 | def describe_instances(self, *instance_ids): |
69 | 35 | """Describe current instances.""" | 38 | """Describe current instances.""" |
70 | @@ -40,91 +43,7 @@ | |||
71 | 40 | action="DescribeInstances", creds=self.creds, | 43 | action="DescribeInstances", creds=self.creds, |
72 | 41 | endpoint=self.endpoint, other_params=instances) | 44 | endpoint=self.endpoint, other_params=instances) |
73 | 42 | d = query.submit() | 45 | d = query.submit() |
159 | 43 | return d.addCallback(self._parse_describe_instances) | 46 | return d.addCallback(self.parser.describe_instances) |
75 | 44 | |||
76 | 45 | def _parse_instances_set(self, root, reservation): | ||
77 | 46 | """Parse instance data out of an XML payload. | ||
78 | 47 | |||
79 | 48 | @param root: The root node of the XML payload. | ||
80 | 49 | @param reservation: The L{Reservation} associated with the instances | ||
81 | 50 | from the response. | ||
82 | 51 | @return: A C{list} of L{Instance}s. | ||
83 | 52 | """ | ||
84 | 53 | instances = [] | ||
85 | 54 | for instance_data in root.find("instancesSet"): | ||
86 | 55 | instances.append(self._parse_instance(instance_data, reservation)) | ||
87 | 56 | return instances | ||
88 | 57 | |||
89 | 58 | def _parse_instance(self, instance_data, reservation): | ||
90 | 59 | """Parse instance data out of an XML payload. | ||
91 | 60 | |||
92 | 61 | @param instance_data: An XML node containing instance data. | ||
93 | 62 | @param reservation: The L{Reservation} associated with the instance. | ||
94 | 63 | @return: An L{Instance}. | ||
95 | 64 | """ | ||
96 | 65 | instance_id = instance_data.findtext("instanceId") | ||
97 | 66 | instance_state = instance_data.find( | ||
98 | 67 | "instanceState").findtext("name") | ||
99 | 68 | instance_type = instance_data.findtext("instanceType") | ||
100 | 69 | image_id = instance_data.findtext("imageId") | ||
101 | 70 | private_dns_name = instance_data.findtext("privateDnsName") | ||
102 | 71 | dns_name = instance_data.findtext("dnsName") | ||
103 | 72 | key_name = instance_data.findtext("keyName") | ||
104 | 73 | ami_launch_index = instance_data.findtext("amiLaunchIndex") | ||
105 | 74 | launch_time = instance_data.findtext("launchTime") | ||
106 | 75 | placement = instance_data.find("placement").findtext( | ||
107 | 76 | "availabilityZone") | ||
108 | 77 | products = [] | ||
109 | 78 | product_codes = instance_data.find("productCodes") | ||
110 | 79 | if product_codes is not None: | ||
111 | 80 | for product_data in instance_data.find("productCodes"): | ||
112 | 81 | products.append(product_data.text) | ||
113 | 82 | kernel_id = instance_data.findtext("kernelId") | ||
114 | 83 | ramdisk_id = instance_data.findtext("ramdiskId") | ||
115 | 84 | instance = model.Instance( | ||
116 | 85 | instance_id, instance_state, instance_type, image_id, | ||
117 | 86 | private_dns_name, dns_name, key_name, ami_launch_index, | ||
118 | 87 | launch_time, placement, products, kernel_id, ramdisk_id, | ||
119 | 88 | reservation=reservation) | ||
120 | 89 | return instance | ||
121 | 90 | |||
122 | 91 | def _parse_describe_instances(self, xml_bytes): | ||
123 | 92 | """ | ||
124 | 93 | Parse the reservations XML payload that is returned from an AWS | ||
125 | 94 | describeInstances API call. | ||
126 | 95 | |||
127 | 96 | Instead of returning the reservations as the "top-most" object, we | ||
128 | 97 | return the object that most developers and their code will be | ||
129 | 98 | interested in: the instances. In instances reservation is available on | ||
130 | 99 | the instance object. | ||
131 | 100 | |||
132 | 101 | The following instance attributes are optional: | ||
133 | 102 | * ami_launch_index | ||
134 | 103 | * key_name | ||
135 | 104 | * kernel_id | ||
136 | 105 | * product_codes | ||
137 | 106 | * ramdisk_id | ||
138 | 107 | * reason | ||
139 | 108 | """ | ||
140 | 109 | root = XML(xml_bytes) | ||
141 | 110 | results = [] | ||
142 | 111 | # May be a more elegant way to do this: | ||
143 | 112 | for reservation_data in root.find("reservationSet"): | ||
144 | 113 | # Get the security group information. | ||
145 | 114 | groups = [] | ||
146 | 115 | for group_data in reservation_data.find("groupSet"): | ||
147 | 116 | group_id = group_data.findtext("groupId") | ||
148 | 117 | groups.append(group_id) | ||
149 | 118 | # Create a reservation object with the parsed data. | ||
150 | 119 | reservation = model.Reservation( | ||
151 | 120 | reservation_id=reservation_data.findtext("reservationId"), | ||
152 | 121 | owner_id=reservation_data.findtext("ownerId"), | ||
153 | 122 | groups=groups) | ||
154 | 123 | # Get the list of instances. | ||
155 | 124 | instances = self._parse_instances_set( | ||
156 | 125 | reservation_data, reservation) | ||
157 | 126 | results.extend(instances) | ||
158 | 127 | return results | ||
160 | 128 | 47 | ||
161 | 129 | def run_instances(self, image_id, min_count, max_count, | 48 | def run_instances(self, image_id, min_count, max_count, |
162 | 130 | security_groups=None, key_name=None, instance_type=None, | 49 | security_groups=None, key_name=None, instance_type=None, |
163 | @@ -152,27 +71,7 @@ | |||
164 | 152 | action="RunInstances", creds=self.creds, endpoint=self.endpoint, | 71 | action="RunInstances", creds=self.creds, endpoint=self.endpoint, |
165 | 153 | other_params=params) | 72 | other_params=params) |
166 | 154 | d = query.submit() | 73 | d = query.submit() |
188 | 155 | return d.addCallback(self._parse_run_instances) | 74 | return d.addCallback(self.parser.run_instances) |
168 | 156 | |||
169 | 157 | def _parse_run_instances(self, xml_bytes): | ||
170 | 158 | """ | ||
171 | 159 | Parse the reservations XML payload that is returned from an AWS | ||
172 | 160 | RunInstances API call. | ||
173 | 161 | """ | ||
174 | 162 | root = XML(xml_bytes) | ||
175 | 163 | # Get the security group information. | ||
176 | 164 | groups = [] | ||
177 | 165 | for group_data in root.find("groupSet"): | ||
178 | 166 | group_id = group_data.findtext("groupId") | ||
179 | 167 | groups.append(group_id) | ||
180 | 168 | # Create a reservation object with the parsed data. | ||
181 | 169 | reservation = model.Reservation( | ||
182 | 170 | reservation_id=root.findtext("reservationId"), | ||
183 | 171 | owner_id=root.findtext("ownerId"), | ||
184 | 172 | groups=groups) | ||
185 | 173 | # Get the list of instances. | ||
186 | 174 | instances = self._parse_instances_set(root, reservation) | ||
187 | 175 | return instances | ||
189 | 176 | 75 | ||
190 | 177 | def terminate_instances(self, *instance_ids): | 76 | def terminate_instances(self, *instance_ids): |
191 | 178 | """Terminate some instances. | 77 | """Terminate some instances. |
192 | @@ -188,20 +87,7 @@ | |||
193 | 188 | action="TerminateInstances", creds=self.creds, | 87 | action="TerminateInstances", creds=self.creds, |
194 | 189 | endpoint=self.endpoint, other_params=instances) | 88 | endpoint=self.endpoint, other_params=instances) |
195 | 190 | d = query.submit() | 89 | d = query.submit() |
210 | 191 | return d.addCallback(self._parse_terminate_instances) | 90 | return d.addCallback(self.parser.terminate_instances) |
197 | 192 | |||
198 | 193 | def _parse_terminate_instances(self, xml_bytes): | ||
199 | 194 | root = XML(xml_bytes) | ||
200 | 195 | result = [] | ||
201 | 196 | # May be a more elegant way to do this: | ||
202 | 197 | for instance in root.find("instancesSet"): | ||
203 | 198 | instanceId = instance.findtext("instanceId") | ||
204 | 199 | previousState = instance.find("previousState").findtext( | ||
205 | 200 | "name") | ||
206 | 201 | shutdownState = instance.find("shutdownState").findtext( | ||
207 | 202 | "name") | ||
208 | 203 | result.append((instanceId, previousState, shutdownState)) | ||
209 | 204 | return result | ||
211 | 205 | 91 | ||
212 | 206 | def describe_security_groups(self, *names): | 92 | def describe_security_groups(self, *names): |
213 | 207 | """Describe security groups. | 93 | """Describe security groups. |
214 | @@ -219,50 +105,7 @@ | |||
215 | 219 | action="DescribeSecurityGroups", creds=self.creds, | 105 | action="DescribeSecurityGroups", creds=self.creds, |
216 | 220 | endpoint=self.endpoint, other_params=group_names) | 106 | endpoint=self.endpoint, other_params=group_names) |
217 | 221 | d = query.submit() | 107 | d = query.submit() |
262 | 222 | return d.addCallback(self._parse_describe_security_groups) | 108 | return d.addCallback(self.parser.describe_security_groups) |
219 | 223 | |||
220 | 224 | def _parse_describe_security_groups(self, xml_bytes): | ||
221 | 225 | """Parse the XML returned by the C{DescribeSecurityGroups} function. | ||
222 | 226 | |||
223 | 227 | @param xml_bytes: XML bytes with a C{DescribeSecurityGroupsResponse} | ||
224 | 228 | root element. | ||
225 | 229 | @return: A list of L{SecurityGroup} instances. | ||
226 | 230 | """ | ||
227 | 231 | root = XML(xml_bytes) | ||
228 | 232 | result = [] | ||
229 | 233 | for group_info in root.findall("securityGroupInfo/item"): | ||
230 | 234 | name = group_info.findtext("groupName") | ||
231 | 235 | description = group_info.findtext("groupDescription") | ||
232 | 236 | owner_id = group_info.findtext("ownerId") | ||
233 | 237 | allowed_groups = [] | ||
234 | 238 | allowed_ips = [] | ||
235 | 239 | ip_permissions = group_info.find("ipPermissions") | ||
236 | 240 | if ip_permissions is None: | ||
237 | 241 | ip_permissions = () | ||
238 | 242 | for ip_permission in ip_permissions: | ||
239 | 243 | ip_protocol = ip_permission.findtext("ipProtocol") | ||
240 | 244 | from_port = int(ip_permission.findtext("fromPort")) | ||
241 | 245 | to_port = int(ip_permission.findtext("toPort")) | ||
242 | 246 | for groups in ip_permission.findall("groups/item") or (): | ||
243 | 247 | user_id = groups.findtext("userId") | ||
244 | 248 | group_name = groups.findtext("groupName") | ||
245 | 249 | if user_id and group_name: | ||
246 | 250 | if (user_id, group_name) not in allowed_groups: | ||
247 | 251 | allowed_groups.append((user_id, group_name)) | ||
248 | 252 | for ip_ranges in ip_permission.findall("ipRanges/item") or (): | ||
249 | 253 | cidr_ip = ip_ranges.findtext("cidrIp") | ||
250 | 254 | allowed_ips.append( | ||
251 | 255 | model.IPPermission( | ||
252 | 256 | ip_protocol, from_port, to_port, cidr_ip)) | ||
253 | 257 | |||
254 | 258 | allowed_groups = [model.UserIDGroupPair(user_id, group_name) | ||
255 | 259 | for user_id, group_name in allowed_groups] | ||
256 | 260 | |||
257 | 261 | security_group = model.SecurityGroup( | ||
258 | 262 | name, description, owner_id=owner_id, | ||
259 | 263 | groups=allowed_groups, ips=allowed_ips) | ||
260 | 264 | result.append(security_group) | ||
261 | 265 | return result | ||
263 | 266 | 109 | ||
264 | 267 | def create_security_group(self, name, description): | 110 | def create_security_group(self, name, description): |
265 | 268 | """Create security group. | 111 | """Create security group. |
266 | @@ -277,11 +120,7 @@ | |||
267 | 277 | action="CreateSecurityGroup", creds=self.creds, | 120 | action="CreateSecurityGroup", creds=self.creds, |
268 | 278 | endpoint=self.endpoint, other_params=parameters) | 121 | endpoint=self.endpoint, other_params=parameters) |
269 | 279 | d = query.submit() | 122 | d = query.submit() |
275 | 280 | return d.addCallback(self._parse_truth_return) | 123 | return d.addCallback(self.parser.truth_return) |
271 | 281 | |||
272 | 282 | def _parse_truth_return(self, xml_bytes): | ||
273 | 283 | root = XML(xml_bytes) | ||
274 | 284 | return root.findtext("return") == "true" | ||
276 | 285 | 124 | ||
277 | 286 | def delete_security_group(self, name): | 125 | def delete_security_group(self, name): |
278 | 287 | """ | 126 | """ |
279 | @@ -294,7 +133,7 @@ | |||
280 | 294 | action="DeleteSecurityGroup", creds=self.creds, | 133 | action="DeleteSecurityGroup", creds=self.creds, |
281 | 295 | endpoint=self.endpoint, other_params=parameter) | 134 | endpoint=self.endpoint, other_params=parameter) |
282 | 296 | d = query.submit() | 135 | d = query.submit() |
284 | 297 | return d.addCallback(self._parse_truth_return) | 136 | return d.addCallback(self.parser.truth_return) |
285 | 298 | 137 | ||
286 | 299 | def authorize_security_group( | 138 | def authorize_security_group( |
287 | 300 | self, group_name, source_group_name="", source_group_owner_id="", | 139 | self, group_name, source_group_name="", source_group_owner_id="", |
288 | @@ -351,7 +190,7 @@ | |||
289 | 351 | action="AuthorizeSecurityGroupIngress", creds=self.creds, | 190 | action="AuthorizeSecurityGroupIngress", creds=self.creds, |
290 | 352 | endpoint=self.endpoint, other_params=parameters) | 191 | endpoint=self.endpoint, other_params=parameters) |
291 | 353 | d = query.submit() | 192 | d = query.submit() |
293 | 354 | return d.addCallback(self._parse_truth_return) | 193 | return d.addCallback(self.parser.truth_return) |
294 | 355 | 194 | ||
295 | 356 | def authorize_group_permission( | 195 | def authorize_group_permission( |
296 | 357 | self, group_name, source_group_name, source_group_owner_id): | 196 | self, group_name, source_group_name, source_group_owner_id): |
297 | @@ -436,7 +275,7 @@ | |||
298 | 436 | action="RevokeSecurityGroupIngress", creds=self.creds, | 275 | action="RevokeSecurityGroupIngress", creds=self.creds, |
299 | 437 | endpoint=self.endpoint, other_params=parameters) | 276 | endpoint=self.endpoint, other_params=parameters) |
300 | 438 | d = query.submit() | 277 | d = query.submit() |
302 | 439 | return d.addCallback(self._parse_truth_return) | 278 | return d.addCallback(self.parser.truth_return) |
303 | 440 | 279 | ||
304 | 441 | def revoke_group_permission( | 280 | def revoke_group_permission( |
305 | 442 | self, group_name, source_group_name, source_group_owner_id): | 281 | self, group_name, source_group_name, source_group_owner_id): |
306 | @@ -475,35 +314,7 @@ | |||
307 | 475 | action="DescribeVolumes", creds=self.creds, endpoint=self.endpoint, | 314 | action="DescribeVolumes", creds=self.creds, endpoint=self.endpoint, |
308 | 476 | other_params=volumeset) | 315 | other_params=volumeset) |
309 | 477 | d = query.submit() | 316 | d = query.submit() |
339 | 478 | return d.addCallback(self._parse_describe_volumes) | 317 | return d.addCallback(self.parser.describe_volumes) |
311 | 479 | |||
312 | 480 | def _parse_describe_volumes(self, xml_bytes): | ||
313 | 481 | root = XML(xml_bytes) | ||
314 | 482 | result = [] | ||
315 | 483 | for volume_data in root.find("volumeSet"): | ||
316 | 484 | volume_id = volume_data.findtext("volumeId") | ||
317 | 485 | size = int(volume_data.findtext("size")) | ||
318 | 486 | status = volume_data.findtext("status") | ||
319 | 487 | availability_zone = volume_data.findtext("availabilityZone") | ||
320 | 488 | snapshot_id = volume_data.findtext("snapshotId") | ||
321 | 489 | create_time = volume_data.findtext("createTime") | ||
322 | 490 | create_time = datetime.strptime( | ||
323 | 491 | create_time[:19], "%Y-%m-%dT%H:%M:%S") | ||
324 | 492 | volume = model.Volume( | ||
325 | 493 | volume_id, size, status, create_time, availability_zone, | ||
326 | 494 | snapshot_id) | ||
327 | 495 | result.append(volume) | ||
328 | 496 | for attachment_data in volume_data.find("attachmentSet"): | ||
329 | 497 | instance_id = attachment_data.findtext("instanceId") | ||
330 | 498 | status = attachment_data.findtext("status") | ||
331 | 499 | device = attachment_data.findtext("device") | ||
332 | 500 | attach_time = attachment_data.findtext("attachTime") | ||
333 | 501 | attach_time = datetime.strptime( | ||
334 | 502 | attach_time[:19], "%Y-%m-%dT%H:%M:%S") | ||
335 | 503 | attachment = model.Attachment( | ||
336 | 504 | instance_id, device, status, attach_time) | ||
337 | 505 | volume.attachments.append(attachment) | ||
338 | 506 | return result | ||
340 | 507 | 318 | ||
341 | 508 | def create_volume(self, availability_zone, size=None, snapshot_id=None): | 319 | def create_volume(self, availability_zone, size=None, snapshot_id=None): |
342 | 509 | """Create a new volume.""" | 320 | """Create a new volume.""" |
343 | @@ -519,29 +330,14 @@ | |||
344 | 519 | action="CreateVolume", creds=self.creds, endpoint=self.endpoint, | 330 | action="CreateVolume", creds=self.creds, endpoint=self.endpoint, |
345 | 520 | other_params=params) | 331 | other_params=params) |
346 | 521 | d = query.submit() | 332 | d = query.submit() |
363 | 522 | return d.addCallback(self._parse_create_volume) | 333 | return d.addCallback(self.parser.create_volume) |
348 | 523 | |||
349 | 524 | def _parse_create_volume(self, xml_bytes): | ||
350 | 525 | root = XML(xml_bytes) | ||
351 | 526 | volume_id = root.findtext("volumeId") | ||
352 | 527 | size = int(root.findtext("size")) | ||
353 | 528 | status = root.findtext("status") | ||
354 | 529 | create_time = root.findtext("createTime") | ||
355 | 530 | availability_zone = root.findtext("availabilityZone") | ||
356 | 531 | snapshot_id = root.findtext("snapshotId") | ||
357 | 532 | create_time = datetime.strptime( | ||
358 | 533 | create_time[:19], "%Y-%m-%dT%H:%M:%S") | ||
359 | 534 | volume = model.Volume( | ||
360 | 535 | volume_id, size, status, create_time, availability_zone, | ||
361 | 536 | snapshot_id) | ||
362 | 537 | return volume | ||
364 | 538 | 334 | ||
365 | 539 | def delete_volume(self, volume_id): | 335 | def delete_volume(self, volume_id): |
366 | 540 | query = self.query_factory( | 336 | query = self.query_factory( |
367 | 541 | action="DeleteVolume", creds=self.creds, endpoint=self.endpoint, | 337 | action="DeleteVolume", creds=self.creds, endpoint=self.endpoint, |
368 | 542 | other_params={"VolumeId": volume_id}) | 338 | other_params={"VolumeId": volume_id}) |
369 | 543 | d = query.submit() | 339 | d = query.submit() |
371 | 544 | return d.addCallback(self._parse_truth_return) | 340 | return d.addCallback(self.parser.truth_return) |
372 | 545 | 341 | ||
373 | 546 | def describe_snapshots(self, *snapshot_ids): | 342 | def describe_snapshots(self, *snapshot_ids): |
374 | 547 | """Describe available snapshots.""" | 343 | """Describe available snapshots.""" |
375 | @@ -552,24 +348,7 @@ | |||
376 | 552 | action="DescribeSnapshots", creds=self.creds, | 348 | action="DescribeSnapshots", creds=self.creds, |
377 | 553 | endpoint=self.endpoint, other_params=snapshot_set) | 349 | endpoint=self.endpoint, other_params=snapshot_set) |
378 | 554 | d = query.submit() | 350 | d = query.submit() |
397 | 555 | return d.addCallback(self._parse_snapshots) | 351 | return d.addCallback(self.parser.snapshots) |
380 | 556 | |||
381 | 557 | def _parse_snapshots(self, xml_bytes): | ||
382 | 558 | root = XML(xml_bytes) | ||
383 | 559 | result = [] | ||
384 | 560 | for snapshot_data in root.find("snapshotSet"): | ||
385 | 561 | snapshot_id = snapshot_data.findtext("snapshotId") | ||
386 | 562 | volume_id = snapshot_data.findtext("volumeId") | ||
387 | 563 | status = snapshot_data.findtext("status") | ||
388 | 564 | start_time = snapshot_data.findtext("startTime") | ||
389 | 565 | start_time = datetime.strptime( | ||
390 | 566 | start_time[:19], "%Y-%m-%dT%H:%M:%S") | ||
391 | 567 | progress = snapshot_data.findtext("progress")[:-1] | ||
392 | 568 | progress = float(progress or "0") / 100. | ||
393 | 569 | snapshot = model.Snapshot( | ||
394 | 570 | snapshot_id, volume_id, status, start_time, progress) | ||
395 | 571 | result.append(snapshot) | ||
396 | 572 | return result | ||
398 | 573 | 352 | ||
399 | 574 | def create_snapshot(self, volume_id): | 353 | def create_snapshot(self, volume_id): |
400 | 575 | """Create a new snapshot of an existing volume.""" | 354 | """Create a new snapshot of an existing volume.""" |
401 | @@ -577,20 +356,7 @@ | |||
402 | 577 | action="CreateSnapshot", creds=self.creds, endpoint=self.endpoint, | 356 | action="CreateSnapshot", creds=self.creds, endpoint=self.endpoint, |
403 | 578 | other_params={"VolumeId": volume_id}) | 357 | other_params={"VolumeId": volume_id}) |
404 | 579 | d = query.submit() | 358 | d = query.submit() |
419 | 580 | return d.addCallback(self._parse_create_snapshot) | 359 | return d.addCallback(self.parser.create_snapshot) |
406 | 581 | |||
407 | 582 | def _parse_create_snapshot(self, xml_bytes): | ||
408 | 583 | root = XML(xml_bytes) | ||
409 | 584 | snapshot_id = root.findtext("snapshotId") | ||
410 | 585 | volume_id = root.findtext("volumeId") | ||
411 | 586 | status = root.findtext("status") | ||
412 | 587 | start_time = root.findtext("startTime") | ||
413 | 588 | start_time = datetime.strptime( | ||
414 | 589 | start_time[:19], "%Y-%m-%dT%H:%M:%S") | ||
415 | 590 | progress = root.findtext("progress")[:-1] | ||
416 | 591 | progress = float(progress or "0") / 100. | ||
417 | 592 | return model.Snapshot( | ||
418 | 593 | snapshot_id, volume_id, status, start_time, progress) | ||
420 | 594 | 360 | ||
421 | 595 | def delete_snapshot(self, snapshot_id): | 361 | def delete_snapshot(self, snapshot_id): |
422 | 596 | """Remove a previously created snapshot.""" | 362 | """Remove a previously created snapshot.""" |
423 | @@ -598,7 +364,7 @@ | |||
424 | 598 | action="DeleteSnapshot", creds=self.creds, endpoint=self.endpoint, | 364 | action="DeleteSnapshot", creds=self.creds, endpoint=self.endpoint, |
425 | 599 | other_params={"SnapshotId": snapshot_id}) | 365 | other_params={"SnapshotId": snapshot_id}) |
426 | 600 | d = query.submit() | 366 | d = query.submit() |
428 | 601 | return d.addCallback(self._parse_truth_return) | 367 | return d.addCallback(self.parser.truth_return) |
429 | 602 | 368 | ||
430 | 603 | def attach_volume(self, volume_id, instance_id, device): | 369 | def attach_volume(self, volume_id, instance_id, device): |
431 | 604 | """Attach the given volume to the specified instance at C{device}.""" | 370 | """Attach the given volume to the specified instance at C{device}.""" |
432 | @@ -607,15 +373,7 @@ | |||
433 | 607 | other_params={"VolumeId": volume_id, "InstanceId": instance_id, | 373 | other_params={"VolumeId": volume_id, "InstanceId": instance_id, |
434 | 608 | "Device": device}) | 374 | "Device": device}) |
435 | 609 | d = query.submit() | 375 | d = query.submit() |
445 | 610 | return d.addCallback(self._parse_attach_volume) | 376 | return d.addCallback(self.parser.attach_volume) |
437 | 611 | |||
438 | 612 | def _parse_attach_volume(self, xml_bytes): | ||
439 | 613 | root = XML(xml_bytes) | ||
440 | 614 | status = root.findtext("status") | ||
441 | 615 | attach_time = root.findtext("attachTime") | ||
442 | 616 | attach_time = datetime.strptime( | ||
443 | 617 | attach_time[:19], "%Y-%m-%dT%H:%M:%S") | ||
444 | 618 | return {"status": status, "attach_time": attach_time} | ||
446 | 619 | 377 | ||
447 | 620 | def describe_keypairs(self, *keypair_names): | 378 | def describe_keypairs(self, *keypair_names): |
448 | 621 | """Returns information about key pairs available.""" | 379 | """Returns information about key pairs available.""" |
449 | @@ -626,19 +384,7 @@ | |||
450 | 626 | action="DescribeKeyPairs", creds=self.creds, | 384 | action="DescribeKeyPairs", creds=self.creds, |
451 | 627 | endpoint=self.endpoint, other_params=keypairs) | 385 | endpoint=self.endpoint, other_params=keypairs) |
452 | 628 | d = query.submit() | 386 | d = query.submit() |
466 | 629 | return d.addCallback(self._parse_describe_keypairs) | 387 | return d.addCallback(self.parser.describe_keypairs) |
454 | 630 | |||
455 | 631 | def _parse_describe_keypairs(self, xml_bytes): | ||
456 | 632 | results = [] | ||
457 | 633 | root = XML(xml_bytes) | ||
458 | 634 | keypairs = root.find("keySet") | ||
459 | 635 | if keypairs is None: | ||
460 | 636 | return results | ||
461 | 637 | for keypair_data in keypairs: | ||
462 | 638 | key_name = keypair_data.findtext("keyName") | ||
463 | 639 | key_fingerprint = keypair_data.findtext("keyFingerprint") | ||
464 | 640 | results.append(model.Keypair(key_name, key_fingerprint)) | ||
465 | 641 | return results | ||
467 | 642 | 388 | ||
468 | 643 | def create_keypair(self, keypair_name): | 389 | def create_keypair(self, keypair_name): |
469 | 644 | """ | 390 | """ |
470 | @@ -649,14 +395,7 @@ | |||
471 | 649 | action="CreateKeyPair", creds=self.creds, endpoint=self.endpoint, | 395 | action="CreateKeyPair", creds=self.creds, endpoint=self.endpoint, |
472 | 650 | other_params={"KeyName": keypair_name}) | 396 | other_params={"KeyName": keypair_name}) |
473 | 651 | d = query.submit() | 397 | d = query.submit() |
482 | 652 | return d.addCallback(self._parse_create_keypair) | 398 | return d.addCallback(self.parser.create_keypair) |
475 | 653 | |||
476 | 654 | def _parse_create_keypair(self, xml_bytes): | ||
477 | 655 | keypair_data = XML(xml_bytes) | ||
478 | 656 | key_name = keypair_data.findtext("keyName") | ||
479 | 657 | key_fingerprint = keypair_data.findtext("keyFingerprint") | ||
480 | 658 | key_material = keypair_data.findtext("keyMaterial") | ||
481 | 659 | return model.Keypair(key_name, key_fingerprint, key_material) | ||
483 | 660 | 399 | ||
484 | 661 | def delete_keypair(self, keypair_name): | 400 | def delete_keypair(self, keypair_name): |
485 | 662 | """Delete a given keypair.""" | 401 | """Delete a given keypair.""" |
486 | @@ -664,7 +403,7 @@ | |||
487 | 664 | action="DeleteKeyPair", creds=self.creds, endpoint=self.endpoint, | 403 | action="DeleteKeyPair", creds=self.creds, endpoint=self.endpoint, |
488 | 665 | other_params={"KeyName": keypair_name}) | 404 | other_params={"KeyName": keypair_name}) |
489 | 666 | d = query.submit() | 405 | d = query.submit() |
491 | 667 | return d.addCallback(self._parse_truth_return) | 406 | return d.addCallback(self.parser.truth_return) |
492 | 668 | 407 | ||
493 | 669 | def import_keypair(self, keypair_name, key_material): | 408 | def import_keypair(self, keypair_name, key_material): |
494 | 670 | """ | 409 | """ |
495 | @@ -685,14 +424,7 @@ | |||
496 | 685 | other_params={"KeyName": keypair_name, | 424 | other_params={"KeyName": keypair_name, |
497 | 686 | "PublicKeyMaterial": b64encode(key_material)}) | 425 | "PublicKeyMaterial": b64encode(key_material)}) |
498 | 687 | d = query.submit() | 426 | d = query.submit() |
507 | 688 | return d.addCallback(self._parse_import_keypair, key_material) | 427 | return d.addCallback(self.parser.import_keypair, key_material) |
500 | 689 | |||
501 | 690 | def _parse_import_keypair(self, xml_bytes, key_material): | ||
502 | 691 | """Extract the key name and the fingerprint from the result.""" | ||
503 | 692 | keypair_data = XML(xml_bytes) | ||
504 | 693 | key_name = keypair_data.findtext("keyName") | ||
505 | 694 | key_fingerprint = keypair_data.findtext("keyFingerprint") | ||
506 | 695 | return model.Keypair(key_name, key_fingerprint, key_material) | ||
508 | 696 | 428 | ||
509 | 697 | def allocate_address(self): | 429 | def allocate_address(self): |
510 | 698 | """ | 430 | """ |
511 | @@ -706,11 +438,7 @@ | |||
512 | 706 | action="AllocateAddress", creds=self.creds, endpoint=self.endpoint, | 438 | action="AllocateAddress", creds=self.creds, endpoint=self.endpoint, |
513 | 707 | other_params={}) | 439 | other_params={}) |
514 | 708 | d = query.submit() | 440 | d = query.submit() |
520 | 709 | return d.addCallback(self._parse_allocate_address) | 441 | return d.addCallback(self.parser.allocate_address) |
516 | 710 | |||
517 | 711 | def _parse_allocate_address(self, xml_bytes): | ||
518 | 712 | address_data = XML(xml_bytes) | ||
519 | 713 | return address_data.findtext("publicIp") | ||
521 | 714 | 442 | ||
522 | 715 | def release_address(self, address): | 443 | def release_address(self, address): |
523 | 716 | """ | 444 | """ |
524 | @@ -722,7 +450,7 @@ | |||
525 | 722 | action="ReleaseAddress", creds=self.creds, endpoint=self.endpoint, | 450 | action="ReleaseAddress", creds=self.creds, endpoint=self.endpoint, |
526 | 723 | other_params={"PublicIp": address}) | 451 | other_params={"PublicIp": address}) |
527 | 724 | d = query.submit() | 452 | d = query.submit() |
529 | 725 | return d.addCallback(self._parse_truth_return) | 453 | return d.addCallback(self.parser.truth_return) |
530 | 726 | 454 | ||
531 | 727 | def associate_address(self, instance_id, address): | 455 | def associate_address(self, instance_id, address): |
532 | 728 | """ | 456 | """ |
533 | @@ -736,7 +464,7 @@ | |||
534 | 736 | endpoint=self.endpoint, | 464 | endpoint=self.endpoint, |
535 | 737 | other_params={"InstanceId": instance_id, "PublicIp": address}) | 465 | other_params={"InstanceId": instance_id, "PublicIp": address}) |
536 | 738 | d = query.submit() | 466 | d = query.submit() |
538 | 739 | return d.addCallback(self._parse_truth_return) | 467 | return d.addCallback(self.parser.truth_return) |
539 | 740 | 468 | ||
540 | 741 | def disassociate_address(self, address): | 469 | def disassociate_address(self, address): |
541 | 742 | """ | 470 | """ |
542 | @@ -748,7 +476,7 @@ | |||
543 | 748 | action="DisassociateAddress", creds=self.creds, | 476 | action="DisassociateAddress", creds=self.creds, |
544 | 749 | endpoint=self.endpoint, other_params={"PublicIp": address}) | 477 | endpoint=self.endpoint, other_params={"PublicIp": address}) |
545 | 750 | d = query.submit() | 478 | d = query.submit() |
547 | 751 | return d.addCallback(self._parse_truth_return) | 479 | return d.addCallback(self.parser.truth_return) |
548 | 752 | 480 | ||
549 | 753 | def describe_addresses(self, *addresses): | 481 | def describe_addresses(self, *addresses): |
550 | 754 | """ | 482 | """ |
551 | @@ -766,16 +494,7 @@ | |||
552 | 766 | action="DescribeAddresses", creds=self.creds, | 494 | action="DescribeAddresses", creds=self.creds, |
553 | 767 | endpoint=self.endpoint, other_params=address_set) | 495 | endpoint=self.endpoint, other_params=address_set) |
554 | 768 | d = query.submit() | 496 | d = query.submit() |
565 | 769 | return d.addCallback(self._parse_describe_addresses) | 497 | return d.addCallback(self.parser.describe_addresses) |
556 | 770 | |||
557 | 771 | def _parse_describe_addresses(self, xml_bytes): | ||
558 | 772 | results = [] | ||
559 | 773 | root = XML(xml_bytes) | ||
560 | 774 | for address_data in root.find("addressesSet"): | ||
561 | 775 | address = address_data.findtext("publicIp") | ||
562 | 776 | instance_id = address_data.findtext("instanceId") | ||
563 | 777 | results.append((address, instance_id)) | ||
564 | 778 | return results | ||
566 | 779 | 498 | ||
567 | 780 | def describe_availability_zones(self, names=None): | 499 | def describe_availability_zones(self, names=None): |
568 | 781 | zone_names = None | 500 | zone_names = None |
569 | @@ -786,9 +505,372 @@ | |||
570 | 786 | action="DescribeAvailabilityZones", creds=self.creds, | 505 | action="DescribeAvailabilityZones", creds=self.creds, |
571 | 787 | endpoint=self.endpoint, other_params=zone_names) | 506 | endpoint=self.endpoint, other_params=zone_names) |
572 | 788 | d = query.submit() | 507 | d = query.submit() |
576 | 789 | return d.addCallback(self._parse_describe_availability_zones) | 508 | return d.addCallback(self.parser.describe_availability_zones) |
577 | 790 | 509 | ||
578 | 791 | def _parse_describe_availability_zones(self, xml_bytes): | 510 | |
579 | 511 | class Parser(object): | ||
580 | 512 | """A parser for EC2 responses""" | ||
581 | 513 | |||
582 | 514 | def instances_set(self, root, reservation): | ||
583 | 515 | """Parse instance data out of an XML payload. | ||
584 | 516 | |||
585 | 517 | @param root: The root node of the XML payload. | ||
586 | 518 | @param reservation: The L{Reservation} associated with the instances | ||
587 | 519 | from the response. | ||
588 | 520 | @return: A C{list} of L{Instance}s. | ||
589 | 521 | """ | ||
590 | 522 | instances = [] | ||
591 | 523 | for instance_data in root.find("instancesSet"): | ||
592 | 524 | instances.append(self.instance(instance_data, reservation)) | ||
593 | 525 | return instances | ||
594 | 526 | |||
595 | 527 | def instance(self, instance_data, reservation): | ||
596 | 528 | """Parse instance data out of an XML payload. | ||
597 | 529 | |||
598 | 530 | @param instance_data: An XML node containing instance data. | ||
599 | 531 | @param reservation: The L{Reservation} associated with the instance. | ||
600 | 532 | @return: An L{Instance}. | ||
601 | 533 | """ | ||
602 | 534 | instance_id = instance_data.findtext("instanceId") | ||
603 | 535 | instance_state = instance_data.find( | ||
604 | 536 | "instanceState").findtext("name") | ||
605 | 537 | instance_type = instance_data.findtext("instanceType") | ||
606 | 538 | image_id = instance_data.findtext("imageId") | ||
607 | 539 | private_dns_name = instance_data.findtext("privateDnsName") | ||
608 | 540 | dns_name = instance_data.findtext("dnsName") | ||
609 | 541 | key_name = instance_data.findtext("keyName") | ||
610 | 542 | ami_launch_index = instance_data.findtext("amiLaunchIndex") | ||
611 | 543 | launch_time = instance_data.findtext("launchTime") | ||
612 | 544 | placement = instance_data.find("placement").findtext( | ||
613 | 545 | "availabilityZone") | ||
614 | 546 | products = [] | ||
615 | 547 | product_codes = instance_data.find("productCodes") | ||
616 | 548 | if product_codes is not None: | ||
617 | 549 | for product_data in instance_data.find("productCodes"): | ||
618 | 550 | products.append(product_data.text) | ||
619 | 551 | kernel_id = instance_data.findtext("kernelId") | ||
620 | 552 | ramdisk_id = instance_data.findtext("ramdiskId") | ||
621 | 553 | instance = model.Instance( | ||
622 | 554 | instance_id, instance_state, instance_type, image_id, | ||
623 | 555 | private_dns_name, dns_name, key_name, ami_launch_index, | ||
624 | 556 | launch_time, placement, products, kernel_id, ramdisk_id, | ||
625 | 557 | reservation=reservation) | ||
626 | 558 | return instance | ||
627 | 559 | |||
628 | 560 | def describe_instances(self, xml_bytes): | ||
629 | 561 | """ | ||
630 | 562 | Parse the reservations XML payload that is returned from an AWS | ||
631 | 563 | describeInstances API call. | ||
632 | 564 | |||
633 | 565 | Instead of returning the reservations as the "top-most" object, we | ||
634 | 566 | return the object that most developers and their code will be | ||
635 | 567 | interested in: the instances. In instances reservation is available on | ||
636 | 568 | the instance object. | ||
637 | 569 | |||
638 | 570 | The following instance attributes are optional: | ||
639 | 571 | * ami_launch_index | ||
640 | 572 | * key_name | ||
641 | 573 | * kernel_id | ||
642 | 574 | * product_codes | ||
643 | 575 | * ramdisk_id | ||
644 | 576 | * reason | ||
645 | 577 | |||
646 | 578 | @param xml_bytes: raw XML payload from AWS. | ||
647 | 579 | """ | ||
648 | 580 | root = XML(xml_bytes) | ||
649 | 581 | results = [] | ||
650 | 582 | # May be a more elegant way to do this: | ||
651 | 583 | for reservation_data in root.find("reservationSet"): | ||
652 | 584 | # Get the security group information. | ||
653 | 585 | groups = [] | ||
654 | 586 | for group_data in reservation_data.find("groupSet"): | ||
655 | 587 | group_id = group_data.findtext("groupId") | ||
656 | 588 | groups.append(group_id) | ||
657 | 589 | # Create a reservation object with the parsed data. | ||
658 | 590 | reservation = model.Reservation( | ||
659 | 591 | reservation_id=reservation_data.findtext("reservationId"), | ||
660 | 592 | owner_id=reservation_data.findtext("ownerId"), | ||
661 | 593 | groups=groups) | ||
662 | 594 | # Get the list of instances. | ||
663 | 595 | instances = self.instances_set( | ||
664 | 596 | reservation_data, reservation) | ||
665 | 597 | results.extend(instances) | ||
666 | 598 | return results | ||
667 | 599 | |||
668 | 600 | def run_instances(self, xml_bytes): | ||
669 | 601 | """ | ||
670 | 602 | Parse the reservations XML payload that is returned from an AWS | ||
671 | 603 | RunInstances API call. | ||
672 | 604 | |||
673 | 605 | @param xml_bytes: raw XML payload from AWS. | ||
674 | 606 | """ | ||
675 | 607 | root = XML(xml_bytes) | ||
676 | 608 | # Get the security group information. | ||
677 | 609 | groups = [] | ||
678 | 610 | for group_data in root.find("groupSet"): | ||
679 | 611 | group_id = group_data.findtext("groupId") | ||
680 | 612 | groups.append(group_id) | ||
681 | 613 | # Create a reservation object with the parsed data. | ||
682 | 614 | reservation = model.Reservation( | ||
683 | 615 | reservation_id=root.findtext("reservationId"), | ||
684 | 616 | owner_id=root.findtext("ownerId"), | ||
685 | 617 | groups=groups) | ||
686 | 618 | # Get the list of instances. | ||
687 | 619 | instances = self.instances_set(root, reservation) | ||
688 | 620 | return instances | ||
689 | 621 | |||
690 | 622 | def terminate_instances(self, xml_bytes): | ||
691 | 623 | """Parse the XML returned by the C{TerminateInstances} function. | ||
692 | 624 | |||
693 | 625 | @param xml_bytes: XML bytes with a C{TerminateInstancesResponse} root | ||
694 | 626 | element. | ||
695 | 627 | @return: An iterable of C{tuple} of (instanceId, previousState, | ||
696 | 628 | shutdownState) for the ec2 instances that where terminated. | ||
697 | 629 | """ | ||
698 | 630 | root = XML(xml_bytes) | ||
699 | 631 | result = [] | ||
700 | 632 | # May be a more elegant way to do this: | ||
701 | 633 | for instance in root.find("instancesSet"): | ||
702 | 634 | instanceId = instance.findtext("instanceId") | ||
703 | 635 | previousState = instance.find("previousState").findtext( | ||
704 | 636 | "name") | ||
705 | 637 | shutdownState = instance.find("shutdownState").findtext( | ||
706 | 638 | "name") | ||
707 | 639 | result.append((instanceId, previousState, shutdownState)) | ||
708 | 640 | return result | ||
709 | 641 | |||
710 | 642 | def describe_security_groups(self, xml_bytes): | ||
711 | 643 | """Parse the XML returned by the C{DescribeSecurityGroups} function. | ||
712 | 644 | |||
713 | 645 | @param xml_bytes: XML bytes with a C{DescribeSecurityGroupsResponse} | ||
714 | 646 | root element. | ||
715 | 647 | @return: A list of L{SecurityGroup} instances. | ||
716 | 648 | """ | ||
717 | 649 | root = XML(xml_bytes) | ||
718 | 650 | result = [] | ||
719 | 651 | for group_info in root.findall("securityGroupInfo/item"): | ||
720 | 652 | name = group_info.findtext("groupName") | ||
721 | 653 | description = group_info.findtext("groupDescription") | ||
722 | 654 | owner_id = group_info.findtext("ownerId") | ||
723 | 655 | allowed_groups = [] | ||
724 | 656 | allowed_ips = [] | ||
725 | 657 | ip_permissions = group_info.find("ipPermissions") | ||
726 | 658 | if ip_permissions is None: | ||
727 | 659 | ip_permissions = () | ||
728 | 660 | for ip_permission in ip_permissions: | ||
729 | 661 | ip_protocol = ip_permission.findtext("ipProtocol") | ||
730 | 662 | from_port = int(ip_permission.findtext("fromPort")) | ||
731 | 663 | to_port = int(ip_permission.findtext("toPort")) | ||
732 | 664 | for groups in ip_permission.findall("groups/item") or (): | ||
733 | 665 | user_id = groups.findtext("userId") | ||
734 | 666 | group_name = groups.findtext("groupName") | ||
735 | 667 | if user_id and group_name: | ||
736 | 668 | if (user_id, group_name) not in allowed_groups: | ||
737 | 669 | allowed_groups.append((user_id, group_name)) | ||
738 | 670 | for ip_ranges in ip_permission.findall("ipRanges/item") or (): | ||
739 | 671 | cidr_ip = ip_ranges.findtext("cidrIp") | ||
740 | 672 | allowed_ips.append( | ||
741 | 673 | model.IPPermission( | ||
742 | 674 | ip_protocol, from_port, to_port, cidr_ip)) | ||
743 | 675 | |||
744 | 676 | allowed_groups = [model.UserIDGroupPair(user_id, group_name) | ||
745 | 677 | for user_id, group_name in allowed_groups] | ||
746 | 678 | |||
747 | 679 | security_group = model.SecurityGroup( | ||
748 | 680 | name, description, owner_id=owner_id, | ||
749 | 681 | groups=allowed_groups, ips=allowed_ips) | ||
750 | 682 | result.append(security_group) | ||
751 | 683 | return result | ||
752 | 684 | |||
753 | 685 | def truth_return(self, xml_bytes): | ||
754 | 686 | """Parse the XML for a truth value. | ||
755 | 687 | |||
756 | 688 | @param xml_bytes: XML bytes. | ||
757 | 689 | @return: True if the node contains "return" otherwise False. | ||
758 | 690 | """ | ||
759 | 691 | root = XML(xml_bytes) | ||
760 | 692 | return root.findtext("return") == "true" | ||
761 | 693 | |||
762 | 694 | def describe_volumes(self, xml_bytes): | ||
763 | 695 | """Parse the XML returned by the C{DescribeVolumes} function. | ||
764 | 696 | |||
765 | 697 | @param xml_bytes: XML bytes with a C{DescribeVolumesResponse} root | ||
766 | 698 | element. | ||
767 | 699 | @return: A list of L{Volume} instances. | ||
768 | 700 | """ | ||
769 | 701 | root = XML(xml_bytes) | ||
770 | 702 | result = [] | ||
771 | 703 | for volume_data in root.find("volumeSet"): | ||
772 | 704 | volume_id = volume_data.findtext("volumeId") | ||
773 | 705 | size = int(volume_data.findtext("size")) | ||
774 | 706 | status = volume_data.findtext("status") | ||
775 | 707 | availability_zone = volume_data.findtext("availabilityZone") | ||
776 | 708 | snapshot_id = volume_data.findtext("snapshotId") | ||
777 | 709 | create_time = volume_data.findtext("createTime") | ||
778 | 710 | create_time = datetime.strptime( | ||
779 | 711 | create_time[:19], "%Y-%m-%dT%H:%M:%S") | ||
780 | 712 | volume = model.Volume( | ||
781 | 713 | volume_id, size, status, create_time, availability_zone, | ||
782 | 714 | snapshot_id) | ||
783 | 715 | result.append(volume) | ||
784 | 716 | for attachment_data in volume_data.find("attachmentSet"): | ||
785 | 717 | instance_id = attachment_data.findtext("instanceId") | ||
786 | 718 | status = attachment_data.findtext("status") | ||
787 | 719 | device = attachment_data.findtext("device") | ||
788 | 720 | attach_time = attachment_data.findtext("attachTime") | ||
789 | 721 | attach_time = datetime.strptime( | ||
790 | 722 | attach_time[:19], "%Y-%m-%dT%H:%M:%S") | ||
791 | 723 | attachment = model.Attachment( | ||
792 | 724 | instance_id, device, status, attach_time) | ||
793 | 725 | volume.attachments.append(attachment) | ||
794 | 726 | return result | ||
795 | 727 | |||
796 | 728 | def create_volume(self, xml_bytes): | ||
797 | 729 | """Parse the XML returned by the C{CreateVolume} function. | ||
798 | 730 | |||
799 | 731 | @param xml_bytes: XML bytes with a C{CreateVolumeResponse} root | ||
800 | 732 | element. | ||
801 | 733 | @return: The L{Volume} instance created. | ||
802 | 734 | """ | ||
803 | 735 | root = XML(xml_bytes) | ||
804 | 736 | volume_id = root.findtext("volumeId") | ||
805 | 737 | size = int(root.findtext("size")) | ||
806 | 738 | status = root.findtext("status") | ||
807 | 739 | create_time = root.findtext("createTime") | ||
808 | 740 | availability_zone = root.findtext("availabilityZone") | ||
809 | 741 | snapshot_id = root.findtext("snapshotId") | ||
810 | 742 | create_time = datetime.strptime( | ||
811 | 743 | create_time[:19], "%Y-%m-%dT%H:%M:%S") | ||
812 | 744 | volume = model.Volume( | ||
813 | 745 | volume_id, size, status, create_time, availability_zone, | ||
814 | 746 | snapshot_id) | ||
815 | 747 | return volume | ||
816 | 748 | |||
817 | 749 | def snapshots(self, xml_bytes): | ||
818 | 750 | """Parse the XML returned by the C{DescribeSnapshots} function. | ||
819 | 751 | |||
820 | 752 | @param xml_bytes: XML bytes with a C{DescribeSnapshotsResponse} root | ||
821 | 753 | element. | ||
822 | 754 | @return: A list of L{Snapshot} instances. | ||
823 | 755 | """ | ||
824 | 756 | root = XML(xml_bytes) | ||
825 | 757 | result = [] | ||
826 | 758 | for snapshot_data in root.find("snapshotSet"): | ||
827 | 759 | snapshot_id = snapshot_data.findtext("snapshotId") | ||
828 | 760 | volume_id = snapshot_data.findtext("volumeId") | ||
829 | 761 | status = snapshot_data.findtext("status") | ||
830 | 762 | start_time = snapshot_data.findtext("startTime") | ||
831 | 763 | start_time = datetime.strptime( | ||
832 | 764 | start_time[:19], "%Y-%m-%dT%H:%M:%S") | ||
833 | 765 | progress = snapshot_data.findtext("progress")[:-1] | ||
834 | 766 | progress = float(progress or "0") / 100. | ||
835 | 767 | snapshot = model.Snapshot( | ||
836 | 768 | snapshot_id, volume_id, status, start_time, progress) | ||
837 | 769 | result.append(snapshot) | ||
838 | 770 | return result | ||
839 | 771 | |||
840 | 772 | def create_snapshot(self, xml_bytes): | ||
841 | 773 | """Parse the XML returned by the C{CreateSnapshot} function. | ||
842 | 774 | |||
843 | 775 | @param xml_bytes: XML bytes with a C{CreateSnapshotResponse} root | ||
844 | 776 | element. | ||
845 | 777 | @return: The L{Snapshot} instance created. | ||
846 | 778 | """ | ||
847 | 779 | root = XML(xml_bytes) | ||
848 | 780 | snapshot_id = root.findtext("snapshotId") | ||
849 | 781 | volume_id = root.findtext("volumeId") | ||
850 | 782 | status = root.findtext("status") | ||
851 | 783 | start_time = root.findtext("startTime") | ||
852 | 784 | start_time = datetime.strptime( | ||
853 | 785 | start_time[:19], "%Y-%m-%dT%H:%M:%S") | ||
854 | 786 | progress = root.findtext("progress")[:-1] | ||
855 | 787 | progress = float(progress or "0") / 100. | ||
856 | 788 | return model.Snapshot( | ||
857 | 789 | snapshot_id, volume_id, status, start_time, progress) | ||
858 | 790 | |||
859 | 791 | def attach_volume(self, xml_bytes): | ||
860 | 792 | """Parse the XML returned by the C{AttachVolume} function. | ||
861 | 793 | |||
862 | 794 | @param xml_bytes: XML bytes with a C{AttachVolumeResponse} root | ||
863 | 795 | element. | ||
864 | 796 | @return: a C{dict} with status and attach_time keys. | ||
865 | 797 | """ | ||
866 | 798 | root = XML(xml_bytes) | ||
867 | 799 | status = root.findtext("status") | ||
868 | 800 | attach_time = root.findtext("attachTime") | ||
869 | 801 | attach_time = datetime.strptime( | ||
870 | 802 | attach_time[:19], "%Y-%m-%dT%H:%M:%S") | ||
871 | 803 | return {"status": status, "attach_time": attach_time} | ||
872 | 804 | |||
873 | 805 | def describe_keypairs(self, xml_bytes): | ||
874 | 806 | """Parse the XML returned by the C{DescribeKeyPairs} function. | ||
875 | 807 | |||
876 | 808 | @param xml_bytes: XML bytes with a C{DescribeKeyPairsResponse} root | ||
877 | 809 | element. | ||
878 | 810 | @return: a C{list} of L{Keypair}. | ||
879 | 811 | """ | ||
880 | 812 | results = [] | ||
881 | 813 | root = XML(xml_bytes) | ||
882 | 814 | keypairs = root.find("keySet") | ||
883 | 815 | if keypairs is None: | ||
884 | 816 | return results | ||
885 | 817 | for keypair_data in keypairs: | ||
886 | 818 | key_name = keypair_data.findtext("keyName") | ||
887 | 819 | key_fingerprint = keypair_data.findtext("keyFingerprint") | ||
888 | 820 | results.append(model.Keypair(key_name, key_fingerprint)) | ||
889 | 821 | return results | ||
890 | 822 | |||
891 | 823 | def create_keypair(self, xml_bytes): | ||
892 | 824 | """Parse the XML returned by the C{CreateKeyPair} function. | ||
893 | 825 | |||
894 | 826 | @param xml_bytes: XML bytes with a C{CreateKeyPairResponse} root | ||
895 | 827 | element. | ||
896 | 828 | @return: The L{Keypair} instance created. | ||
897 | 829 | """ | ||
898 | 830 | keypair_data = XML(xml_bytes) | ||
899 | 831 | key_name = keypair_data.findtext("keyName") | ||
900 | 832 | key_fingerprint = keypair_data.findtext("keyFingerprint") | ||
901 | 833 | key_material = keypair_data.findtext("keyMaterial") | ||
902 | 834 | return model.Keypair(key_name, key_fingerprint, key_material) | ||
903 | 835 | |||
904 | 836 | def import_keypair(self, xml_bytes, key_material): | ||
905 | 837 | """Extract the key name and the fingerprint from the result.""" | ||
906 | 838 | keypair_data = XML(xml_bytes) | ||
907 | 839 | key_name = keypair_data.findtext("keyName") | ||
908 | 840 | key_fingerprint = keypair_data.findtext("keyFingerprint") | ||
909 | 841 | return model.Keypair(key_name, key_fingerprint, key_material) | ||
910 | 842 | |||
911 | 843 | def allocate_address(self, xml_bytes): | ||
912 | 844 | """Parse the XML returned by the C{AllocateAddress} function. | ||
913 | 845 | |||
914 | 846 | @param xml_bytes: XML bytes with a C{AllocateAddress} root element. | ||
915 | 847 | @return: The public ip address as a string. | ||
916 | 848 | """ | ||
917 | 849 | address_data = XML(xml_bytes) | ||
918 | 850 | return address_data.findtext("publicIp") | ||
919 | 851 | |||
920 | 852 | def describe_addresses(self, xml_bytes): | ||
921 | 853 | """Parse the XML returned by the C{DescribeAddresses} function. | ||
922 | 854 | |||
923 | 855 | @param xml_bytes: XML bytes with a C{DescribeAddressesResponse} root | ||
924 | 856 | element. | ||
925 | 857 | @return: a C{list} of L{tuple} of (publicIp, instancId). | ||
926 | 858 | """ | ||
927 | 859 | results = [] | ||
928 | 860 | root = XML(xml_bytes) | ||
929 | 861 | for address_data in root.find("addressesSet"): | ||
930 | 862 | address = address_data.findtext("publicIp") | ||
931 | 863 | instance_id = address_data.findtext("instanceId") | ||
932 | 864 | results.append((address, instance_id)) | ||
933 | 865 | return results | ||
934 | 866 | |||
935 | 867 | def describe_availability_zones(self, xml_bytes): | ||
936 | 868 | """Parse the XML returned by the C{DescribeAvailibilityZones} function. | ||
937 | 869 | |||
938 | 870 | @param xml_bytes: XML bytes with a C{DescribeAvailibilityZonesResponse} | ||
939 | 871 | root element. | ||
940 | 872 | @return: a C{list} of L{AvailabilityZone}. | ||
941 | 873 | """ | ||
942 | 792 | results = [] | 874 | results = [] |
943 | 793 | root = XML(xml_bytes) | 875 | root = XML(xml_bytes) |
944 | 794 | for zone_data in root.find("availabilityZoneInfo"): | 876 | for zone_data in root.find("availabilityZoneInfo"): |
945 | 795 | 877 | ||
946 | === modified file 'txaws/ec2/tests/test_client.py' | |||
947 | --- txaws/ec2/tests/test_client.py 2011-04-21 16:14:58 +0000 | |||
948 | +++ txaws/ec2/tests/test_client.py 2011-04-26 16:30:59 +0000 | |||
949 | @@ -217,7 +217,7 @@ | |||
950 | 217 | def test_parse_reservation(self): | 217 | def test_parse_reservation(self): |
951 | 218 | creds = AWSCredentials("foo", "bar") | 218 | creds = AWSCredentials("foo", "bar") |
952 | 219 | ec2 = client.EC2Client(creds=creds) | 219 | ec2 = client.EC2Client(creds=creds) |
954 | 220 | results = ec2._parse_describe_instances( | 220 | results = ec2.parser.describe_instances( |
955 | 221 | payload.sample_describe_instances_result) | 221 | payload.sample_describe_instances_result) |
956 | 222 | self.check_parsed_instances(results) | 222 | self.check_parsed_instances(results) |
957 | 223 | 223 | ||
958 | 224 | 224 | ||
959 | === modified file 'txaws/testing/ec2.py' | |||
960 | --- txaws/testing/ec2.py 2011-04-19 16:26:26 +0000 | |||
961 | +++ txaws/testing/ec2.py 2011-04-26 16:30:59 +0000 | |||
962 | @@ -16,11 +16,12 @@ | |||
963 | 16 | def __init__(self, creds, endpoint, instances=None, keypairs=None, | 16 | def __init__(self, creds, endpoint, instances=None, keypairs=None, |
964 | 17 | volumes=None, key_material="", security_groups=None, | 17 | volumes=None, key_material="", security_groups=None, |
965 | 18 | snapshots=None, addresses=None, availability_zones=None, | 18 | snapshots=None, addresses=None, availability_zones=None, |
967 | 19 | query_factory=None): | 19 | query_factory=None, parser=None): |
968 | 20 | 20 | ||
969 | 21 | self.creds = creds | 21 | self.creds = creds |
970 | 22 | self.endpoint = endpoint | 22 | self.endpoint = endpoint |
971 | 23 | self.query_factory = query_factory | 23 | self.query_factory = query_factory |
972 | 24 | self.parser = parser | ||
973 | 24 | 25 | ||
974 | 25 | self.instances = instances or [] | 26 | self.instances = instances or [] |
975 | 26 | self.keypairs = keypairs or [] | 27 | self.keypairs = keypairs or [] |
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.