You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@libcloud.apache.org by to...@apache.org on 2013/01/11 14:22:44 UTC

svn commit: r1432038 - in /libcloud/trunk/libcloud: common/aws.py compute/drivers/ec2.py dns/drivers/route53.py dns/types.py loadbalancer/drivers/elb.py

Author: tomaz
Date: Fri Jan 11 13:22:43 2013
New Revision: 1432038

URL: http://svn.apache.org/viewvc?rev=1432038&view=rev
Log:
Remove duplicated authentication code in AWS drivers and refactor common
functionality into a new class.

Part of LIBCLOUD-169.

Modified:
    libcloud/trunk/libcloud/common/aws.py
    libcloud/trunk/libcloud/compute/drivers/ec2.py
    libcloud/trunk/libcloud/dns/drivers/route53.py
    libcloud/trunk/libcloud/dns/types.py
    libcloud/trunk/libcloud/loadbalancer/drivers/elb.py

Modified: libcloud/trunk/libcloud/common/aws.py
URL: http://svn.apache.org/viewvc/libcloud/trunk/libcloud/common/aws.py?rev=1432038&r1=1432037&r2=1432038&view=diff
==============================================================================
--- libcloud/trunk/libcloud/common/aws.py (original)
+++ libcloud/trunk/libcloud/common/aws.py Fri Jan 11 13:22:43 2013
@@ -13,8 +13,129 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from libcloud.common.base import XmlResponse
+import base64
+import hmac
+import time
+from hashlib import sha256
+from xml.etree import ElementTree as ET
+
+from libcloud.common.base import ConnectionUserAndKey, XmlResponse
+from libcloud.common.types import InvalidCredsError, MalformedResponseError
+from libcloud.utils.py3 import b, httplib, urlquote
+from libcloud.utils.xml import findtext, findall
 
 
 class AWSBaseResponse(XmlResponse):
     pass
+
+
+class AWSGenericResponse(AWSBaseResponse):
+    # There are multiple error messages in AWS, but they all have an Error node
+    # with Code and Message child nodes. Xpath to select them
+    # None if the root node *is* the Error node
+    xpath = None
+
+    # This dict maps <Error><Code>CodeName</Code></Error> to a specific
+    # exception class that is raised immediately.
+    # If a custom exception class is not defined, errors are accumulated and
+    # returned from the parse_error method.
+    expections = {}
+
+    def success(self):
+        return self.status in [httplib.OK, httplib.CREATED, httplib.ACCEPTED]
+
+    def parse_error(self):
+        context = self.connection.context
+        status = int(self.status)
+
+        # FIXME: Probably ditch this as the forbidden message will have
+        # corresponding XML.
+        if status == httplib.FORBIDDEN:
+            if not self.body:
+                raise InvalidCredsError(str(self.status) + ': ' + self.error)
+            else:
+                raise InvalidCredsError(self.body)
+
+        try:
+            body = ET.XML(self.body)
+        except Exception:
+            raise MalformedResponseError('Failed to parse XML',
+                                         body=self.body,
+                                         driver=self.connection.driver)
+
+        if self.xpath:
+            errs = findall(element=body, xpath=self.xpath,
+                           namespace=self.namespace)
+        else:
+            errs = [body]
+
+        msgs = []
+        for err in errs:
+            code = findtext(element=err, xpath='Code',
+                            namespace=self.namespace)
+            message = findtext(element=err, xpath='Message',
+                               namespace=self.namespace)
+
+            exceptionCls = self.exceptions.get(code, None)
+
+            if exceptionCls is None:
+                msgs.append('%s: %s' % (code, message))
+                continue
+
+            # Custom exception class is defined, immediately throw an exception
+            params = {}
+            if hasattr(exceptionCls, 'kwargs'):
+                for key in exceptionCls.kwargs:
+                    if key in context:
+                        params[key] = context[key]
+
+            raise exceptionCls(value=message, driver=self.connection.driver,
+                               **params)
+
+        return "\n".join(msgs)
+
+
+class SignedAWSConnection(ConnectionUserAndKey):
+    def add_default_params(self, params):
+        params['SignatureVersion'] = '2'
+        params['SignatureMethod'] = 'HmacSHA256'
+        params['AWSAccessKeyId'] = self.user_id
+        params['Version'] = self.version
+        params['Timestamp'] = time.strftime('%Y-%m-%dT%H:%M:%SZ',
+                                            time.gmtime())
+        params['Signature'] = self._get_aws_auth_param(params, self.key,
+                                                       self.action)
+        return params
+
+    def _get_aws_auth_param(self, params, secret_key, path='/'):
+        """
+        Creates the signature required for AWS, per
+        http://bit.ly/aR7GaQ [docs.amazonwebservices.com]:
+
+        StringToSign = HTTPVerb + "\n" +
+                       ValueOfHostHeaderInLowercase + "\n" +
+                       HTTPRequestURI + "\n" +
+                       CanonicalizedQueryString <from the preceding step>
+        """
+        keys = list(params.keys())
+        keys.sort()
+        pairs = []
+        for key in keys:
+            pairs.append(urlquote(key, safe='') + '=' +
+                         urlquote(params[key], safe='-_~'))
+
+        qs = '&'.join(pairs)
+
+        hostname = self.host
+        if (self.secure and self.port != 443) or \
+           (not self.secure and self.port != 80):
+            hostname += ":" + str(self.port)
+
+        string_to_sign = '\n'.join(('GET', hostname, path, qs))
+
+        b64_hmac = base64.b64encode(
+            hmac.new(b(secret_key), b(string_to_sign),
+                     digestmod=sha256).digest()
+        )
+
+        return b64_hmac.decode('utf-8')

Modified: libcloud/trunk/libcloud/compute/drivers/ec2.py
URL: http://svn.apache.org/viewvc/libcloud/trunk/libcloud/compute/drivers/ec2.py?rev=1432038&r1=1432037&r2=1432038&view=diff
==============================================================================
--- libcloud/trunk/libcloud/compute/drivers/ec2.py (original)
+++ libcloud/trunk/libcloud/compute/drivers/ec2.py Fri Jan 11 13:22:43 2013
@@ -21,20 +21,15 @@ from __future__ import with_statement
 
 import sys
 import base64
-import hmac
 import os
-import time
 import copy
 
-from hashlib import sha256
 from xml.etree import ElementTree as ET
 
-from libcloud.utils.py3 import urlquote
 from libcloud.utils.py3 import b
 
 from libcloud.utils.xml import fixxpath, findtext, findattr, findall
-from libcloud.common.base import ConnectionUserAndKey
-from libcloud.common.aws import AWSBaseResponse
+from libcloud.common.aws import AWSBaseResponse, SignedAWSConnection
 from libcloud.common.types import (InvalidCredsError, MalformedResponseError,
                                    LibcloudError)
 from libcloud.compute.providers import Provider
@@ -373,57 +368,15 @@ class EC2Response(AWSBaseResponse):
         return "\n".join(err_list)
 
 
-class EC2Connection(ConnectionUserAndKey):
+class EC2Connection(SignedAWSConnection):
     """
     Represents a single connection to the EC2 Endpoint.
     """
 
+    version = API_VERSION
     host = REGION_DETAILS['us-east-1']['endpoint']
     responseCls = EC2Response
 
-    def add_default_params(self, params):
-        params['SignatureVersion'] = '2'
-        params['SignatureMethod'] = 'HmacSHA256'
-        params['AWSAccessKeyId'] = self.user_id
-        params['Version'] = API_VERSION
-        params['Timestamp'] = time.strftime('%Y-%m-%dT%H:%M:%SZ',
-                                            time.gmtime())
-        params['Signature'] = self._get_aws_auth_param(params, self.key,
-                                                       self.action)
-        return params
-
-    def _get_aws_auth_param(self, params, secret_key, path='/'):
-        """
-        Creates the signature required for AWS, per
-        http://bit.ly/aR7GaQ [docs.amazonwebservices.com]:
-
-        StringToSign = HTTPVerb + "\n" +
-                       ValueOfHostHeaderInLowercase + "\n" +
-                       HTTPRequestURI + "\n" +
-                       CanonicalizedQueryString <from the preceding step>
-        """
-        keys = list(params.keys())
-        keys.sort()
-        pairs = []
-        for key in keys:
-            pairs.append(urlquote(key, safe='') + '=' +
-                         urlquote(params[key], safe='-_~'))
-
-        qs = '&'.join(pairs)
-
-        hostname = self.host
-        if (self.secure and self.port != 443) or \
-           (not self.secure and self.port != 80):
-            hostname += ":" + str(self.port)
-
-        string_to_sign = '\n'.join(('GET', hostname, path, qs))
-
-        b64_hmac = base64.b64encode(
-            hmac.new(b(secret_key), b(string_to_sign),
-                     digestmod=sha256).digest()
-        )
-        return b64_hmac.decode('utf-8')
-
 
 class ExEC2AvailabilityZone(object):
     """

Modified: libcloud/trunk/libcloud/dns/drivers/route53.py
URL: http://svn.apache.org/viewvc/libcloud/trunk/libcloud/dns/drivers/route53.py?rev=1432038&r1=1432037&r2=1432038&view=diff
==============================================================================
--- libcloud/trunk/libcloud/dns/drivers/route53.py (original)
+++ libcloud/trunk/libcloud/dns/drivers/route53.py Fri Jan 11 13:22:43 2013
@@ -32,9 +32,8 @@ from libcloud.utils.xml import findtext,
 from libcloud.dns.types import Provider, RecordType
 from libcloud.dns.types import ZoneDoesNotExistError, RecordDoesNotExistError
 from libcloud.dns.base import DNSDriver, Zone, Record
-from libcloud.common.types import InvalidCredsError
 from libcloud.common.types import LibcloudError
-from libcloud.common.aws import AWSBaseResponse
+from libcloud.common.aws import AWSGenericResponse
 from libcloud.common.base import ConnectionUserAndKey
 
 
@@ -49,44 +48,18 @@ class InvalidChangeBatch(LibcloudError):
     pass
 
 
-class Route53DNSResponse(AWSBaseResponse):
+class Route53DNSResponse(AWSGenericResponse):
     """
     Amazon Route53 response class.
     """
-    def success(self):
-        return self.status in [httplib.OK, httplib.CREATED, httplib.ACCEPTED]
 
-    def parse_error(self):
-        context = self.connection.context
-        status = int(self.status)
-
-        if status == httplib.FORBIDDEN:
-            if not self.body:
-                raise InvalidCredsError(str(self.status) + ': ' + self.error)
-            else:
-                raise InvalidCredsError(self.body)
+    namespace = NAMESPACE
+    xpath = 'Error'
 
-        try:
-            body = ET.XML(self.body)
-        except Exception:
-            raise MalformedResponseError('Failed to parse XML',
-                                         body=self.body, driver=self.driver)
-
-        errs = findall(element=body, xpath='Error', namespace=NAMESPACE)
-
-        if errs:
-            t, code, message = errs[0].getchildren()
-
-            if code.text == 'NoSuchHostedZone':
-                zone_id = context.get('zone_id', None)
-                raise ZoneDoesNotExistError(value=message.text, driver=self,
-                                            zone_id=zone_id)
-            elif code.text == 'InvalidChangeBatch':
-                raise InvalidChangeBatch(value=message.text)
-            else:
-                return message.text
-
-        return self.body
+    exceptions = {
+        'NoSuchHostedZone': ZoneDoesNotExistError,
+        'InvalidChangeBatch': InvalidChangeBatch,
+    }
 
 
 class Route53Connection(ConnectionUserAndKey):
@@ -268,7 +241,7 @@ class Route53DNSDriver(DNSDriver):
         uri = API_ROOT + 'hostedzone/' + zone.id + '/rrset'
         data = ET.tostring(changeset)
         self.connection.set_context({'zone_id': zone.id})
-        rsp = self.connection.request(uri, method='POST', data=data).object
+        self.connection.request(uri, method='POST', data=data)
 
     def _to_zones(self, data):
         zones = []

Modified: libcloud/trunk/libcloud/dns/types.py
URL: http://svn.apache.org/viewvc/libcloud/trunk/libcloud/dns/types.py?rev=1432038&r1=1432037&r2=1432038&view=diff
==============================================================================
--- libcloud/trunk/libcloud/dns/types.py (original)
+++ libcloud/trunk/libcloud/dns/types.py Fri Jan 11 13:22:43 2013
@@ -66,7 +66,8 @@ class RecordType(object):
 
 class ZoneError(LibcloudError):
     error_type = 'ZoneError'
-
+    kwargs = ('zone_id', )
+    
     def __init__(self, value, driver, zone_id):
         self.zone_id = zone_id
         super(ZoneError, self).__init__(value=value, driver=driver)

Modified: libcloud/trunk/libcloud/loadbalancer/drivers/elb.py
URL: http://svn.apache.org/viewvc/libcloud/trunk/libcloud/loadbalancer/drivers/elb.py?rev=1432038&r1=1432037&r2=1432038&view=diff
==============================================================================
--- libcloud/trunk/libcloud/loadbalancer/drivers/elb.py (original)
+++ libcloud/trunk/libcloud/loadbalancer/drivers/elb.py Fri Jan 11 13:22:43 2013
@@ -17,19 +17,11 @@ __all__ = [
     'ElasticLBDriver'
 ]
 
-import base64
-import hmac
-import time
 
-from hashlib import sha256
-
-from libcloud.utils.py3 import httplib, urlquote, b
 from libcloud.utils.xml import findtext, findall
 from libcloud.loadbalancer.types import State
 from libcloud.loadbalancer.base import Driver, LoadBalancer, Member
-from libcloud.common.types import InvalidCredsError
-from libcloud.common.aws import AWSBaseResponse
-from libcloud.common.base import ConnectionUserAndKey
+from libcloud.common.aws import AWSGenericResponse, SignedAWSConnection
 
 
 VERSION = '2012-06-01'
@@ -38,70 +30,18 @@ ROOT = '/%s/' % (VERSION)
 NS = 'http://elasticloadbalancing.amazonaws.com/doc/%s/' % (VERSION, )
 
 
-class ELBResponse(AWSBaseResponse):
+class ELBResponse(AWSGenericResponse):
     """
     Amazon ELB response class.
     """
-    def success(self):
-        return self.status in [httplib.OK, httplib.CREATED, httplib.ACCEPTED]
-
-    def parse_error(self):
-        status = int(self.status)
-
-        if status == httplib.FORBIDDEN:
-            if not self.body:
-                raise InvalidCredsError(str(self.status) + ': ' + self.error)
-            else:
-                raise InvalidCredsError(self.body)
+    namespace = NS
 
 
-class ELBConnection(ConnectionUserAndKey):
+class ELBConnection(SignedAWSConnection):
+    version = VERSION
     host = HOST
     responseCls = ELBResponse
 
-    def add_default_params(self, params):
-        params['SignatureVersion'] = '2'
-        params['SignatureMethod'] = 'HmacSHA256'
-        params['AWSAccessKeyId'] = self.user_id
-        params['Version'] = VERSION
-        params['Timestamp'] = time.strftime('%Y-%m-%dT%H:%M:%SZ',
-                                            time.gmtime())
-        params['Signature'] = self._get_aws_auth_param(params, self.key,
-                                                       self.action)
-        return params
-
-    def _get_aws_auth_param(self, params, secret_key, path='/'):
-        """
-        Creates the signature required for AWS, per
-        http://bit.ly/aR7GaQ [docs.amazonwebservices.com]:
-
-        StringToSign = HTTPVerb + "\n" +
-                       ValueOfHostHeaderInLowercase + "\n" +
-                       HTTPRequestURI + "\n" +
-                       CanonicalizedQueryString <from the preceding step>
-        """
-        keys = list(params.keys())
-        keys.sort()
-        pairs = []
-        for key in keys:
-            pairs.append(urlquote(key, safe='') + '=' +
-                         urlquote(params[key], safe='-_~'))
-
-        qs = '&'.join(pairs)
-
-        hostname = self.host
-        if (self.secure and self.port != 443) or \
-           (not self.secure and self.port != 80):
-            hostname += ":" + str(self.port)
-
-        string_to_sign = '\n'.join(('GET', hostname, path, qs))
-
-        b64_hmac = base64.b64encode(
-            hmac.new(b(secret_key), b(string_to_sign),
-                     digestmod=sha256).digest()
-        )
-        return b64_hmac.decode('utf-8')
-
 
 class ElasticLBDriver(Driver):
     name = 'ELB'