1#  This software code is made available "AS IS" without warranties of any
2#  kind.  You may copy, display, modify and redistribute the software
3#  code either by itself or as incorporated into your code; provided that
4#  you do not remove any proprietary notices.  Your use of this software
5#  code is at your own risk and you waive any claim against Amazon Web
6#  Services LLC or its affiliates with respect to your use of this software
7#  code. (c) 2006 Amazon Web Services LLC or its affiliates.  All rights
8#  reserved.
9
10import base64
11import hmac
12import httplib
13import re
14import sys
15import time
16import urllib
17import hashlib
18# ElementTree is in stdlib from Python 2.5, so get it from there if we can:
19try:
20    from xml.etree import ElementTree as ET
21except ImportError:
22    from elementtree import ElementTree as ET
23
24DEFAULT_HOST = 'ec2.amazonaws.com'
25PORTS_BY_SECURITY = { True: 443, False: 80 }
26API_VERSION = '2008-02-01'
27RELEASE_VERSION = "22395"
28
29class AWSAuthConnection(object):
30
31    """
32    Creates an authorized connection to EC2 containing wrappers for
33    Query API calls.
34
35    Each API call has a matching method on this class to perform the
36    appropriate E2 action.
37
38    @ivar verbose: Verbosity flag, defaults to false.  If set to true,
39    some debug information is printed.
40
41    """
42
43    def __init__(self, aws_access_key_id, aws_secret_access_key,
44                 is_secure=True, server=DEFAULT_HOST, port=None):
45
46        if not port:
47            port = PORTS_BY_SECURITY[is_secure]
48
49        self.verbose = False
50        self.aws_access_key_id = aws_access_key_id
51        self.aws_secret_access_key = aws_secret_access_key
52        if (is_secure):
53            self.connection = httplib.HTTPSConnection("%s:%d" % (server, port))
54        else:
55            self.connection = httplib.HTTPConnection("%s:%d" % (server, port))
56
57    def pathlist(self, key, arr):
58        """Converts a key and an array of values into AWS query param format."""
59        params = {}
60        i = 0
61        for value in arr:
62            i += 1
63            params["%s.%s" % (key, i)] = value
64        return params
65
66    def allocate_address(self):
67        """Makes an C{AllocateAddress} call.
68
69        """
70        params = {}
71        return AllocateAddressResponse(self.make_request("AllocateAddress", params))
72
73    def describe_addresses(self, publicIps=None):
74        """Makes a C{DescribeAddresses} call.
75
76        @param publicIps: List of addresses to describe.  If empty or
77        omitted, all addresses will be described.
78
79        """
80        if publicIps == None: publicIps = []
81        params = self.pathlist("PublicIp", publicIps)
82        return DescribeAddressesResponse(self.make_request("DescribeAddresses", params))
83
84    def release_address(self, publicIp):
85        """Makes a C{ReleaseAddress} call.
86
87        @param publicIp: Address to release
88
89        """
90        params = { "PublicIp": publicIp }
91        return ReleaseAddressResponse(self.make_request("ReleaseAddress", params))
92
93    def associate_address(self, publicIp, instanceId):
94        """Makes an C{AssociateAddress} call.
95
96        @param publicIp: Address to associate
97        @param instanceId: Instance to associate address with
98
99        """
100        params = { "PublicIp": publicIp, "InstanceId": instanceId }
101        return AssociateAddressResponse(self.make_request("AssociateAddress", params))
102
103    def disassociate_address(self, publicIp):
104        """Makes a C{DisassociateAddress} call.
105
106        @param publicIp: Address to disassociate
107
108        """
109        params = { "PublicIp": publicIp }
110        return DisassociateAddressResponse(self.make_request("DisassociateAddress", params))
111
112    def describe_availability_zones(self, zoneNames=None):
113        """Makes a C{DescribeAvailabilityZones} call.
114
115        @param zoneNames: List of availability zones to describe.  If
116        empty or omitted, all availability zones will be described.
117
118        """
119        if zoneNames == None: zoneNames = []
120        params = self.pathlist("ZoneName", zoneNames)
121        return DescribeAvailabilityZonesResponse(self.make_request("DescribeAvailabilityZones", params))
122
123
124    def register_image(self, imageLocation):
125        """Makes a C{RegisterImage} call.
126
127        @param imageLocation: The location of the image manifest to
128        register in S3.
129
130        """
131        params = { "ImageLocation": imageLocation }
132        return RegisterImageResponse(self.make_request("RegisterImage", params))
133
134    def describe_images(self, imageIds=None, owners=None, executableBy=None):
135        """Makes a C{DescribeImages} call.
136
137        @param imageIds: List of images to describe.  If empty or omitted, all
138        images visible to the user are returned.
139
140        @param owners: List of users to filter returned images by
141        owner.  If empty or omitted, no filtering is done.
142
143        @param executableBy: List of users (or user groups) to filter
144        returned images by execution permissions.  If empty or
145        omitted, no filtering is done.
146
147        """
148        if imageIds == None: imageIds = []
149        if owners == None: owners = []
150        if executableBy == None: executableBy = []
151        params = self.pathlist("ImageId", imageIds)
152        params.update(self.pathlist("Owner", owners))
153        params.update(self.pathlist("ExecutableBy", executableBy))
154        return DescribeImagesResponse(self.make_request("DescribeImages", params))
155
156    def deregister_image(self, imageId):
157        """Makes a C{DeregisterImage} call.
158
159        @param imageId: The image id to deregister.
160
161        """
162        params = { "ImageId": imageId }
163        return DeregisterImageResponse(self.make_request("DeregisterImage", params))
164
165    def create_keypair(self, keyName):
166        """Makes a C{CreateKeypair} call.
167
168        @param keyName: Name for the new keypair.
169
170        """
171        params = { "KeyName": keyName }
172        return CreateKeyPairResponse(self.make_request("CreateKeyPair", params))
173
174    def describe_keypairs(self, keyNames=None):
175        """Makes a C{DescribeKeypairs} call.
176
177        @param keyNames: List of keypairs to describe.  If empty or
178        omitted, all keypairs are returned.
179
180        """
181        if keyNames == None: keyNames = []
182        params = self.pathlist("KeyName", keyNames)
183        return DescribeKeyPairsResponse(self.make_request("DescribeKeyPairs", params))
184
185    def delete_keypair(self, keyName):
186        """Makes a C{DeleteKeypair} call.
187
188        @param keyName: Name of keypair to delete.
189
190        """
191
192        params = { "KeyName": keyName }
193        return DeleteKeyPairResponse(self.make_request("DeleteKeyPair", params))
194
195    def run_instances(self, imageId, minCount=1, maxCount=1, keyName=None,
196                      groupIds=None, userData=None, base64Encode=True,
197                      addressingType=None, instanceType=None,
198                      availabilityZone=None, kernelId=None, ramdiskId=None,
199                      blockDeviceMapping=None):
200        """Makes a C{RunInstances} call.
201
202        @param imageId: AMI id to launch instances of.
203
204        @param minCount: Minimum number of instances to launch.  If
205        EC2 cannot launch at least this many, the call will fail.
206
207        @param maxCount: Maximum number of instances to launch.  EC2
208        will make a best-effort attempt to launch this many instances,
209        but will not fail unless fewer than C{minCount} are available.
210
211        @param keyName: Name of keypair to launch instances with.
212
213        @param groupIds: List of security groups to launch instances
214        in.
215
216        @param userData: String containing user data to inject into
217        launched instances.
218
219        @param base64Encode: Specifies whether C{userData} string
220        qshould be base64 encoded.  Defaults to True.
221
222        @param addressingType: Specifies the address scheme to use for
223        the instance. The supported (default) type is C{"public"}, or
224        C{None} for the default. See the "Instance Addressing" section
225        of the latest developer guide for more information.
226
227        @param availabilityZone: Specifies the availability zone to
228        launch in.
229
230        @param kernelId: Kernel to launch the instances with.
231
232        @param ramdiskId: Ramdisk to launch the instances with.
233
234        @param blockDeviceMapping: Specifies the virtual block device
235        mapping. This parameter is a list of two element lists or
236        tuples. The first element is the virtual name and the second
237        is the device name.
238
239        """
240        if groupIds == None: groupIds = []
241        params = {
242            "ImageId": imageId,
243            "MinCount": str(minCount),
244            "MaxCount": str(maxCount),
245            }
246        if addressingType is not None:
247            params["AddressingType"] = addressingType
248        params.update(self.pathlist("SecurityGroup", groupIds))
249        if userData is not None:
250            if base64Encode:
251                userData = base64.encodestring(userData)
252            params["UserData"] = userData
253        if keyName is not None:
254            params["KeyName"] = keyName
255        if availabilityZone is not None:
256            params["Placement.AvailabilityZone"] = availabilityZone
257        if kernelId is not None:
258            params["KernelId"] = kernelId
259        if ramdiskId is not None:
260            params["RamdiskId"] = ramdiskId
261        if instanceType is not None:
262            params["InstanceType"] = instanceType
263        if blockDeviceMapping is not None:
264            virtualNames, deviceNames = zip(*blockDeviceMapping)
265            i = 0
266            for value in arr:
267                i += 1
268                params["BlockDeviceMapping.%s.VirtualName" % (i)] = virtualNames[i]
269                params["BlockDeviceMapping.%s.DeviceName" % (i)] = deviceNames[i]
270
271        #return self.make_request("RunInstances", params)
272        return RunInstancesResponseGetReservationId(self.make_request("RunInstances", params))
273        #return RunInstancesResponse(self.make_request("RunInstances", params))
274
275    def describe_instances(self, instanceIds=[]):
276        """Makes a C{DescribeInstances} call.
277
278        @param instanceIds: List of instances to describe.  If empty
279        or omitted, all instances will be returned.
280
281        """
282        params = self.pathlist("InstanceId", instanceIds)
283        return DescribeInstancesResponse(self.make_request("DescribeInstances", params))
284
285    def get_console_output(self, instanceId):
286        """Makes a C{GetConsoleOutput} call.
287
288        @param instanceId: Instance from which to get console output.
289
290        """
291        params = { "InstanceId": instanceId }
292        return GetConsoleOutputResponse(self.make_request("GetConsoleOutput", params))
293
294    def reboot_instances(self, instanceIds):
295        """Makes a C{RebootInstances} call.
296
297        @param instanceIds: List of instances to reboot.
298
299        """
300        params = self.pathlist("InstanceId", instanceIds)
301        return RebootInstancesResponse(self.make_request("RebootInstances", params))
302
303    def terminate_instances(self, instanceIds):
304        """Makes a C{TerminateInstances} call.
305
306        @param instanceIds: List of instances to terminate.
307
308        """
309        params = self.pathlist("InstanceId", instanceIds)
310        return TerminateInstancesResponse(self.make_request("TerminateInstances", params))
311
312    def create_securitygroup(self, groupName, groupDescription):
313        """Makes a C{CreateSecurityGroup} call.
314
315        @param groupName: Name of group to create.
316
317        @param groupDescription: Brief description of security group.
318
319        """
320        params = {
321            "GroupName": groupName,
322            "GroupDescription": groupDescription
323            }
324        return CreateSecurityGroupResponse(self.make_request("CreateSecurityGroup", params))
325
326    def describe_securitygroups(self, groupNames=None):
327        """Makes a C{DescribeSecurityGroups} call.
328
329        @param groupNames: List of security groups to describe.  If
330        empty or omitted, all security groups will be described.
331
332        """
333        if groupNames == None: groupNames = []
334        params = self.pathlist("GroupName", groupNames)
335        return DescribeSecurityGroupsResponse(self.make_request("DescribeSecurityGroups", params))
336
337    def delete_securitygroup(self, groupName):
338        """Makes a C{DeleteSecurityGroup} call.
339
340        @param groupName: Name of security group to delete.
341
342        """
343        params = { "GroupName": groupName }
344        return DeleteSecurityGroupResponse(self.make_request("DeleteSecurityGroup", params))
345
346    def authorize(self, *args, **kwargs):
347        """Makes an C{AuthorizeSecurityGroupIngress} call.
348
349        L{authorize} and L{revoke} share parameter parsing code.
350        See L{auth_revoke_impl} for parameters.  Also, see API docs
351        for details of valid parameter combinations.
352
353        """
354        params = self.auth_revoke_impl(*args, **kwargs)
355        return AuthorizeSecurityGroupIngressResponse(self.make_request("AuthorizeSecurityGroupIngress", params))
356
357    def revoke(self, *args, **kwargs):
358        """Makes an C{RevokeSecurityGroupIngress} call.
359
360        L{authorize} and L{revoke} share parameter parsing code.
361        See L{auth_revoke_impl} for parameters.  Also, see API docs
362        for details of valid parameter combinations.
363
364        """
365        params = self.auth_revoke_impl(*args, **kwargs)
366        return RevokeSecurityGroupIngressResponse(self.make_request("RevokeSecurityGroupIngress", params))
367
368    def modify_image_attribute(self, imageId, attribute, operationType,
369                               **kwargs):
370        """Makes a C{ModifyImageAttribute} call.
371
372        @param imageId: AMI to modify attribute of.
373
374        @param attribute: Name of attribute to modify.
375
376        @param operationType: Operation to perform on attribute.
377
378        @param kwargs: Values for the attribute operation, documented below.
379
380        @kwarg userIds: List of userIds (valid with
381        C{'launchPermission'} attribute.)
382
383        @kwarg userGroups: List of userGroups (valid with
384        C{'launchPermission'} attribute.)
385
386        """
387        params = {
388            "ImageId": imageId,
389            "Attribute": attribute,
390            "OperationType": operationType
391            }
392        if attribute == "launchPermission":
393            if "userIds" in kwargs:
394                params.update(self.pathlist("UserId", kwargs["userIds"]))
395            if "userGroups" in kwargs:
396                params.update(self.pathlist("UserGroup", kwargs["userGroups"]))
397        elif attribute == "productCodes":
398            if "productCodes" in kwargs:
399                params.update(self.pathlist("ProductCode", kwargs["productCodes"]))
400        return ModifyImageAttributeResponse(self.make_request("ModifyImageAttribute", params))
401
402    def reset_image_attribute(self, imageId, attribute):
403        """Makes a C{ResetImageAttribute} call.
404
405        @param imageId: AMI to reset attribute of.
406
407        @param attribute: Name of attribute to reset.
408
409        """
410        params = { "ImageId": imageId, "Attribute": attribute }
411        return ResetImageAttributeResponse(self.make_request("ResetImageAttribute", params))
412
413    def describe_image_attribute(self, imageId, attribute):
414        """Makes a C{DescribeImageAttribute} call.
415
416        @param imageId: AMI to describe attribute of.
417
418        @param attribute: Name of attribute to describe.
419
420        """
421        params = { "ImageId": imageId, "Attribute": attribute }
422        return DescribeImageAttributeResponse(self.make_request("DescribeImageAttribute", params))
423
424    def auth_revoke_impl(self, groupName, ipProtocol=None, fromPort=None,
425                         toPort=None, cidrIp=None,
426                         sourceSecurityGroupName=None,
427                         sourceSecurityGroupOwnerId=None):
428        """Processes parameters for C{authorize} and C{revoke}.
429
430        @param groupName: Name of security group to modify.
431
432        @param ipProtocol: IP protocol in rule.  Valid vlaues are
433        C{'tcp'}, C{'udp'} and C{'icmp'}.
434
435        @param fromPort: Bottom of IP port range in rule.
436
437        @param toPort: Top of IP port range in rule.
438
439        @param cidrIp: CIDR IP range in rule.
440
441        @param sourceSecurityGroupName: Security group name in rule.
442
443        @param sourceSecurityGroupOwnerId: User id of security group
444        in rule.
445
446        """
447        params = { "GroupName": groupName }
448        if ipProtocol != None: params["IpProtocol"] = ipProtocol
449        if fromPort != None: params["FromPort"] = str(fromPort)
450        if toPort != None: params["ToPort"] = str(toPort)
451        if cidrIp != None: params["CidrIp"] = cidrIp
452        if sourceSecurityGroupName != None:
453            params["SourceSecurityGroupName"] = sourceSecurityGroupName
454        if sourceSecurityGroupOwnerId != None:
455            params["SourceSecurityGroupOwnerId"] = sourceSecurityGroupOwnerId
456        return params
457
458    def confirm_product_instance(self, productCode, instanceId):
459        """Makes a C{ConfirmProdcutInstance} call.
460
461        @param productCode: The product code to confirm
462
463        @param instanceId: The instance for which to confirm the product code.
464
465        """
466        params = { "ProcudtCode": productCode, "InstanceId": instanceId }
467        return ConfirmProductInstanceResponse(self.make_request("ConfirmProductInstance", params))
468
469    def make_request(self, action, params, data=''):
470        params["Action"] = action
471        if self.verbose:
472            print params
473
474        params["SignatureVersion"] = "1"
475        params["AWSAccessKeyId"] = self.aws_access_key_id
476        params["Version"] = API_VERSION
477        params["Timestamp"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
478
479        params = zip(params.keys(), params.values())
480        params.sort(key=lambda x: str.lower(x[0]))
481
482        sig = self.get_aws_auth_param(params, self.aws_secret_access_key)
483
484        path = "?%s&Signature=%s" % (
485            "&".join(["=".join([param[0], urllib.quote_plus(param[1])]) for param in params]),
486            sig)
487
488        if self.verbose:
489            print path
490
491        headers = {
492            'User-Agent': 'ec2-python-query 1.2-%s' % (RELEASE_VERSION)
493            }
494
495        self.connection.request("GET", "/%s" % path, data, headers)
496        return self.connection.getresponse()
497
498    def get_aws_auth_param(self, params, aws_secret_access_key):
499        canonical_string = "".join(["".join(param) for param in params])
500        return self.encode(aws_secret_access_key, canonical_string)
501
502    # computes the base64'ed hmac-sha hash of the canonical string and
503    # the secret access key, optionally urlencoding the result
504
505    def encode(self, aws_secret_access_key, str, urlencode=True):
506        b64_hmac = base64.encodestring(hmac.new(aws_secret_access_key, str, hashlib.sha1).digest()).strip()
507        if urlencode:
508            return urllib.quote_plus(b64_hmac)
509        else:
510            return b64_hmac
511
512
513
514class Response(object):
515    """Base class for XML response parsers.
516
517    This class does everything except the API-call dependent parsing,
518    which is handled in the child classes below.  Each child class
519    should override the L{parse} method.
520
521    """
522
523    ERROR_XPATH = "Errors/Error"
524    NAMESPACE = "http://ec2.amazonaws.com/doc/%s/" % (API_VERSION)
525
526    def __init__(self, http_response):
527        self.http_response = http_response
528        self.http_xml = http_response.read()
529        self.is_error = False
530        if http_response.status == 200:
531            self.structure = self.parse()
532        else:
533            self.is_error = True
534            self.structure = self.parse_error()
535
536    def parse_error(self):
537        doc = ET.XML(self.http_xml)
538        element = doc.find(self.ERROR_XPATH)
539        errorCode = element.findtext("Code")
540        errorMessage = element.findtext("Message")
541        return [["%s: %s" % (errorCode, errorMessage)]]
542
543    def parse(self):
544        # Placeholder -- this method should be overridden in child classes.
545        return None
546
547    def __str__(self):
548        return "\n".join(["\t".join(line) for line in self.structure])
549
550    def fixxpath(self, xpath):
551        # ElementTree wants namespaces in its xpaths, so here we add them.
552        return "/".join(["{%s}%s" % (self.NAMESPACE, e) for e in xpath.split("/")])
553
554    def findtext(self, element, xpath):
555        return element.findtext(self.fixxpath(xpath))
556
557    def findall(self, element, xpath):
558        return element.findall(self.fixxpath(xpath))
559
560    def find(self, element, xpath):
561        return element.find(self.fixxpath(xpath))
562
563    def fixnone(self, strnone):
564        if strnone is None: return ""
565        return strnone
566
567
568class DescribeImagesResponse(Response):
569    """Response parser class for C{DescribeImages} API call."""
570    ELEMENT_XPATH = "imagesSet/item"
571    def translate_isPublic(self, isPublic):
572        return { "true": "public",
573                 "false": "private" }[isPublic]
574
575    def parse(self):
576        doc = ET.XML(self.http_xml)
577        lines = []
578        for element in self.findall(doc, self.ELEMENT_XPATH):
579            lines.append(["IMAGE",
580                          self.findtext(element, "imageId"),
581                          self.findtext(element, "imageLocation"),
582                          self.findtext(element, "imageOwnerId"),
583                          self.findtext(element, "imageState"),
584                          self.translate_isPublic(self.findtext(element, "isPublic")),
585                          self.findtext(element, "architecture"),
586                          self.findtext(element, "imageType"),
587                          self.fixnone(self.findtext(element, "kernelId")),
588                          self.fixnone(self.findtext(element, "ramdiskId")),
589                          ])
590        return lines
591
592
593class RegisterImageResponse(Response):
594    """Response parser class for C{RegisterImage} API call."""
595    ELEMENT_XPATH = "imageId"
596    def parse(self):
597        doc = ET.XML(self.http_xml)
598        return [["IMAGE", self.findtext(doc, self.ELEMENT_XPATH)]]
599
600
601class DeregisterImageResponse(Response):
602    """Response parser class for C{DeregisterImage} API call."""
603    def parse(self):
604        # If we don't get an error, the deregistration succeeded.
605        return [["Image deregistered."]]
606
607
608class CreateKeyPairResponse(Response):
609    """Response parser class for C{CreateKeyPair} API call."""
610    def parse(self):
611        doc = ET.XML(self.http_xml)
612        keyName = self.findtext(doc, "keyName")
613        keyFingerprint = self.findtext(doc, "keyFingerprint")
614        keyMaterial = self.findtext(doc, "keyMaterial")
615        return [["KEYPAIR", keyName, keyFingerprint], [keyMaterial]]
616
617
618class DescribeKeyPairsResponse(Response):
619    """Response parser class for C{DescribeKeyPairs} API call."""
620    ELEMENT_XPATH = "keySet/item"
621    def parse(self):
622        doc = ET.XML(self.http_xml)
623        lines = []
624        for element in self.findall(doc, self.ELEMENT_XPATH):
625            keyName = self.findtext(element, "keyName")
626            keyFingerprint = self.findtext(element, "keyFingerprint")
627            lines.append(["KEYPAIR", keyName, keyFingerprint])
628        return lines
629
630
631class DeleteKeyPairResponse(Response):
632    """Response parser class for C{DeleteKeyPair} API call."""
633    def parse(self):
634        # If we don't get an error, the deletion succeeded.
635        return [["Keypair deleted."]]
636
637
638class InstanceSetResponse(Response):
639    """ Response containing instance set items """
640    def parseInstanceSet(self, root):
641        """ Parse a set of instanceSet/item nodes """
642        lines = []
643        for element in self.findall(root, "instancesSet/item"):
644            lines.append(["INSTANCE",
645                          self.findtext(element, "instanceId"),
646                          self.findtext(element, "imageId"),
647                          self.findtext(element, "dnsName"),
648                          self.findtext(element, "privateDnsName"),
649                          self.findtext(element, "instanceState/name"),
650                          self.fixnone(self.findtext(element, "keyName")),
651                          self.findtext(element, "amiLaunchIndex"),
652                          ",".join([p.text for p in self.findall(element, "productCodesSet/item/productCode")]),
653                          self.findtext(element, "instanceType"),
654                          self.findtext(element, "launchTime"),
655                          self.findtext(element, "placement/availabilityZone"),
656                          self.fixnone(self.findtext(element, "kernelId")),
657                          self.fixnone(self.findtext(element, "ramdiskId")),
658                          ])
659        return lines
660
661class RunInstancesResponse(InstanceSetResponse):
662    """Response parser class for C{RunInstances} API call."""
663    def parse(self):
664        doc = ET.XML(self.http_xml)
665        lines = []
666        reservationId = self.findtext(doc, "reservationId")
667        ownerId = self.findtext(doc, "ownerId")
668        groups = [g.text for g in self.findall(doc, "groupSet/item/groupId")]
669        lines.append(["RESERVATION", reservationId, ownerId, ",".join(groups)])
670        lines.extend(self.parseInstanceSet(doc))
671        return lines
672
673class DescribeInstancesResponse(InstanceSetResponse):
674    """Response parser class for C{DescribeInstances} API call."""
675    ELEMENT_XPATH = "reservationSet/item"
676    def parse(self):
677        doc = ET.XML(self.http_xml)
678        lines = []
679        for rootelement in self.findall(doc, self.ELEMENT_XPATH):
680            reservationId = self.findtext(rootelement, "reservationId")
681            ownerId = self.findtext(rootelement, "ownerId")
682            groups = [g.text for g in self.findall(rootelement, "groupSet/item/groupId")]
683            lines.append(["RESERVATION", reservationId, ownerId, ",".join(groups)])
684            lines.extend(self.parseInstanceSet(rootelement))
685        return lines
686
687
688class GetConsoleOutputResponse(Response):
689    def parse(self):
690        doc = ET.XML(self.http_xml)
691        return [ [self.findtext(doc, "instanceId")],
692                 [self.findtext(doc, "timestamp")],
693                 [self.findtext(doc, "output")] ]
694
695
696class TerminateInstancesResponse(Response):
697    """Response parser class for C{TerminateInstances} API call."""
698    ELEMENT_XPATH = "instancesSet/item"
699    def parse(self):
700        doc = ET.XML(self.http_xml)
701        lines = []
702        for element in self.findall(doc, self.ELEMENT_XPATH):
703            instanceId = self.findtext(element, "instanceId")
704            shutdownState = self.findtext(element, "shutdownState/name")
705            previousState = self.findtext(element, "previousState/name")
706            # Only for debug mode, which we don't support yet:
707            shutdownStateCode = self.findtext(element, "shutdownState/code")
708            previousStateCode = self.findtext(element, "previousState/code")
709            lines.append(["INSTANCE", instanceId, previousState, shutdownState])
710        return lines
711
712
713class RebootInstancesResponse(Response):
714    """Response parser class for C{RebootInstances} API call."""
715    def parse(self):
716        # If we don't get an error, the creation succeeded.
717        return [["Instances rebooted."]]
718
719
720class CreateSecurityGroupResponse(Response):
721    """Response parser class for C{CreateSecurityGroup} API call."""
722    def parse(self):
723        # If we don't get an error, the creation succeeded.
724        return [["Security Group created."]]
725
726
727class DescribeSecurityGroupsResponse(Response):
728    """Response parser class for C{DescribeSecurityGroups} API call."""
729    ELEMENT_XPATH = "securityGroupInfo/item"
730    def parse(self):
731        doc = ET.XML(self.http_xml)
732        lines = []
733        for rootelement in self.findall(doc, self.ELEMENT_XPATH):
734            groupName = self.findtext(rootelement, "groupName")
735            ownerId = self.findtext(rootelement, "ownerId")
736            groupDescription = self.findtext(rootelement, "groupDescription")
737            lines.append(["GROUP", ownerId, groupName, groupDescription])
738            for element in self.findall(rootelement, "ipPermissions/item"):
739                ipProtocol = self.findtext(element, "ipProtocol")
740                fromPort = self.findtext(element, "fromPort")
741                toPort = self.findtext(element, "toPort")
742                permList = [
743                    "PERMISSION",
744                    ownerId,
745                    groupName,
746                    "ALLOWS",
747                    ipProtocol,
748                    fromPort,
749                    toPort,
750                    "FROM"
751                    ]
752                for subelement in self.findall(element, "groups/item"):
753                    userId = self.findtext(subelement, "userId")
754                    targetGroupName = self.findtext(subelement, "groupName")
755                    lines.append(permList + ["USER", userId, "GRPNAME", targetGroupName])
756                for subelement in self.findall(element, "ipRanges/item"):
757                    cidrIp = self.findtext(subelement, "cidrIp")
758                    lines.append(permList + ["CIDR", cidrIp])
759        return lines
760
761
762class DeleteSecurityGroupResponse(Response):
763    """Response parser class for C{DeleteSecurityGroup} API call."""
764    def parse(self):
765        # If we don't get an error, the deletion succeeded.
766        return [["Security Group deleted."]]
767
768
769class AuthorizeSecurityGroupIngressResponse(Response):
770    """Response parser class for C{AuthorizeSecurityGroupIngress} API call."""
771    def parse(self):
772        # If we don't get an error, the authorization succeeded.
773        return [["Ingress authorized."]]
774
775
776class RevokeSecurityGroupIngressResponse(Response):
777    """Response parser class for C{RevokeSecurityGroupIngress} API call."""
778    def parse(self):
779        # If we don't get an error, the revocation succeeded.
780        return [["Ingress revoked."]]
781
782
783class ModifyImageAttributeResponse(Response):
784    """Response parser class for C{ModifyImageAttribute} API call."""
785    def parse(self):
786        # If we don't get an error, modification succeeded.
787        return [["Image attribute modified."]]
788
789
790class ResetImageAttributeResponse(Response):
791    """Response parser class for C{ResetImageAttribute} API call."""
792    def parse(self):
793        # If we don't get an error, reset succeeded.
794        return [["Image attribute reset."]]
795
796
797class DescribeImageAttributeResponse(Response):
798    """Response parser class for C{DescribeImageAttribute} API call."""
799    def parse(self):
800        doc = ET.XML(self.http_xml)
801        lines = []
802
803        imageId = self.findtext(doc, "imageId")
804
805        # Handle launchPermission attributes:
806        element = self.find(doc, "launchPermission/item")
807        if element != None:
808            for subelement in element.getchildren():
809                lines.append([
810                    "launchPermission",
811                    imageId,
812                    subelement.tag.split("}")[1],
813                    subelement.text
814                    ])
815
816        # Handle launchPermission attributes:
817        element = self.find(doc, "productCodes/item")
818        if element != None:
819            for subelement in element.getchildren():
820                lines.append([
821                    "productCode",
822                    imageId,
823                    subelement.text
824                    ])
825
826        return lines
827
828class AllocateAddressResponse(Response):
829    """Response parser class for C{AllocateAddress} API call."""
830    def parse(self):
831        doc = ET.XML(self.http_xml)
832        publicIp = self.findtext(doc, "publicIp")
833        return [["ADDRESS", publicIp]]
834
835class DescribeAddressesResponse(Response):
836    """Response parser class for C{DescribeAddresses} API call."""
837    ELEMENT_XPATH = "addressesSet/item"
838    def parse(self):
839        doc = ET.XML(self.http_xml)
840        lines = []
841        for element in self.findall(doc, self.ELEMENT_XPATH):
842            publicIp = self.findtext(element, "publicIp")
843            instanceId = self.findtext(element, "instanceId")
844            lines.append(["ADDRESS", publicIp, instanceId])
845        return lines
846
847class ReleaseAddressResponse(Response):
848    """Response parser class for C{ReleaseAddress} API call."""
849    def parse(self):
850        # If we don't get an error, reset succeeded.
851        return [["Address released."]]
852
853class AssociateAddressResponse(Response):
854    """Response parser class for C{AssociateAddress} API call."""
855    def parse(self):
856        # If we don't get an error, reset succeeded.
857        return [["Address associated."]]
858
859class DisassociateAddressResponse(Response):
860    """Response parser class for C{DisassociateAddress} API call."""
861    def parse(self):
862        # If we don't get an error, reset succeeded.
863        return [["Address disassociated."]]
864
865class ConfirmProductInstanceResponse(Response):
866    """Response parser class for C{ConfirmProductInstance} API call."""
867    def parse(self):
868        doc = ET.XML(self.http_xml)
869        result = self.findtext(doc, "result")
870        lines = [[result]]
871        if result == "true":
872            lines[0].append(self.findtext(doc, "ownerId"))
873
874class DescribeAvailabilityZonesResponse(Response):
875    """Response parser class for C{DescribeAvailabilityZones} API call."""
876    ELEMENT_XPATH = "availabilityZoneInfo/item"
877    def parse(self):
878        doc = ET.XML(self.http_xml)
879        lines = []
880        for element in self.findall(doc, self.ELEMENT_XPATH):
881            zoneName = self.findtext(element, "zoneName")
882            zoneState = self.findtext(element, "zoneState")
883            lines.append(["AVAILABILITYZONE", zoneName, zoneState])
884        return lines
885