diff --git a/demos/secrets.py-dist b/demos/secrets.py-dist index f7e3fc668f..5589c5176f 100644 --- a/demos/secrets.py-dist +++ b/demos/secrets.py-dist @@ -24,8 +24,7 @@ ECP_PARAMS = ('user_name', 'password') GANDI_PARAMS = ('user',) HOSTINGCOM_PARAMS = ('user', 'secret') IBM_PARAMS = ('user', 'secret') -# OPENSTACK_PARAMS = ('user_name', 'api_key', secure_bool, 'host', port_int) -OPENSTACK_PARAMS = ('user_name', 'api_key', False, 'host', 8774) +OPENSTACK_PARAMS = ('user_name', 'api_key', 'auth_url') OPENNEBULA_PARAMS = ('user', 'key') OPSOURCE_PARAMS = ('user', 'password') RACKSPACE_PARAMS = ('user', 'key') diff --git a/libcloud/common/base.py b/libcloud/common/base.py index 2b51792817..e0f02e592c 100644 --- a/libcloud/common/base.py +++ b/libcloud/common/base.py @@ -38,13 +38,13 @@ class Response(object): status = httplib.OK headers = {} error = None - connection = None - def __init__(self, response): + def __init__(self, response, connection=None): self.body = response.read() self.status = response.status self.headers = dict(response.getheaders()) self.error = response.reason + self.connection = connection if not self.success(): raise Exception(self.parse_error()) @@ -84,12 +84,13 @@ def success(self): class RawResponse(Response): - def __init__(self, response=None): + def __init__(self, response=None, connection=None): self._status = None self._response = None self._headers = {} self._error = None self._reason = None + self.connection = connection @property def response(self): @@ -195,7 +196,7 @@ def getresponse(self): def request(self, method, url, body=None, headers=None): headers.update({'X-LC-Request-ID': str(id(self))}) if self.log is not None: - pre = "# -------- begin %d request ----------\n" % id(self) + pre = "# -------- begin %d request ----------\n" % id(self) self.log.write(pre + self._log_curl(method, url, body, headers) + "\n") self.log.flush() @@ -217,7 +218,7 @@ def getresponse(self): def request(self, method, url, body=None, headers=None): headers.update({'X-LC-Request-ID': str(id(self))}) if self.log is not None: - pre = "# -------- begin %d request ----------\n" % id(self) + pre = "# -------- begin %d request ----------\n" % id(self) self.log.write(pre + self._log_curl(method, url, body, headers) + "\n") self.log.flush() @@ -285,7 +286,7 @@ def _tuple_from_url(self, url): return (host, port, secure, request_path) - def connect(self, host=None, port=None, base_url = None): + def connect(self, host=None, port=None, base_url=None): """ Establish a connection with the API server. @@ -298,7 +299,6 @@ def connect(self, host=None, port=None, base_url = None): @returns: A connection """ # prefer the attribute base_url if its set or sent - connection = None secure = self.secure if getattr(self, 'base_url', None) and base_url == None: @@ -389,7 +389,6 @@ def request(self, headers = self.add_default_headers(headers) # We always send a user-agent header headers.update({'User-Agent': self._user_agent()}) - headers.update({'Host': self.host}) # Encode data if necessary if data != '' and data != None: data = self.encode_data(data) @@ -424,15 +423,12 @@ def request(self, raise ssl.SSLError(str(e)) if raw: - response = self.rawResponseCls() + return self.rawResponseCls(connection=self) else: - response = self.responseCls(self.connection.getresponse()) - - response.connection = self - return response + return self.responseCls(self.connection.getresponse(), connection=self) def morph_action_hook(self, action): - return self.request_path + action + return self.request_path + action def add_default_params(self, params): """ @@ -473,7 +469,7 @@ def encode_data(self, data): Override in a provider's subclass. """ return data - + class ConnectionKey(Connection): """ A Base Connection class to derive from, which includes a diff --git a/libcloud/common/openstack.py b/libcloud/common/openstack.py index 7a2f2ca119..c1612533d5 100644 --- a/libcloud/common/openstack.py +++ b/libcloud/common/openstack.py @@ -21,66 +21,31 @@ from libcloud.common.base import ConnectionUserAndKey, Response from libcloud.compute.types import LibcloudError, InvalidCredsError, MalformedResponseError -try: - import simplejson as json -except ImportError: - import json - -AUTH_API_VERSION = 'v1.0' - __all__ = [ "OpenStackBaseConnection", - "OpenStackAuthConnection", + "OpenStackAuthConnection_v1_0", ] - -# @TODO: Refactor for re-use by other openstack drivers -class OpenStackAuthResponse(Response): +class OpenStackAuthResponse_v1_0(Response): + # TODO: Any reason Response's couldn't be this way? def success(self): - return True - - def parse_body(self): - if not self.body: - return None + return 200 <= self.status <= 299 - if 'content-type' in self.headers: - key = 'content-type' - elif 'Content-Type' in self.headers: - key = 'Content-Type' - else: - raise LibcloudError('Missing content-type header', driver=OpenStackAuthConnection) - - content_type = self.headers[key] - if content_type.find(';') != -1: - content_type = content_type.split(';')[0] - - if content_type == 'application/json': - try: - data = json.loads(self.body) - except: - raise MalformedResponseError('Failed to parse JSON', - body=self.body, - driver=OpenStackAuthConnection) - elif content_type == 'text/plain': - data = self.body - else: - data = self.body - - return data - -class OpenStackAuthConnection(ConnectionUserAndKey): +# @TODO: Refactor for re-use by other openstack drivers +class OpenStackAuthConnection_v1_0(ConnectionUserAndKey): - responseCls = OpenStackAuthResponse + responseCls = OpenStackAuthResponse_v1_0 name = 'OpenStack Auth' - def __init__(self, parent_conn, auth_url, user_id, key): + def __init__(self, parent_conn, auth_url, user_id, key, tenant_id=None): self.parent_conn = parent_conn # enable tests to use the same mock connection classes. self.conn_classes = parent_conn.conn_classes - super(OpenStackAuthConnection, self).__init__( + super(OpenStackAuthConnection_v1_0, self).__init__( user_id, key, url=auth_url) + self.tenant_id = tenant_id self.auth_url = auth_url self.urls = {} self.driver = self.parent_conn.driver @@ -91,44 +56,59 @@ def add_default_headers(self, headers): return headers def authenticate(self): - reqbody = json.dumps({'credentials': {'username': self.user_id, 'key': self.key}}) - resp = self.request("/auth", - data=reqbody, - headers={ - 'X-Auth-User': self.user_id, - 'X-Auth-Key': self.key, - }, - method='POST') + headers = { + 'X-Auth-User': self.user_id, + 'X-Auth-Key': self.key, + } + if self.tenant_id: + headers['X-Auth-Project-Id'] = self.tenant_id + + resp = self.request("/", headers=headers) if resp.status == httplib.UNAUTHORIZED: # HTTP UNAUTHORIZED (401): auth failed raise InvalidCredsError() - elif resp.status != httplib.OK: + + elif resp.status in (httplib.OK, httplib.NO_CONTENT): + self.auth_token = resp.headers.get('x-auth-token') or resp.headers.get('X-Auth-Token') + # TODO: Investigate down-casing entire headers dict in Response + self.urls = dict([ + (name.lower(), url) + for name, url in resp.headers.items() + if name.lower().endswith('-url') + ]) + + else: raise MalformedResponseError('Malformed response', body='code: %s body:%s' % (resp.status, resp.body), driver=self.driver) - else: - try: - body = json.loads(resp.body) - except Exception, e: - raise MalformedResponseError('Failed to parse JSON', e) - try: - self.auth_token = body['auth']['token']['id'] - self.urls = body['auth']['serviceCatalog'] - except KeyError, e: - raise MalformedResponseError('Auth JSON response is missing required elements', e) + class OpenStackBaseConnection(ConnectionUserAndKey): auth_url = None - - def __init__(self, user_id, key, secure=True, - host=None, port=None, ex_force_base_url=None): + tenant_id = None + accept_format = 'application/json' # Just a default + + def __init__(self, + user_id, + key, + secure=True, + host=None, + port=None, + auth_url=None, + tenant_id=None, + ex_force_base_url=None, + ): self.server_url = None self.cdn_management_url = None self.storage_url = None self.lb_url = None self.auth_token = None + if auth_url: + self.auth_url = auth_url + if tenant_id: + self.tenant_id = tenant_id self._force_base_url = ex_force_base_url super(OpenStackBaseConnection, self).__init__( user_id, key) @@ -136,6 +116,9 @@ def __init__(self, user_id, key, secure=True, def add_default_headers(self, headers): headers['X-Auth-Token'] = self.auth_token headers['Accept'] = self.accept_format + if hasattr(self, 'content_type'): + headers['Content-Type'] = self.content_type + return headers def morph_action(self, action): @@ -158,22 +141,11 @@ def _get_base_url(self, url_key): if not value: self._populate_hosts_and_request_paths() value = getattr(self, url_key, None) - if self._force_base_url != None: - value = self._force_base_url return value - def _get_default_region(self, arr): - if len(arr): - for i in arr: - if i.get('v1Default', False): - return i['publicURL'] - # uber lame - return arr[0] - return None - - def request(self, **kwargs): + def request(self, action, **kwargs): self._populate_hosts_and_request_paths() - return super(OpenStackBaseConnection, self).request(**kwargs) + return super(OpenStackBaseConnection, self).request(action, **kwargs) def _populate_hosts_and_request_paths(self): """ @@ -185,17 +157,16 @@ def _populate_hosts_and_request_paths(self): if self.auth_url == None: raise LibcloudError('OpenStack instance must have auth_url set') - osa = OpenStackAuthConnection(self, self.auth_url, self.user_id, self.key) + osa = OpenStackAuthConnection_v1_0(self, self.auth_url, self.user_id, self.key, tenant_id=self.tenant_id) # may throw InvalidCreds, etc osa.authenticate() self.auth_token = osa.auth_token + self.server_url = osa.urls.get('x-server-management-url', None) + self.cdn_management_url = osa.urls.get('x-cdn-management-url', None) + self.storage_url = osa.urls.get('x-storage-url', None) - # TODO: Multi-region support - self.server_url = self._get_default_region(osa.urls.get('cloudServers', [])) - self.cdn_management_url = self._get_default_region(osa.urls.get('cloudFilesCDN', [])) - self.storage_url = self._get_default_region(osa.urls.get('cloudFiles', [])) # TODO: this is even more broken, the service catalog does NOT show load # balanacers :( You must hard code in the Rackspace Load balancer URLs... self.lb_url = self.server_url.replace("servers", "ord.loadbalancers") diff --git a/libcloud/common/rackspace.py b/libcloud/common/rackspace.py index d0802d74b8..f511a3f953 100644 --- a/libcloud/common/rackspace.py +++ b/libcloud/common/rackspace.py @@ -17,8 +17,8 @@ Common settings for Rackspace Cloud Servers and Cloud Files """ -AUTH_URL_US = 'https://auth.api.rackspacecloud.com/v1.1/' -AUTH_URL_UK = 'https://lon.auth.api.rackspacecloud.com/v1.1/' +AUTH_URL_US = 'https://auth.api.rackspacecloud.com/v1.0' +AUTH_URL_UK = 'https://lon.auth.api.rackspacecloud.com/v1.0' __all__ = [ "AUTH_URL_US", diff --git a/libcloud/compute/drivers/linode.py b/libcloud/compute/drivers/linode.py index 8ea65800b3..b825b39bb9 100644 --- a/libcloud/compute/drivers/linode.py +++ b/libcloud/compute/drivers/linode.py @@ -86,7 +86,7 @@ class LinodeResponse(Response): libcloud does not take advantage of batching, so a response will always reflect the above format. A few weird quirks are caught here as well.""" - def __init__(self, response): + def __init__(self, response, connection=None): """Instantiate a LinodeResponse from the HTTP response @keyword response: The raw response returned by urllib @@ -95,6 +95,7 @@ def __init__(self, response): self.status = response.status self.headers = dict(response.getheaders()) self.error = response.reason + self.connection = connection self.invalid = LinodeException(0xFF, "Invalid JSON received from server") @@ -605,7 +606,7 @@ def _izip_longest(*args, **kwds): http://docs.python.org/library/itertools.html#itertools.izip """ fillvalue = kwds.get('fillvalue') - def sentinel(counter = ([fillvalue]*(len(args)-1)).pop): + def sentinel(counter=([fillvalue]*(len(args) - 1)).pop): yield counter() # yields the fillvalue, or raises IndexError fillers = itertools.repeat(fillvalue) iters = [itertools.chain(it, sentinel(), fillers) for it in args] diff --git a/libcloud/compute/drivers/openstack/__init__.py b/libcloud/compute/drivers/openstack/__init__.py new file mode 100644 index 0000000000..896af89526 --- /dev/null +++ b/libcloud/compute/drivers/openstack/__init__.py @@ -0,0 +1,147 @@ +# 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. +""" +OpenStack Nova driver base class and factory. +""" + +try: + import simplejson as json +except ImportError: + import json + +import sys +import httplib + +from libcloud.common.types import MalformedResponseError +from libcloud.common.base import Response +from libcloud.common.openstack import OpenStackBaseConnection +from libcloud.compute.types import Provider +from libcloud.compute.base import NodeState, NodeDriver + + +class OpenStackResponse(Response): + + def success(self): + status = int(self.status) + return status >= 200 and status <= 299 + + def has_content_type(self, content_type): + content_type_value = self.headers.get('content-type') or '' + content_type_value = content_type_value.lower() + return content_type_value.find(content_type.lower()) > -1 + + def parse_body(self): + if not self.body or self.status == httplib.NO_CONTENT: + return None + + try: + if not self.has_content_type('application/json'): + raise ValueError + return json.loads(self.body) + except ValueError: + raise MalformedResponseError('Invalid JSON Response', body=self.body, driver=self.connection.driver) + + def parse_error(self): + return '%s %s; %s' % ( + self.status, + self.error, + ';'.join([fault_data['message'] for fault_data in self.parse_body().values()]), + ) + + +class OpenStackConnection(OpenStackBaseConnection): + # Unhappy naming - this class, named per the pattern in compute drivers, + # is inheriting from a common (non-service-specific) base class. + + responseCls = OpenStackResponse + _url_key = "server_url" + content_type = 'application/json; charset=UTF-8' + + def encode_data(self, data): + return json.dumps(data) + + +class OpenStackNodeDriver(NodeDriver): + + connectionCls = OpenStackConnection + name = 'OpenStack' + api_name = 'openstack' + type = Provider.OPENSTACK + _auth_url = None + _tenant_id = None + features = { + 'create_node': ['generates_password'], + } + + NODE_STATE_MAP = { + 'BUILD': NodeState.PENDING, + 'REBUILD': NodeState.PENDING, + 'ACTIVE': NodeState.RUNNING, + 'SUSPENDED': NodeState.TERMINATED, + 'QUEUE_RESIZE': NodeState.PENDING, + 'PREP_RESIZE': NodeState.PENDING, + 'VERIFY_RESIZE': NodeState.RUNNING, + 'PASSWORD': NodeState.PENDING, + 'RESCUE': NodeState.PENDING, + 'REBUILD': NodeState.PENDING, + 'REBOOT': NodeState.REBOOTING, + 'HARD_REBOOT': NodeState.REBOOTING, + 'SHARE_IP': NodeState.PENDING, + 'SHARE_IP_NO_CONFIG': NodeState.PENDING, + 'DELETE_IP': NodeState.PENDING, + 'UNKNOWN': NodeState.UNKNOWN, + } + + def __new__(cls, *args, **kwargs): + + if cls is OpenStackNodeDriver: + # This base class is a factory. + + version = kwargs.get('version', None) # TODO: Would it be unwise to have a default version? + + if not version: + raise TypeError('An OpenStack "version" keyword argument is required.') + + ver_mod_name = 'libcloud.compute.drivers.openstack.v%s' % (version.replace('.', '_'),) + try: + __import__(ver_mod_name) + except ImportError: + raise NotImplementedError( + 'API version %s is not supported by this OpenStack driver' % (version,) + ) + + ver_mod = sys.modules[ver_mod_name] + cls = ver_mod.OpenStackNodeDriver + + return object.__new__(cls) + + def __init__(self, username, api_key, auth_url=None, tenant_id=None, ex_force_base_url=None, version=None): + # version is there because the sig must be compatible with __new__, but it's ignored. + if auth_url: + self._auth_url = auth_url + if tenant_id: + self._tenant_id = tenant_id + self._ex_force_base_url = ex_force_base_url + NodeDriver.__init__(self, username, secret=api_key) + + def _ex_connection_class_kwargs(self): + kwargs = {} + if self._auth_url: + kwargs['auth_url'] = self._auth_url + if self._tenant_id: + kwargs['tenant_id'] = self._tenant_id + if self._ex_force_base_url: + kwargs['ex_force_base_url'] = self._ex_force_base_url + return kwargs diff --git a/libcloud/compute/drivers/openstack.py b/libcloud/compute/drivers/openstack/v1_0.py similarity index 83% rename from libcloud/compute/drivers/openstack.py rename to libcloud/compute/drivers/openstack/v1_0.py index d2fd52851c..1077570ac2 100644 --- a/libcloud/compute/drivers/openstack.py +++ b/libcloud/compute/drivers/openstack/v1_0.py @@ -24,12 +24,12 @@ from xml.parsers.expat import ExpatError from libcloud.pricing import get_size_price, PRICING_DATA -from libcloud.common.base import Response from libcloud.common.types import MalformedResponseError -from libcloud.compute.types import NodeState, Provider -from libcloud.compute.base import NodeDriver, Node -from libcloud.compute.base import NodeSize, NodeImage -from libcloud.common.openstack import OpenStackBaseConnection +from libcloud.compute.types import NodeState +from libcloud.compute.base import Node, NodeSize, NodeImage +from libcloud.compute.drivers.openstack import OpenStackNodeDriver as OpenStackNodeDriverBase +from libcloud.compute.drivers.openstack import OpenStackResponse as OpenStackResponseBase +from libcloud.compute.drivers.openstack import OpenStackConnection as OpenStackConnectionBase __all__ = [ 'OpenStackResponse', @@ -42,16 +42,8 @@ NAMESPACE = 'http://docs.rackspacecloud.com/servers/api/v1.0' -class OpenStackResponse(Response): - - def success(self): - i = int(self.status) - return i >= 200 and i <= 299 - - def has_content_type(self, content_type): - content_type_value = self.headers.get('content-type') or '' - content_type_value = content_type_value.lower() - return content_type_value.find(content_type.lower()) > -1 +class OpenStackResponse(OpenStackResponseBase): + # Note that the 1.0 driver is XML-based, hence these overrides. def parse_body(self): if self.has_content_type('application/xml'): @@ -61,7 +53,7 @@ def parse_body(self): raise MalformedResponseError( 'Failed to parse XML', body=self.body, - driver=OpenStackNodeDriver) + driver=self.connection.driver) else: return self.body @@ -84,36 +76,34 @@ def parse_error(self): return '%s %s %s' % (self.status, self.error, text) -class OpenStackConnection(OpenStackBaseConnection): +class OpenStackConnection(OpenStackConnectionBase): responseCls = OpenStackResponse - _url_key = "server_url" - - def __init__(self, user_id, key, secure=True, host=None, port=None, ex_force_base_url=None): - super(OpenStackConnection, self).__init__( - user_id, key, host=host, port=port, ex_force_base_url=ex_force_base_url) - self.api_version = 'v1.0' - self.accept_format = 'application/xml' - - def request(self, action, params=None, data='', headers=None, - method='GET'): - if not headers: - headers = {} - if not params: - params = {} - - if method in ("POST", "PUT"): - headers = {'Content-Type': 'application/xml; charset=UTF-8'} + accept_format = 'application/xml' + content_type = 'application/xml; charset=UTF-8' + + def request(self, action, params=None, data='', headers=None, method='GET'): + params = params or {} + + # Note: can't move this bit baseward because it breaks the 1.1 server. if method == "GET": params['cache-busting'] = os.urandom(8).encode('hex') + return super(OpenStackConnection, self).request( action=action, params=params, data=data, method=method, headers=headers ) + def encode_data(self, data): + if isinstance(data, basestring): + # Already encoded. One oddball method (_node_action). :P + return data + else: + return ET.tostring(data) -class OpenStackNodeDriver(NodeDriver): + +class OpenStackNodeDriver(OpenStackNodeDriverBase): """ OpenStack node driver. @@ -124,38 +114,6 @@ class OpenStackNodeDriver(NodeDriver): - flavorId: id of flavor """ connectionCls = OpenStackConnection - type = Provider.OPENSTACK - api_name = 'openstack' - name = 'OpenStack' - - features = {"create_node": ["generates_password"]} - - NODE_STATE_MAP = {'BUILD': NodeState.PENDING, - 'REBUILD': NodeState.PENDING, - 'ACTIVE': NodeState.RUNNING, - 'SUSPENDED': NodeState.TERMINATED, - 'QUEUE_RESIZE': NodeState.PENDING, - 'PREP_RESIZE': NodeState.PENDING, - 'VERIFY_RESIZE': NodeState.RUNNING, - 'PASSWORD': NodeState.PENDING, - 'RESCUE': NodeState.PENDING, - 'REBUILD': NodeState.PENDING, - 'REBOOT': NodeState.REBOOTING, - 'HARD_REBOOT': NodeState.REBOOTING, - 'SHARE_IP': NodeState.PENDING, - 'SHARE_IP_NO_CONFIG': NodeState.PENDING, - 'DELETE_IP': NodeState.PENDING, - 'UNKNOWN': NodeState.UNKNOWN} - - def __init__(self, *args, **kwargs): - self._ex_force_base_url = kwargs.pop('ex_force_base_url', None) - super(OpenStackNodeDriver, self).__init__(*args, **kwargs) - - def _ex_connection_class_kwargs(self): - if self._ex_force_base_url: - return {'ex_force_base_url': self._ex_force_base_url} - return {} - def list_nodes(self): return self._to_nodes(self.connection.request('/servers/detail') @@ -184,8 +142,7 @@ def _change_password_or_name(self, node, name=None, password=None): server_elm = ET.Element('server', body) - resp = self.connection.request( - uri, method='PUT', data=ET.tostring(server_elm)) + resp = self.connection.request(uri, method='PUT', data=server_elm) if resp.status == 204 and password != None: node.extra['password'] = password @@ -252,9 +209,7 @@ def create_node(self, **kwargs): files_elm = self._files_to_xml(kwargs.get("ex_files", {})) if files_elm: server_elm.append(files_elm) - resp = self.connection.request("/servers", - method='POST', - data=ET.tostring(server_elm)) + resp = self.connection.request("/servers", method='POST', data=server_elm) return self._to_node(resp.object) def ex_resize(self, node, size): @@ -276,7 +231,7 @@ def ex_resize(self, node, size): resp = self.connection.request("/servers/%s/action" % (node.id), method='POST', - data=ET.tostring(elm)) + data=elm) return resp.status == 202 def ex_confirm_resize(self, node): @@ -297,7 +252,7 @@ def ex_confirm_resize(self, node): resp = self.connection.request("/servers/%s/action" % (node.id), method='POST', - data=ET.tostring(elm)) + data=elm) return resp.status == 204 def ex_revert_resize(self, node): @@ -318,7 +273,7 @@ def ex_revert_resize(self, node): resp = self.connection.request("/servers/%s/action" % (node.id), method='POST', - data=ET.tostring(elm)) + data=elm) return resp.status == 204 def ex_rebuild(self, node_id, image_id): @@ -337,7 +292,7 @@ def ex_rebuild(self, node_id, image_id): ) resp = self.connection.request("/servers/%s/action" % node_id, method='POST', - data=ET.tostring(elm)) + data=elm) return resp.status == 202 def ex_create_ip_group(self, group_name, node_id=None): @@ -358,9 +313,7 @@ def ex_create_ip_group(self, group_name, node_id=None): {'id': node_id} ) - resp = self.connection.request('/shared_ip_groups', - method='POST', - data=ET.tostring(group_elm)) + resp = self.connection.request('/shared_ip_groups', method='POST', data=group_elm) return self._to_shared_ip_group(resp.object) def ex_list_ip_groups(self, details=False): @@ -394,9 +347,7 @@ def ex_share_ip(self, group_id, node_id, ip, configure_node=True): uri = '/servers/%s/ips/public/%s' % (node_id, ip) - resp = self.connection.request(uri, - method='PUT', - data=ET.tostring(elm)) + resp = self.connection.request(uri, method='PUT', data=elm) return resp.status == 202 def ex_unshare_ip(self, node_id, ip): @@ -605,7 +556,7 @@ def ex_save_image(self, node, name): return self._to_image(self.connection.request("/images", method="POST", - data=ET.tostring(image_elm)).object) + data=image_elm).object) def _to_shared_ip_group(self, el): servers_el = self._findall(el, 'servers') diff --git a/libcloud/compute/drivers/openstack/v1_1.py b/libcloud/compute/drivers/openstack/v1_1.py new file mode 100644 index 0000000000..df1544a0a7 --- /dev/null +++ b/libcloud/compute/drivers/openstack/v1_1.py @@ -0,0 +1,178 @@ +# 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. +''' +OpenStack Nova 1.1 driver +''' + +from libcloud.compute.base import Node, NodeState, NodeSize, NodeImage +from libcloud.compute.drivers.openstack import OpenStackNodeDriver as OpenStackNodeDriverBase + + +class OpenStackNodeDriver(OpenStackNodeDriverBase): + + def list_nodes(self): + return [ + self._to_node(api_server) + for api_server in self.connection.request('/servers/detail').object['servers'] + ] + + def list_sizes(self): + return [ + self._to_size(api_flavor) + for api_flavor in self.connection.request('/flavors/detail').object['flavors'] + ] + + def list_images(self): + return [ + self._to_image(api_image) + for api_image + in self.connection.request('/images/detail?status=ACTIVE').object['images'] + ] + + def create_node(self, name, size, image, metadata=None, files=None): + # TODO: should we ref images and flavors by link instead of id? It + # opens up the theoretical possibility of remote images and such. + # TODO: "personality" support. + # TODO: "reservation_id" support. + # TODO: "security_groups" support. + server_params = dict( + name=name, + flavorRef=str(size.id), + imageRef=str(image.id), + ) + if metadata: + server_params['metadata'] = metadata + if files: + server_params['files'] = files + + return self._to_node( + self.connection.request( + '/servers', method='POST', data=dict(server=server_params) + ).object['server'] + ) + + def destroy_node(self, node): + self.connection.request('/servers/%s' % (node.id,), method='DELETE') + + def reboot_node(self, node, hard=False): + self._node_action(node, 'reboot', type=('SOFT', 'HARD')[hard]) + + def ex_set_password(self, node, password): + self._node_action(node, 'changePassword', adminPass=password) + node.extra['password'] = password + + def ex_rebuild(self, node, image, name=None, metadata=None): + # TODO: "personality" support. + optional_params = {} + if name: + optional_params['name'] = name + if metadata: + optional_params['metadata'] = metadata + + # Note: At this time, the docs say this should be "image={'id': image.id}". + # Educating guessing turned up the actual working syntax here. + self._node_action(node, 'rebuild', imageRef=image.id, **optional_params) + + def ex_resize(self, node, size): + # Note: At this time, the docs say this should be "flavor={'id': image.id}". + # Educating guessing turned up the actual working syntax here. + self._node_action(node, 'resize', flavorRef=size.id) + + def ex_confirm_resize(self, node): + self._node_action(node, 'confirmResize') + + def ex_revert_resize(self, node): + self._node_action(node, 'revertResize') + + def ex_save_image(self, node, name, metadata=None): + optional_params = {} + if metadata: + optional_params['metadata'] = metadata + self._node_action(node, 'createImage', name=name, **optional_params) + + def ex_update_node(self, node, **node_updates): + # At this time, only name is supported, but this signature covers the future. + self.connection.request( + '/servers/%s' % (node.id,), method='PUT', data=dict(server=node_updates) + ) + + def ex_get_node(self, node_id): + return self._to_node(self.connection.request('/servers/%s' % (node_id,)).object['server']) + + def ex_get_size(self, size_id): + return self._to_size(self.connection.request('/flavors/%s' % (size_id,)).object['flavor']) + + def ex_get_image(self, image_id): + return self._to_image(self.connection.request('/images/%s' % (image_id,)).object['image']) + + def ex_delete_image(self, image): + self.connection.request('/images/%s' % (image.id,), method='DELETE') + + def ex_quotas(self, tenant_id=None): + if tenant_id is None: + tenant_id = self.connection.tenant_id + + return self.connection.request('/os-quota-sets/%s' % (tenant_id,)).object['quota_set'] + + def _node_action(self, node, action, **params): + params = params or None + self.connection.request('/servers/%s/action' % (node.id,), method='POST', data={action: params}) + + def _to_node(self, api_node): + return Node( + id=api_node['id'], + name=api_node['name'], + state=self.NODE_STATE_MAP.get(api_node['status'], NodeState.UNKNOWN), + public_ip=[addr_desc['addr'] for addr_desc in api_node['addresses'].get('public', [])], + private_ip=[addr_desc['addr'] for addr_desc in api_node['addresses'].get('private', [])], + driver=self, + extra=dict( + hostId=api_node['hostId'], + # Docs says "tenantId", but actual is "tenant_id". *sigh* Best handle both. + tenantId=api_node.get('tenant_id') or api_node['tenantId'], + imageId=api_node['image']['id'], + flavorId=api_node['flavor']['id'], + uri=(link['href'] for link in api_node['links'] if link['rel'] == 'self').next(), + metadata=api_node['metadata'], + password=api_node.get('adminPass'), + ), + ) + + def _to_size(self, api_flavor, price=None, bandwidth=None): + # if provider-specific subclasses can get better values for + # price/bandwidth, then can pass them in when they super(). + return NodeSize( + id=api_flavor['id'], + name=api_flavor['name'], + ram=api_flavor['ram'], + disk=api_flavor['disk'], + bandwidth=bandwidth, + price=price, + driver=self, + ) + + def _to_image(self, api_image): + return NodeImage( + id=api_image['id'], + name=api_image['name'], + driver=self, + extra=dict( + updated=api_image['updated'], + created=api_image['created'], + status=api_image['status'], + progress=api_image['progress'], + metadata=api_image.get('metadata'), + ), + ) diff --git a/libcloud/compute/drivers/rackspace.py b/libcloud/compute/drivers/rackspace.py index baef06e9d8..fd40840f69 100644 --- a/libcloud/compute/drivers/rackspace.py +++ b/libcloud/compute/drivers/rackspace.py @@ -17,7 +17,7 @@ """ from libcloud.compute.types import Provider from libcloud.compute.base import NodeLocation -from libcloud.compute.drivers.openstack import OpenStackConnection, OpenStackNodeDriver, OpenStackResponse +from libcloud.compute.drivers.openstack.v1_0 import OpenStackConnection, OpenStackNodeDriver, OpenStackResponse from libcloud.common.rackspace import ( AUTH_URL_US, AUTH_URL_UK) diff --git a/libcloud/compute/drivers/rimuhosting.py b/libcloud/compute/drivers/rimuhosting.py index 36966ff621..e1e90af645 100644 --- a/libcloud/compute/drivers/rimuhosting.py +++ b/libcloud/compute/drivers/rimuhosting.py @@ -41,11 +41,12 @@ def __repr__(self): return "" % (self.args[0]) class RimuHostingResponse(Response): - def __init__(self, response): + def __init__(self, response, connection=None): self.body = response.read() self.status = response.status self.headers = dict(response.getheaders()) self.error = response.reason + self.connection = connection if self.success(): self.object = self.parse_body() @@ -81,7 +82,7 @@ class RimuHostingConnection(ConnectionKey): def __init__(self, key, secure=True): # override __init__ so that we can set secure of False for testing - ConnectionKey.__init__(self,key,secure) + ConnectionKey.__init__(self, key, secure) def add_default_headers(self, headers): # We want JSON back from the server. Could be application/xml @@ -116,16 +117,16 @@ def __init__(self, key, host=API_HOST, port=443, # Pass in some extra vars so that self.key = key self.secure = secure - self.connection = self.connectionCls(key ,secure) + self.connection = self.connectionCls(key, secure) self.connection.host = host self.connection.api_context = api_context self.connection.port = port self.connection.driver = self self.connection.connect() - def _order_uri(self, node,resource): + def _order_uri(self, node, resource): # Returns the order uri with its resourse appended. - return "/orders/%s/%s" % (node.id,resource) + return "/orders/%s/%s" % (node.id, resource) # TODO: Get the node state. def _to_node(self, order): @@ -142,7 +143,7 @@ def _to_node(self, order): 'monthly_recurring_fee': order.get('billing_info').get('monthly_recurring_fee')}) return n - def _to_size(self,plan): + def _to_size(self, plan): return NodeSize( id=plan['pricing_plan_code'], name=plan['pricing_plan_description'], @@ -153,7 +154,7 @@ def _to_size(self,plan): driver=self.connection.driver ) - def _to_image(self,image): + def _to_image(self, image): return NodeImage(id=image['distro_code'], name=image['distro_description'], driver=self.connection.driver) @@ -188,15 +189,15 @@ def reboot_node(self, node): # PUT the state of RESTARTING to restart a VPS. # All data is encoded as JSON data = {'reboot_request':{'running_state':'RESTARTING'}} - uri = self._order_uri(node,'vps/running-state') - self.connection.request(uri,data=json.dumps(data),method='PUT') + uri = self._order_uri(node, 'vps/running-state') + self.connection.request(uri, data=json.dumps(data), method='PUT') # XXX check that the response was actually successful return True def destroy_node(self, node): # Shutdown a VPS. - uri = self._order_uri(node,'vps') - self.connection.request(uri,method='DELETE') + uri = self._order_uri(node, 'vps') + self.connection.request(uri, method='DELETE') # XXX check that the response was actually successful return True diff --git a/libcloud/compute/drivers/voxel.py b/libcloud/compute/drivers/voxel.py index 1e6659a1fb..14e3bc4038 100644 --- a/libcloud/compute/drivers/voxel.py +++ b/libcloud/compute/drivers/voxel.py @@ -32,9 +32,9 @@ class VoxelResponse(Response): - def __init__(self, response): + def __init__(self, response, connection=None): self.parsed = None - super(VoxelResponse, self).__init__(response) + super(VoxelResponse, self).__init__(response, connection=connection) def parse_body(self): if not self.body: @@ -79,7 +79,7 @@ class VoxelConnection(ConnectionUserAndKey): def add_default_params(self, params): params["key"] = self.user_id - params["timestamp"] = datetime.datetime.utcnow().isoformat()+"+0000" + params["timestamp"] = datetime.datetime.utcnow().isoformat() + "+0000" for param in params.keys(): if params[param] is None: @@ -93,7 +93,7 @@ def add_default_params(self, params): for key in keys: if params[key]: if not params[key] is None: - md5.update("%s%s"% (key, params[key])) + md5.update("%s%s" % (key, params[key])) else: md5.update(key) params['api_sig'] = md5.hexdigest() @@ -121,7 +121,7 @@ class VoxelNodeDriver(NodeDriver): name = 'Voxel VoxCLOUD' def _initialize_instance_types(): - for cpus in range(1,14): + for cpus in range(1, 14): if cpus == 1: name = "Single CPU" else: @@ -129,7 +129,7 @@ def _initialize_instance_types(): id = "%dcpu" % cpus ram = cpus * RAM_PER_CPU - VOXEL_INSTANCE_TYPES[id]= { + VOXEL_INSTANCE_TYPES[id] = { 'id': id, 'name': name, 'ram': ram, @@ -229,12 +229,12 @@ def create_node(self, **kwargs): if self._getstatus(object): return Node( - id = object.findtext("device/id"), - name = kwargs["name"], - state = NODE_STATE_MAP[object.findtext("device/status")], - public_ip = kwargs.get("publicip", None), - private_ip = kwargs.get("privateip", None), - driver = self.connection.driver + id=object.findtext("device/id"), + name=kwargs["name"], + state=NODE_STATE_MAP[object.findtext("device/status")], + public_ip=kwargs.get("publicip", None), + private_ip=kwargs.get("privateip", None), + driver=self.connection.driver ) else: return None @@ -286,23 +286,23 @@ def _to_nodes(self, object): public_ip = private_ip = None ipassignments = element.findall("ipassignments/ipassignment") for ip in ipassignments: - if ip.attrib["type"] =="frontend": + if ip.attrib["type"] == "frontend": public_ip = ip.text elif ip.attrib["type"] == "backend": private_ip = ip.text - nodes.append(Node(id= element.attrib['id'], + nodes.append(Node(id=element.attrib['id'], name=element.attrib['label'], state=state, - public_ip= public_ip, - private_ip= private_ip, + public_ip=public_ip, + private_ip=private_ip, driver=self.connection.driver)) return nodes def _to_images(self, object): images = [] for element in object.findall("images/image"): - images.append(NodeImage(id = element.attrib["id"], - name = element.attrib["summary"], - driver = self.connection.driver)) + images.append(NodeImage(id=element.attrib["id"], + name=element.attrib["summary"], + driver=self.connection.driver)) return images diff --git a/libcloud/loadbalancer/drivers/rackspace.py b/libcloud/loadbalancer/drivers/rackspace.py index e26138c8b6..3ea81ddc2f 100644 --- a/libcloud/loadbalancer/drivers/rackspace.py +++ b/libcloud/loadbalancer/drivers/rackspace.py @@ -29,6 +29,7 @@ from libcloud.common.rackspace import ( AUTH_URL_US, AUTH_URL_UK) + class RackspaceResponse(Response): def success(self): @@ -46,11 +47,6 @@ class RackspaceConnection(OpenStackBaseConnection): auth_url = AUTH_URL_US _url_key = "lb_url" - def __init__(self, user_id, key, secure=True): - super(RackspaceConnection, self).__init__(user_id, key, secure) - self.api_version = 'v1.0' - self.accept_format = 'application/json' - def request(self, action, params=None, data='', headers=None, method='GET'): if not headers: headers = {} diff --git a/libcloud/storage/drivers/cloudfiles.py b/libcloud/storage/drivers/cloudfiles.py index 6662bd684d..c5ae2fa10c 100644 --- a/libcloud/storage/drivers/cloudfiles.py +++ b/libcloud/storage/drivers/cloudfiles.py @@ -80,9 +80,11 @@ def parse_body(self): return data + class CloudFilesRawResponse(CloudFilesResponse, RawResponse): pass + class CloudFilesConnection(OpenStackBaseConnection): """ Base connection class for the Cloudfiles driver. @@ -93,11 +95,6 @@ class CloudFilesConnection(OpenStackBaseConnection): rawResponseCls = CloudFilesRawResponse _url_key = "storage_url" - def __init__(self, user_id, key, secure=True): - super(CloudFilesConnection, self).__init__(user_id, key, secure=secure) - self.api_version = API_VERSION - self.accept_format = 'application/json' - def request(self, action, params=None, data='', headers=None, method='GET', raw=False, cdn_request=False): if not headers: @@ -502,6 +499,7 @@ def _headers_to_object(self, name, container, headers): meta_data=meta_data, container=container, driver=self) return obj + class CloudFilesUSStorageDriver(CloudFilesStorageDriver): """ Cloudfiles storage driver for the US endpoint. @@ -511,6 +509,7 @@ class CloudFilesUSStorageDriver(CloudFilesStorageDriver): name = 'CloudFiles (US)' connectionCls = CloudFilesUSConnection + class CloudFilesUKStorageDriver(CloudFilesStorageDriver): """ Cloudfiles storage driver for the UK endpoint. diff --git a/test/__init__.py b/test/__init__.py index 1c87072e17..fbce49a3a8 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -221,7 +221,7 @@ class MockRawResponse(BaseMockHttpObject): type = None responseCls = MockResponse - def __init__(self): + def __init__(self, connection=None): super(MockRawResponse, self).__init__() self._data = [] self._current_item = 0 @@ -230,6 +230,7 @@ def __init__(self): self._response = None self._headers = None self._reason = None + self.connection = connection def next(self): if self._current_item == len(self._data): diff --git a/test/secrets.py-dist b/test/secrets.py-dist index f7e3fc668f..a2aee11395 100644 --- a/test/secrets.py-dist +++ b/test/secrets.py-dist @@ -24,8 +24,7 @@ ECP_PARAMS = ('user_name', 'password') GANDI_PARAMS = ('user',) HOSTINGCOM_PARAMS = ('user', 'secret') IBM_PARAMS = ('user', 'secret') -# OPENSTACK_PARAMS = ('user_name', 'api_key', secure_bool, 'host', port_int) -OPENSTACK_PARAMS = ('user_name', 'api_key', False, 'host', 8774) +OPENSTACK_PARAMS = ('user_name', 'api_key') OPENNEBULA_PARAMS = ('user', 'key') OPSOURCE_PARAMS = ('user', 'password') RACKSPACE_PARAMS = ('user', 'key')