From a38db60fe4a3b99e6aa6d233a70471563dd7fbf9 Mon Sep 17 00:00:00 2001 From: John Carr Date: Thu, 31 Jan 2013 20:24:43 +0000 Subject: [PATCH 1/6] Move common xmlrpc code out of gandi driver --- libcloud/common/gandi.py | 42 ++---------- libcloud/common/xmlrpc.py | 121 ++++++++++++++++++++++++++++++++++ libcloud/dns/drivers/gandi.py | 10 +-- 3 files changed, 130 insertions(+), 43 deletions(-) create mode 100644 libcloud/common/xmlrpc.py diff --git a/libcloud/common/gandi.py b/libcloud/common/gandi.py index 0761b48526..b88add6e64 100644 --- a/libcloud/common/gandi.py +++ b/libcloud/common/gandi.py @@ -20,14 +20,13 @@ import hashlib import sys -from libcloud.utils.py3 import xmlrpclib from libcloud.utils.py3 import b -from libcloud.common.base import Response, ConnectionKey +from libcloud.common.base import ConnectionKey +from libcloud.common.xmlrpc import XMLRPCResponse, XMLRPCConnection # Global constants - DEFAULT_TIMEOUT = 600 # operation pooling max seconds DEFAULT_INTERVAL = 20 # seconds between 2 operation.info @@ -43,33 +42,13 @@ def __repr__(self): return '' % (self.args[0], self.args[1]) -class GandiResponse(Response): +class GandiResponse(XMLRPCResponse): """ A Base Gandi Response class to derive from. """ - def parse_body(self): - try: - params, methodname = xmlrpclib.loads(self.body) - - if len(params) == 1: - return params[0] - - return params - except xmlrpclib.Fault: - e = sys.exc_info()[1] - self.parse_error(e.faultCode, e.faultString) - raise GandiException(1000, e) - - def parse_error(self, code=None, message=None): - """ - This hook allows you to inspect any xmlrpclib errors and - potentially raise a more useful and specific exception. - """ - pass - -class GandiConnection(ConnectionKey): +class GandiConnection(XMLRPCConnection, ConnectionKey): """ Connection class for the Gandi driver """ @@ -77,17 +56,8 @@ class GandiConnection(ConnectionKey): responseCls = GandiResponse host = 'rpc.gandi.net' - def request(self, method, *args): - """ Request xmlrpc method with given args""" - args = (self.key, ) + args - data = xmlrpclib.dumps(args, methodname=method, allow_none=True) - headers = { - 'Content-Type': 'text/xml', - } - return super(GandiConnection, self).request('/xmlrpc/', - data=data, - headers=headers, - method='POST') + def pre_marshall_hook(self, method_name, args): + return (self.key, ) + args class BaseGandiDriver(object): diff --git a/libcloud/common/xmlrpc.py b/libcloud/common/xmlrpc.py new file mode 100644 index 0000000000..aad03ea51d --- /dev/null +++ b/libcloud/common/xmlrpc.py @@ -0,0 +1,121 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Base classes for working with xmlrpc APIs +""" + +import sys + +from libcloud.utils.py3 import xmlrpclib +from libcloud.common.base import Response, ConnectionKey + + +class ProtocolError(Exception): + pass + + +class ErrorCodeMixin(object): + """ + This is a helper for API's that have a well defined collection of error + codes that are easily parsed out of error messages. It acts as a factory: + it finds the right exception for the error code, fetches any parameters it + needs from the context and raises it. + """ + + exceptions = {} + + def raise_exception_for_error(self, error_code, message): + exceptionCls = self.exceptions.get(error_code, None) + if exceptionCls is None: + return + context = self.connection.context + driver = self.connection.driver + params = {} + if hasattr(exceptionCls, 'kwargs'): + for key in exceptionCls.kwargs: + if key in context: + params[key] = context[key] + raise exceptionCls(value=message, driver=driver, **params) + + +class XMLRPCResponse(ErrorCodeMixin, Response): + + def success(self): + return self.status == 200 + + def parse_body(self): + try: + body, methodname = xmlrpclib.loads(self.body) + if len(body) == 1: + body = body[0] + return body + except xmlrpclib.Fault: + e = sys.exc_info()[1] + self.raise_exception_for_error(e.faultCode, e.faultString) + raise Exception("%s: %s" % (e.faultCode, e.faultString)) + + def parse_error(self): + msg = 'Server returned an invalid xmlrpc response (%d)' % self.status + raise ProtocolError(msg) + + +class XMLRPCConnection(ConnectionKey): + """ + Connection class which can call XMLRPC based API's. + + This class uses the xmlrpclib marshalling and demarshalling code but uses + the http transports provided by libcloud giving it better certificate + validation and debugging helpers than the core client library. + """ + + responseCls = XMLRPCResponse + + def add_default_headers(self, headers): + headers['Content-Type'] = 'text/xml' + return headers + + def pre_marshall_hook(self, method_name, args): + """ + A hook which is called before marshalling the rpc arguments into XML + and transmitting it to the server. This is useful as it is common for + API's to have you pass your authorization token as the first parameter + of all calls or similar. + + @type method_name: C{str} + @param method_name: The name of the remote method that will be called. + + @type args: C{tuple} + @param args: A tuple of arguments that will be sent to the remote. + + Should return a tuple of arguments + """ + return args + + def request(self, method_name, *args): + """ + Call a given `method_name`. + + @type method_name: C{str} + @param method_name: A method exposed by the xmlrpc endpoint that you + are connecting to. + + @type args: C{tuple} + @param args: Arguments to invoke with method with. + """ + args = self.pre_marshall_hook(method_name, args) + data = xmlrpclib.dumps(args, methodname=method_name, allow_none=True) + return super(XMLRPCConnection, self).request('/xmlrpc/', + data=data, + method='POST') diff --git a/libcloud/dns/drivers/gandi.py b/libcloud/dns/drivers/gandi.py index b4419a47ca..ecc7522c47 100644 --- a/libcloud/dns/drivers/gandi.py +++ b/libcloud/dns/drivers/gandi.py @@ -66,13 +66,9 @@ def __exit__(self, type, value, traceback): class GandiDNSResponse(GandiResponse): - def parse_error(self, code, message): - context = self.connection.context - driver = self.connection.driver - if code == 581042: - zone_id = str(context.get('zone_id', None)) - raise ZoneDoesNotExistError(value='', driver=driver, - zone_id=zone_id) + exceptions = { + 581042: ZoneDoesNotExistError, + } class GandiDNSConnection(GandiConnection): From 9a82985bacc53e5afbf79b09b6dd607ee70fb133 Mon Sep 17 00:00:00 2001 From: John Carr Date: Thu, 31 Jan 2013 21:35:57 +0000 Subject: [PATCH 2/6] We expect the zone id to be a string id in some places, so lets make sure we are --- libcloud/dns/drivers/gandi.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/libcloud/dns/drivers/gandi.py b/libcloud/dns/drivers/gandi.py index ecc7522c47..2407cf7efc 100644 --- a/libcloud/dns/drivers/gandi.py +++ b/libcloud/dns/drivers/gandi.py @@ -103,7 +103,7 @@ class GandiDNSDriver(BaseGandiDriver, DNSDriver): def _to_zone(self, zone): return Zone( - id=zone['id'], + id=str(zone['id']), domain=zone['name'], type='master', ttl=0, @@ -123,7 +123,7 @@ def list_zones(self): def get_zone(self, zone_id): zid = int(zone_id) - self.connection.set_context({'zone_id': zid}) + self.connection.set_context({'zone_id': zone_id}) zone = self.connection.request('domain.zone.info', zid) return self._to_zone(zone.object) @@ -137,13 +137,13 @@ def create_zone(self, domain, type='master', ttl=None, extra=None): def update_zone(self, zone, domain=None, type=None, ttl=None, extra=None): zid = int(zone.id) params = {'name': domain} - self.connection.set_context({'zone_id': zid}) + self.connection.set_context({'zone_id': zone.id}) zone = self.connection.request('domain.zone.update', zid, params) return self._to_zone(zone.object) def delete_zone(self, zone): zid = int(zone.id) - self.connection.set_context({'zone_id': zid}) + self.connection.set_context({'zone_id': zone.id}) res = self.connection.request('domain.zone.delete', zid) return res.object @@ -166,7 +166,7 @@ def _to_records(self, records, zone): def list_records(self, zone): zid = int(zone.id) - self.connection.set_context({'zone_id': zid}) + self.connection.set_context({'zone_id': zone.id}) records = self.connection.request('domain.zone.record.list', zid, 0) return self._to_records(records.object, zone) @@ -177,7 +177,7 @@ def get_record(self, zone_id, record_id): 'name': name, 'type': record_type } - self.connection.set_context({'zone_id': zid}) + self.connection.set_context({'zone_id': zone_id}) records = self.connection.request('domain.zone.record.list', zid, 0, filter_opts).object @@ -215,7 +215,7 @@ def create_record(self, name, zone, type, data, extra=None): with NewZoneVersion(self, zone) as vid: con = self.connection - con.set_context({'zone_id': zid}) + con.set_context({'zone_id': zone.id}) rec = con.request('domain.zone.record.add', zid, vid, create).object @@ -242,7 +242,7 @@ def update_record(self, record, name, type, data, extra): with NewZoneVersion(self, record.zone) as vid: con = self.connection - con.set_context({'zone_id': zid}) + con.set_context({'zone_id': record.zone.id}) con.request('domain.zone.record.delete', zid, vid, filter_opts) res = con.request('domain.zone.record.add', @@ -260,7 +260,7 @@ def delete_record(self, record): with NewZoneVersion(self, record.zone) as vid: con = self.connection - con.set_context({'zone_id': zid}) + con.set_context({'zone_id': record.zone.id}) count = con.request('domain.zone.record.delete', zid, vid, filter_opts).object From 725143858cc08358376737f89ad5fc7e66335317 Mon Sep 17 00:00:00 2001 From: John Carr Date: Thu, 31 Jan 2013 22:37:47 +0000 Subject: [PATCH 3/6] Port VCLNodeDriver to use shared xmlrpc layer --- libcloud/common/gandi.py | 3 +- libcloud/common/xmlrpc.py | 6 +- libcloud/compute/drivers/vcl.py | 97 ++++++------------------------- libcloud/dns/drivers/gandi.py | 1 - libcloud/test/compute/test_vcl.py | 34 +++++------ 5 files changed, 36 insertions(+), 105 deletions(-) diff --git a/libcloud/common/gandi.py b/libcloud/common/gandi.py index b88add6e64..83b0f08223 100644 --- a/libcloud/common/gandi.py +++ b/libcloud/common/gandi.py @@ -55,7 +55,8 @@ class GandiConnection(XMLRPCConnection, ConnectionKey): responseCls = GandiResponse host = 'rpc.gandi.net' - + endpoint = '/xmlrpc/' + def pre_marshall_hook(self, method_name, args): return (self.key, ) + args diff --git a/libcloud/common/xmlrpc.py b/libcloud/common/xmlrpc.py index aad03ea51d..aaf3534caf 100644 --- a/libcloud/common/xmlrpc.py +++ b/libcloud/common/xmlrpc.py @@ -19,7 +19,7 @@ import sys from libcloud.utils.py3 import xmlrpclib -from libcloud.common.base import Response, ConnectionKey +from libcloud.common.base import Response, Connection class ProtocolError(Exception): @@ -71,7 +71,7 @@ def parse_error(self): raise ProtocolError(msg) -class XMLRPCConnection(ConnectionKey): +class XMLRPCConnection(Connection): """ Connection class which can call XMLRPC based API's. @@ -116,6 +116,6 @@ def request(self, method_name, *args): """ args = self.pre_marshall_hook(method_name, args) data = xmlrpclib.dumps(args, methodname=method_name, allow_none=True) - return super(XMLRPCConnection, self).request('/xmlrpc/', + return super(XMLRPCConnection, self).request(self.endpoint, data=data, method='POST') diff --git a/libcloud/compute/drivers/vcl.py b/libcloud/compute/drivers/vcl.py index 866d6e44e4..0d4e80f6d5 100644 --- a/libcloud/compute/drivers/vcl.py +++ b/libcloud/compute/drivers/vcl.py @@ -16,89 +16,30 @@ VCL driver """ -import sys import time -from libcloud.utils.py3 import xmlrpclib - +from libcloud.common.base import ConnectionUserAndKey +from libcloud.common.xmlrpc import XMLRPCResponse, XMLRPCConnection from libcloud.common.types import InvalidCredsError, LibcloudError from libcloud.compute.types import Provider, NodeState from libcloud.compute.base import NodeDriver, Node from libcloud.compute.base import NodeSize, NodeImage -class VCLSafeTransport(xmlrpclib.SafeTransport): - def __init__(self, datetime, user, passwd, host): - - self._pass = passwd - self._use_datetime = datetime - self._connection = (None, None) - self._extra_headers = [] - - def send_content(self, connection, request_body): - connection.putheader('Content-Type', 'text/xml') - connection.putheader('X-APIVERSION', '2') - connection.putheader('X-User', self._user) - connection.putheader('X-Pass', self._pass) - connection.putheader('Content-Length', str(len(request_body))) - connection.endheaders(request_body) - - -class VCLProxy(xmlrpclib.ServerProxy): - API_POSTFIX = '/index.php?mode=xmlrpccall' - transportCls = VCLSafeTransport - - def __init__(self, user, key, secure, host, port, driver, verbose=False): - url = '' - cls = self.transportCls - - if secure: - url = 'https://' - port = port or 443 - else: - url = 'http://' - port = port or 80 - - url += host + ':' + str(port) - url += VCLProxy.API_POSTFIX - - self.API = url - t = cls(0, user, key, self.API) - - xmlrpclib.ServerProxy.__init__( - self, - uri=self.API, - transport=t, - verbose=verbose - ) - - -class VCLConnection(object): - """ - Connection class for the VCL driver - """ - - proxyCls = VCLProxy - driver = None +class VCLResponse(XMLRPCResponse): + exceptions = { + 'VCL_Account': InvalidCredsError, + } - def __init__(self, user, key, secure, host, port): - self.user = user - self.key = key - self.secure = secure - self.host = host - self.port = port - def request(self, method, *args, **kwargs): - sl = self.proxyCls(user=self.user, key=self.key, secure=self.secure, - host=self.host, port=self.port, driver=self.driver) +class VCLConnection(XMLRPCConnection, ConnectionUserAndKey): + endpoint = '/index.php?mode=xmlrpccall' - try: - return getattr(sl, method)(*args) - except xmlrpclib.Fault: - e = sys.exc_info()[1] - if e.faultCode == 'VCL_Account': - raise InvalidCredsError(e.faultString) - raise LibcloudError(e, driver=self.driver) + def add_default_headers(self, headers): + headers['X-APIVERSION'] = '2' + headers['X-User'] = self.user_id + headers['X-Pass'] = self.key + return headers class VCLNodeDriver(NodeDriver): @@ -151,17 +92,15 @@ def __init__(self, key, secret, secure=True, host=None, port=None, *args, raise Exception('When instantiating VCL driver directly ' + 'you also need to provide host') - self.key = key - self.host = host - self.secret = secret - self.connection = self.connectionCls(key, secret, secure, host, port) - self.connection.driver = self + super(VCLNodeDriver, self).__init__(key, secret, secure=True, + host=None, port=None, *args, + **kwargs) def _vcl_request(self, method, *args): res = self.connection.request( method, *args - ) + ).object if(res['status'] == 'error'): raise LibcloudError(res['errormsg'], driver=self) return res @@ -237,7 +176,7 @@ def list_images(self, location=None): """ res = self.connection.request( "XMLRPCgetImages" - ) + ).object return [self._to_image(i) for i in res] def list_sizes(self, location=None): diff --git a/libcloud/dns/drivers/gandi.py b/libcloud/dns/drivers/gandi.py index 2407cf7efc..2643f6126c 100644 --- a/libcloud/dns/drivers/gandi.py +++ b/libcloud/dns/drivers/gandi.py @@ -65,7 +65,6 @@ def __exit__(self, type, value, traceback): class GandiDNSResponse(GandiResponse): - exceptions = { 581042: ZoneDoesNotExistError, } diff --git a/libcloud/test/compute/test_vcl.py b/libcloud/test/compute/test_vcl.py index 40a237c514..00e6374f80 100644 --- a/libcloud/test/compute/test_vcl.py +++ b/libcloud/test/compute/test_vcl.py @@ -29,31 +29,13 @@ from libcloud.test.file_fixtures import ComputeFileFixtures from libcloud.test.secrets import VCL_PARAMS -class MockVCLTransport(xmlrpclib.Transport): - - def __init__(self, datetime, user, passwd, host): - self._use_datetime = datetime - self._connection = (None, None) - self._extra_headers = [] - self._use_builtin_types = False - - def request(self, host, handler, request_body, verbose=0): - self.verbose = 0 - method = ET.XML(request_body).find('methodName').text - mock = VCLMockHttp(host, 80) - mock.request('POST', method) - resp = mock.getresponse() - - if sys.version[0] == '2' and sys.version[2] == '7': - response = self.parse_response(resp) - else: - response = self.parse_response(resp.body) - return response class VCLTests(unittest.TestCase): def setUp(self): - VCL.connectionCls.proxyCls.transportCls = MockVCLTransport + VCL.connectionCls.conn_classes = ( + VCLMockHttp, VCLMockHttp) + VCLMockHttp.type = None self.driver = VCL(*VCL_PARAMS) def test_list_nodes(self): @@ -98,9 +80,19 @@ def test_ex_get_request_end_time(self): 1334168100 ) + class VCLMockHttp(MockHttp): fixtures = ComputeFileFixtures('vcl') + def _get_method_name(self, type, use_param, qs, path): + return "_xmlrpc" + + def _xmlrpc(self, method, url, body, headers): + params, meth_name = xmlrpclib.loads(body) + if self.type: + meth_name = "%s_%s" % (meth_name, self.type) + return getattr(self, meth_name)(method, url, body, headers) + def XMLRPCgetImages(self, method, url, body, headers): body = self.fixtures.load('XMLRPCgetImages.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) From 732bbaddaaff04fca8d2d7e639c97becbd2ef8bf Mon Sep 17 00:00:00 2001 From: John Carr Date: Sat, 2 Feb 2013 00:41:34 +0000 Subject: [PATCH 4/6] Move SoftLayer to shared xmlrpc code --- libcloud/common/gandi.py | 5 +- libcloud/common/xmlrpc.py | 23 +---- libcloud/compute/drivers/softlayer.py | 95 ++++--------------- .../fixtures/softlayer/SoftLayer_Account.xml | 17 ++++ .../test/compute/fixtures/softlayer/fail.xml | 17 ++++ libcloud/test/compute/test_softlayer.py | 61 +++++++----- 6 files changed, 94 insertions(+), 124 deletions(-) create mode 100644 libcloud/test/compute/fixtures/softlayer/SoftLayer_Account.xml create mode 100644 libcloud/test/compute/fixtures/softlayer/fail.xml diff --git a/libcloud/common/gandi.py b/libcloud/common/gandi.py index 83b0f08223..0122f945ec 100644 --- a/libcloud/common/gandi.py +++ b/libcloud/common/gandi.py @@ -57,8 +57,9 @@ class GandiConnection(XMLRPCConnection, ConnectionKey): host = 'rpc.gandi.net' endpoint = '/xmlrpc/' - def pre_marshall_hook(self, method_name, args): - return (self.key, ) + args + def request(self, method, *args): + args = (self.key, ) + args + return super(GandiConnection, self).request(method, *args) class BaseGandiDriver(object): diff --git a/libcloud/common/xmlrpc.py b/libcloud/common/xmlrpc.py index aaf3534caf..6dba2442aa 100644 --- a/libcloud/common/xmlrpc.py +++ b/libcloud/common/xmlrpc.py @@ -86,24 +86,7 @@ def add_default_headers(self, headers): headers['Content-Type'] = 'text/xml' return headers - def pre_marshall_hook(self, method_name, args): - """ - A hook which is called before marshalling the rpc arguments into XML - and transmitting it to the server. This is useful as it is common for - API's to have you pass your authorization token as the first parameter - of all calls or similar. - - @type method_name: C{str} - @param method_name: The name of the remote method that will be called. - - @type args: C{tuple} - @param args: A tuple of arguments that will be sent to the remote. - - Should return a tuple of arguments - """ - return args - - def request(self, method_name, *args): + def request(self, method_name, *args, **kwargs): """ Call a given `method_name`. @@ -114,8 +97,8 @@ def request(self, method_name, *args): @type args: C{tuple} @param args: Arguments to invoke with method with. """ - args = self.pre_marshall_hook(method_name, args) + endpoint = kwargs.get('endpoint', self.endpoint) data = xmlrpclib.dumps(args, methodname=method_name, allow_none=True) - return super(XMLRPCConnection, self).request(self.endpoint, + return super(XMLRPCConnection, self).request(endpoint, data=data, method='POST') diff --git a/libcloud/compute/drivers/softlayer.py b/libcloud/compute/drivers/softlayer.py index 3f87d21a9f..c0ebb1c26c 100644 --- a/libcloud/compute/drivers/softlayer.py +++ b/libcloud/compute/drivers/softlayer.py @@ -23,6 +23,8 @@ from libcloud.utils.py3 import xmlrpclib +from libcloud.common.base import ConnectionUserAndKey +from libcloud.common.xmlrpc import XMLRPCResponse, XMLRPCConnection from libcloud.common.types import InvalidCredsError, LibcloudError from libcloud.compute.types import Provider, NodeState from libcloud.compute.base import NodeDriver, Node, NodeLocation, NodeSize, \ @@ -97,78 +99,33 @@ class SoftLayerException(LibcloudError): pass -class SoftLayerSafeTransport(xmlrpclib.SafeTransport): - pass - +class SoftLayerResponse(XMLRPCResponse): + exceptions = { + 'SoftLayer_Account': InvalidCredsError, + } -class SoftLayerTransport(xmlrpclib.Transport): - pass - -class SoftLayerProxy(xmlrpclib.ServerProxy): - transportCls = (SoftLayerTransport, SoftLayerSafeTransport) - API_PREFIX = 'https://api.softlayer.com/xmlrpc/v3/' - - def __init__(self, service, user_agent, verbose=False): - cls = self.transportCls[0] - if SoftLayerProxy.API_PREFIX[:8] == 'https://': - cls = self.transportCls[1] - t = cls(use_datetime=0) - t.user_agent = user_agent - xmlrpclib.ServerProxy.__init__( - self, - uri='%s/%s' % (SoftLayerProxy.API_PREFIX, service), - transport=t, - verbose=verbose, - ) - - -class SoftLayerConnection(object): - """ - Connection class for the SoftLayer driver - """ - - proxyCls = SoftLayerProxy - driver = None - - def __init__(self, user, key): - self.user = user - self.key = key - self.ua = [] +class SoftLayerConnection(XMLRPCConnection, ConnectionUserAndKey): + endpoint = '/xmlrpc/v3/' def request(self, service, method, *args, **kwargs): - sl = self.proxyCls(service, self._user_agent()) - headers = {} headers.update(self._get_auth_headers()) headers.update(self._get_init_params(service, kwargs.get('id'))) headers.update( self._get_object_mask(service, kwargs.get('object_mask'))) - headers.update( self._get_object_mask(service, kwargs.get('object_mask'))) - params = [{'headers': headers}] + list(args) - try: - return getattr(sl, method)(*params) - except xmlrpclib.Fault: - e = sys.exc_info()[1] - if e.faultCode == 'SoftLayer_Account': - raise InvalidCredsError(e.faultString) - raise SoftLayerException(e) - - def _user_agent(self): - return 'libcloud/%s (%s)%s' % (libcloud.__version__, - self.driver.name, - ''.join([' (%s)' % x for x in self.ua])) + args = ({'headers': headers}, ) + args + endpoint = '%s/%s' % (self.endpoint, service) - def user_agent_append(self, s): - self.ua.append(s) + return super(SoftLayerConnection, self).request(method, *args, endpoint=endpoint) def _get_auth_headers(self): return { 'authenticate': { - 'username': self.user, + 'username': self.user_id, 'apiKey': self.key } } @@ -208,24 +165,6 @@ class SoftLayerNodeDriver(NodeDriver): features = {'create_node': ['generates_password']} - def __init__(self, key, secret=None, secure=False): - """ - @param key: API key or username to used (required) - @type key: C{str} - - @param secret: Secret password to be used (required) - @type secret: C{str} - - @param secure: Weither to use HTTPS or HTTP. - @type secure: C{bool} - - @rtype: C{None} - """ - self.key = key - self.secret = secret - self.connection = self.connectionCls(key, secret) - self.connection.driver = self - def _to_node(self, host): try: password = \ @@ -283,7 +222,7 @@ def _get_order_information(self, node_id, timeout=1200, check_interval=5): 'getObject', id=node_id, object_mask=mask - ) + ).object if res.get('provisionDate', None): return res @@ -385,7 +324,7 @@ def create_node(self, **kwargs): res = self.connection.request( 'SoftLayer_Virtual_Guest', 'createObject', newCCI - ) + ).object node_id = res['id'] raw_node = self._get_order_information(node_id) @@ -402,7 +341,7 @@ def _to_image(self, img): def list_images(self, location=None): result = self.connection.request( 'SoftLayer_Virtual_Guest', 'getCreateObjectOptions' - ) + ).object return [self._to_image(i) for i in result['operatingSystems']] def _to_size(self, id, size): @@ -429,7 +368,7 @@ def _to_loc(self, loc): def list_locations(self): res = self.connection.request( 'SoftLayer_Location_Datacenter', 'getDatacenters' - ) + ).object return [self._to_loc(l) for l in res] def list_nodes(self): @@ -444,5 +383,5 @@ def list_nodes(self): "SoftLayer_Account", "getVirtualGuests", object_mask=mask - ) + ).object return [self._to_node(h) for h in res] diff --git a/libcloud/test/compute/fixtures/softlayer/SoftLayer_Account.xml b/libcloud/test/compute/fixtures/softlayer/SoftLayer_Account.xml new file mode 100644 index 0000000000..0f38ef053f --- /dev/null +++ b/libcloud/test/compute/fixtures/softlayer/SoftLayer_Account.xml @@ -0,0 +1,17 @@ + + + + + + + faultCode + SoftLayer_Account + + + faultString + Failed Call + + + + + diff --git a/libcloud/test/compute/fixtures/softlayer/fail.xml b/libcloud/test/compute/fixtures/softlayer/fail.xml new file mode 100644 index 0000000000..4cd0162f66 --- /dev/null +++ b/libcloud/test/compute/fixtures/softlayer/fail.xml @@ -0,0 +1,17 @@ + + + + + + + faultCode + fail + + + faultString + Failed Call + + + + + diff --git a/libcloud/test/compute/test_softlayer.py b/libcloud/test/compute/test_softlayer.py index 2978ce6f71..f51ecf914d 100644 --- a/libcloud/test/compute/test_softlayer.py +++ b/libcloud/test/compute/test_softlayer.py @@ -34,33 +34,12 @@ from libcloud.test.secrets import SOFTLAYER_PARAMS -class MockSoftLayerTransport(xmlrpclib.Transport): - - def request(self, host, handler, request_body, verbose=0): - self.verbose = 0 - - if 'SOFTLAYEREXCEPTION' in u(request_body): - raise xmlrpclib.Fault('fail', 'Failed Call') - if 'INVALIDCREDSERROR' in u(request_body): - raise xmlrpclib.Fault('SoftLayer_Account', 'Failed Call') - - method = ET.XML(request_body).find('methodName').text - mock = SoftLayerMockHttp(host, 80) - mock.request('POST', "%s/%s" % (handler, method)) - resp = mock.getresponse() - - if sys.version[0] == '2' and sys.version[2] == '7': - response = self.parse_response(resp) - else: - response = self.parse_response(resp.body) - return response - - class SoftLayerTests(unittest.TestCase): def setUp(self): - SoftLayer.connectionCls.proxyCls.transportCls = [ - MockSoftLayerTransport, MockSoftLayerTransport] + SoftLayer.connectionCls.conn_classes = ( + SoftLayerMockHttp, SoftLayerMockHttp) + SoftLayerMockHttp.type = None self.driver = SoftLayer(*SOFTLAYER_PARAMS) def test_list_nodes(self): @@ -94,6 +73,7 @@ def test_create_node(self): image=self.driver.list_images()[0]) def test_create_fail(self): + SoftLayerMockHttp.type = "SOFTLAYEREXCEPTION" self.assertRaises( SoftLayerException, self.driver.create_node, @@ -103,6 +83,7 @@ def test_create_fail(self): image=self.driver.list_images()[0]) def test_create_creds_error(self): + SoftLayerMockHttp.type = "INVALIDCREDSERROR" self.assertRaises( InvalidCredsError, self.driver.create_node, @@ -153,12 +134,28 @@ def test_destroy_node(self): class SoftLayerMockHttp(MockHttp): fixtures = ComputeFileFixtures('softlayer') + def _get_method_name(self, type, use_param, qs, path): + return "_xmlrpc" + + def _xmlrpc(self, method, url, body, headers): + params, meth_name = xmlrpclib.loads(body) + url = url.replace("/", "_") + meth_name = "%s_%s" % (url, meth_name) + if self.type: + meth_name = "%s_%s" % (meth_name, self.type) + return getattr(self, meth_name)(method, url, body, headers) + def _xmlrpc_v3__SoftLayer_Virtual_Guest_getCreateObjectOptions( self, method, url, body, headers): body = self.fixtures.load( 'v3__SoftLayer_Virtual_Guest_getCreateObjectOptions.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + _xmlrpc_v3__SoftLayer_Virtual_Guest_getCreateObjectOptions_INVALIDCREDSERROR = \ + _xmlrpc_v3__SoftLayer_Virtual_Guest_getCreateObjectOptions + _xmlrpc_v3__SoftLayer_Virtual_Guest_getCreateObjectOptions_SOFTLAYEREXCEPTION = \ + _xmlrpc_v3__SoftLayer_Virtual_Guest_getCreateObjectOptions + def _xmlrpc_v3__SoftLayer_Account_getVirtualGuests( self, method, url, body, headers): body = self.fixtures.load('v3_SoftLayer_Account_getVirtualGuests.xml') @@ -170,12 +167,27 @@ def _xmlrpc_v3__SoftLayer_Location_Datacenter_getDatacenters( 'v3_SoftLayer_Location_Datacenter_getDatacenters.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + _xmlrpc_v3__SoftLayer_Location_Datacenter_getDatacenters_INVALIDCREDSERROR = \ + _xmlrpc_v3__SoftLayer_Location_Datacenter_getDatacenters + _xmlrpc_v3__SoftLayer_Location_Datacenter_getDatacenters_SOFTLAYEREXCEPTION = \ + _xmlrpc_v3__SoftLayer_Location_Datacenter_getDatacenters + def _xmlrpc_v3__SoftLayer_Virtual_Guest_createObject( self, method, url, body, headers): body = self.fixtures.load( 'v3__SoftLayer_Virtual_Guest_createObject.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + def _xmlrpc_v3__SoftLayer_Virtual_Guest_createObject_INVALIDCREDSERROR( + self, method, url, body, headers): + body = self.fixtures.load('SoftLayer_Account.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _xmlrpc_v3__SoftLayer_Virtual_Guest_createObject_SOFTLAYEREXCEPTION( + self, method, url, body, headers): + body = self.fixtures.load('fail.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + def _xmlrpc_v3__SoftLayer_Virtual_Guest_getObject( self, method, url, body, headers): body = self.fixtures.load( @@ -192,5 +204,6 @@ def _xmlrpc_v3__SoftLayer_Virtual_Guest_deleteObject( body = self.fixtures.load('empty.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + if __name__ == '__main__': sys.exit(unittest.main()) From f7de0a8cd72b004940de951f3cdab5d57292a6b1 Mon Sep 17 00:00:00 2001 From: John Carr Date: Sat, 2 Feb 2013 11:16:00 +0000 Subject: [PATCH 5/6] Make sure SoftLayer continues to raise same exceptions as before --- libcloud/common/xmlrpc.py | 4 +++- libcloud/compute/drivers/softlayer.py | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/libcloud/common/xmlrpc.py b/libcloud/common/xmlrpc.py index 6dba2442aa..ae9dfe2ffa 100644 --- a/libcloud/common/xmlrpc.py +++ b/libcloud/common/xmlrpc.py @@ -52,6 +52,8 @@ def raise_exception_for_error(self, error_code, message): class XMLRPCResponse(ErrorCodeMixin, Response): + defaultExceptionCls = Exception + def success(self): return self.status == 200 @@ -64,7 +66,7 @@ def parse_body(self): except xmlrpclib.Fault: e = sys.exc_info()[1] self.raise_exception_for_error(e.faultCode, e.faultString) - raise Exception("%s: %s" % (e.faultCode, e.faultString)) + raise self.defaultExceptionCls("%s: %s" % (e.faultCode, e.faultString)) def parse_error(self): msg = 'Server returned an invalid xmlrpc response (%d)' % self.status diff --git a/libcloud/compute/drivers/softlayer.py b/libcloud/compute/drivers/softlayer.py index c0ebb1c26c..c31ab5b393 100644 --- a/libcloud/compute/drivers/softlayer.py +++ b/libcloud/compute/drivers/softlayer.py @@ -100,12 +100,14 @@ class SoftLayerException(LibcloudError): class SoftLayerResponse(XMLRPCResponse): + defaultExceptionCls = SoftLayerException exceptions = { 'SoftLayer_Account': InvalidCredsError, } class SoftLayerConnection(XMLRPCConnection, ConnectionUserAndKey): + responseCls = SoftLayerResponse endpoint = '/xmlrpc/v3/' def request(self, service, method, *args, **kwargs): From 5a3e01baec0d88e9b011b7c0cb2fc8a7fa6f42eb Mon Sep 17 00:00:00 2001 From: John Carr Date: Sat, 2 Feb 2013 11:32:53 +0000 Subject: [PATCH 6/6] PEP8 fixes --- libcloud/common/gandi.py | 2 +- libcloud/common/xmlrpc.py | 3 ++- libcloud/compute/drivers/softlayer.py | 3 ++- libcloud/dns/drivers/gandi.py | 2 +- libcloud/test/compute/test_softlayer.py | 30 +++++-------------------- libcloud/test/compute/test_vcl.py | 24 +++++--------------- libcloud/test/dns/test_gandi.py | 27 ++++++++++++++-------- 7 files changed, 36 insertions(+), 55 deletions(-) diff --git a/libcloud/common/gandi.py b/libcloud/common/gandi.py index 0122f945ec..17a9193b0e 100644 --- a/libcloud/common/gandi.py +++ b/libcloud/common/gandi.py @@ -56,7 +56,7 @@ class GandiConnection(XMLRPCConnection, ConnectionKey): responseCls = GandiResponse host = 'rpc.gandi.net' endpoint = '/xmlrpc/' - + def request(self, method, *args): args = (self.key, ) + args return super(GandiConnection, self).request(method, *args) diff --git a/libcloud/common/xmlrpc.py b/libcloud/common/xmlrpc.py index ae9dfe2ffa..86e96b8565 100644 --- a/libcloud/common/xmlrpc.py +++ b/libcloud/common/xmlrpc.py @@ -66,7 +66,8 @@ def parse_body(self): except xmlrpclib.Fault: e = sys.exc_info()[1] self.raise_exception_for_error(e.faultCode, e.faultString) - raise self.defaultExceptionCls("%s: %s" % (e.faultCode, e.faultString)) + error_string = '%s: %s' % (e.faultCode, e.faultString) + raise self.defaultExceptionCls(error_string) def parse_error(self): msg = 'Server returned an invalid xmlrpc response (%d)' % self.status diff --git a/libcloud/compute/drivers/softlayer.py b/libcloud/compute/drivers/softlayer.py index c31ab5b393..946e80ec83 100644 --- a/libcloud/compute/drivers/softlayer.py +++ b/libcloud/compute/drivers/softlayer.py @@ -122,7 +122,8 @@ def request(self, service, method, *args, **kwargs): args = ({'headers': headers}, ) + args endpoint = '%s/%s' % (self.endpoint, service) - return super(SoftLayerConnection, self).request(method, *args, endpoint=endpoint) + return super(SoftLayerConnection, self).request(method, *args, + endpoint=endpoint) def _get_auth_headers(self): return { diff --git a/libcloud/dns/drivers/gandi.py b/libcloud/dns/drivers/gandi.py index 2643f6126c..8a8f68a0a8 100644 --- a/libcloud/dns/drivers/gandi.py +++ b/libcloud/dns/drivers/gandi.py @@ -67,7 +67,7 @@ def __exit__(self, type, value, traceback): class GandiDNSResponse(GandiResponse): exceptions = { 581042: ZoneDoesNotExistError, - } + } class GandiDNSConnection(GandiConnection): diff --git a/libcloud/test/compute/test_softlayer.py b/libcloud/test/compute/test_softlayer.py index f51ecf914d..fc58d12e68 100644 --- a/libcloud/test/compute/test_softlayer.py +++ b/libcloud/test/compute/test_softlayer.py @@ -141,8 +141,6 @@ def _xmlrpc(self, method, url, body, headers): params, meth_name = xmlrpclib.loads(body) url = url.replace("/", "_") meth_name = "%s_%s" % (url, meth_name) - if self.type: - meth_name = "%s_%s" % (meth_name, self.type) return getattr(self, meth_name)(method, url, body, headers) def _xmlrpc_v3__SoftLayer_Virtual_Guest_getCreateObjectOptions( @@ -151,11 +149,6 @@ def _xmlrpc_v3__SoftLayer_Virtual_Guest_getCreateObjectOptions( 'v3__SoftLayer_Virtual_Guest_getCreateObjectOptions.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - _xmlrpc_v3__SoftLayer_Virtual_Guest_getCreateObjectOptions_INVALIDCREDSERROR = \ - _xmlrpc_v3__SoftLayer_Virtual_Guest_getCreateObjectOptions - _xmlrpc_v3__SoftLayer_Virtual_Guest_getCreateObjectOptions_SOFTLAYEREXCEPTION = \ - _xmlrpc_v3__SoftLayer_Virtual_Guest_getCreateObjectOptions - def _xmlrpc_v3__SoftLayer_Account_getVirtualGuests( self, method, url, body, headers): body = self.fixtures.load('v3_SoftLayer_Account_getVirtualGuests.xml') @@ -167,25 +160,14 @@ def _xmlrpc_v3__SoftLayer_Location_Datacenter_getDatacenters( 'v3_SoftLayer_Location_Datacenter_getDatacenters.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - _xmlrpc_v3__SoftLayer_Location_Datacenter_getDatacenters_INVALIDCREDSERROR = \ - _xmlrpc_v3__SoftLayer_Location_Datacenter_getDatacenters - _xmlrpc_v3__SoftLayer_Location_Datacenter_getDatacenters_SOFTLAYEREXCEPTION = \ - _xmlrpc_v3__SoftLayer_Location_Datacenter_getDatacenters - def _xmlrpc_v3__SoftLayer_Virtual_Guest_createObject( self, method, url, body, headers): - body = self.fixtures.load( - 'v3__SoftLayer_Virtual_Guest_createObject.xml') - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _xmlrpc_v3__SoftLayer_Virtual_Guest_createObject_INVALIDCREDSERROR( - self, method, url, body, headers): - body = self.fixtures.load('SoftLayer_Account.xml') - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - - def _xmlrpc_v3__SoftLayer_Virtual_Guest_createObject_SOFTLAYEREXCEPTION( - self, method, url, body, headers): - body = self.fixtures.load('fail.xml') + fixture = { + None: 'v3__SoftLayer_Virtual_Guest_createObject.xml', + 'INVALIDCREDSERROR': 'SoftLayer_Account.xml', + 'SOFTLAYEREXCEPTION': 'fail.xml', + }[self.type] + body = self.fixtures.load(fixture) return (httplib.OK, body, {}, httplib.responses[httplib.OK]) def _xmlrpc_v3__SoftLayer_Virtual_Guest_getObject( diff --git a/libcloud/test/compute/test_vcl.py b/libcloud/test/compute/test_vcl.py index 00e6374f80..6240a9615c 100644 --- a/libcloud/test/compute/test_vcl.py +++ b/libcloud/test/compute/test_vcl.py @@ -97,43 +97,31 @@ def XMLRPCgetImages(self, method, url, body, headers): body = self.fixtures.load('XMLRPCgetImages.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def XMLRPCextendRequest( - self, method, url, body, headers): - + def XMLRPCextendRequest(self, method, url, body, headers): body = self.fixtures.load('XMLRPCextendRequest.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def XMLRPCgetRequestIds( - self, method, url, body, headers): - + def XMLRPCgetRequestIds(self, method, url, body, headers): body = self.fixtures.load( 'XMLRPCgetRequestIds.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def XMLRPCgetRequestStatus( - self, method, url, body, headers): - + def XMLRPCgetRequestStatus(self, method, url, body, headers): body = self.fixtures.load( 'XMLRPCgetRequestStatus.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def XMLRPCendRequest( - self, method, url, body, headers): - + def XMLRPCendRequest(self, method, url, body, headers): body = self.fixtures.load( 'XMLRPCendRequest.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def XMLRPCaddRequest( - self, method, url, body, headers): - + def XMLRPCaddRequest(self, method, url, body, headers): body = self.fixtures.load( 'XMLRPCaddRequest.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def XMLRPCgetRequestConnectData( - self, method, url, body, headers): - + def XMLRPCgetRequestConnectData(self, method, url, body, headers): body = self.fixtures.load( 'XMLRPCgetRequestConnectData.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) diff --git a/libcloud/test/dns/test_gandi.py b/libcloud/test/dns/test_gandi.py index b9a5d621bc..04060778b2 100644 --- a/libcloud/test/dns/test_gandi.py +++ b/libcloud/test/dns/test_gandi.py @@ -247,39 +247,48 @@ def _xmlrpc__domain_zone_version_set(self, method, url, body, headers): body = self.fixtures.load('new_version.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _xmlrpc__domain_zone_record_list_ZONE_DOES_NOT_EXIST(self, method, url, body, headers): + def _xmlrpc__domain_zone_record_list_ZONE_DOES_NOT_EXIST(self, method, url, + body, headers): body = self.fixtures.load('zone_doesnt_exist.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _xmlrpc__domain_zone_info_ZONE_DOES_NOT_EXIST(self, method, url, body, headers): + def _xmlrpc__domain_zone_info_ZONE_DOES_NOT_EXIST(self, method, url, body, + headers): body = self.fixtures.load('zone_doesnt_exist.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _xmlrpc__domain_zone_list_ZONE_DOES_NOT_EXIST(self, method, url, body, headers): + def _xmlrpc__domain_zone_list_ZONE_DOES_NOT_EXIST(self, method, url, body, + headers): body = self.fixtures.load('zone_doesnt_exist.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _xmlrpc__domain_zone_delete_ZONE_DOES_NOT_EXIST(self, method, url, body, headers): + def _xmlrpc__domain_zone_delete_ZONE_DOES_NOT_EXIST(self, method, url, + body, headers): body = self.fixtures.load('zone_doesnt_exist.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _xmlrpc__domain_zone_record_list_RECORD_DOES_NOT_EXIST(self, method, url, body, headers): + def _xmlrpc__domain_zone_record_list_RECORD_DOES_NOT_EXIST( + self, method, url, body, headers): body = self.fixtures.load('list_records_empty.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _xmlrpc__domain_zone_info_RECORD_DOES_NOT_EXIST(self, method, url, body, headers): + def _xmlrpc__domain_zone_info_RECORD_DOES_NOT_EXIST(self, method, url, + body, headers): body = self.fixtures.load('list_zones.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _xmlrpc__domain_zone_record_delete_RECORD_DOES_NOT_EXIST(self, method, url, body, headers): + def _xmlrpc__domain_zone_record_delete_RECORD_DOES_NOT_EXIST( + self, method, url, body, headers): body = self.fixtures.load('delete_record_doesnotexist.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _xmlrpc__domain_zone_version_new_RECORD_DOES_NOT_EXIST(self, method, url, body, headers): + def _xmlrpc__domain_zone_version_new_RECORD_DOES_NOT_EXIST( + self, method, url, body, headers): body = self.fixtures.load('new_version.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _xmlrpc__domain_zone_version_set_RECORD_DOES_NOT_EXIST(self, method, url, body, headers): + def _xmlrpc__domain_zone_version_set_RECORD_DOES_NOT_EXIST( + self, method, url, body, headers): body = self.fixtures.load('new_version.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK])