You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@libcloud.apache.org by an...@apache.org on 2016/10/06 22:24:06 UTC
[3/4] libcloud git commit: Replace RunAbove code by OVH
Replace RunAbove code by OVH
Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo
Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/d8100961
Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/d8100961
Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/d8100961
Branch: refs/heads/trunk
Commit: d8100961f0ac5ff2051dcfe8e56ad5fce81a257d
Parents: 25c5ba9
Author: ZuluPro <mo...@hotmail.com>
Authored: Wed Oct 5 14:44:38 2016 -0400
Committer: Anthony Shaw <an...@apache.org>
Committed: Fri Oct 7 09:23:34 2016 +1100
----------------------------------------------------------------------
libcloud/common/ovh.py | 173 ++++++++++++
libcloud/common/runabove.py | 165 -----------
libcloud/compute/drivers/ovh.py | 455 ++++++++++++++++++++++++++++++
libcloud/compute/drivers/runabove.py | 453 -----------------------------
libcloud/compute/providers.py | 4 +-
libcloud/compute/types.py | 2 +-
libcloud/test/common/test_ovh.py | 29 ++
7 files changed, 660 insertions(+), 621 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/libcloud/blob/d8100961/libcloud/common/ovh.py
----------------------------------------------------------------------
diff --git a/libcloud/common/ovh.py b/libcloud/common/ovh.py
new file mode 100644
index 0000000..3854e0b
--- /dev/null
+++ b/libcloud/common/ovh.py
@@ -0,0 +1,173 @@
+# 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.
+
+
+import hashlib
+import time
+
+try:
+ import simplejson as json
+except ImportError:
+ import json
+
+from libcloud.utils.py3 import httplib
+from libcloud.utils.connection import get_response_object
+from libcloud.common.types import InvalidCredsError
+from libcloud.common.base import ConnectionUserAndKey, JsonResponse
+from libcloud.httplib_ssl import LibcloudHTTPSConnection
+
+__all__ = [
+ 'OvhResponse',
+ 'OvhConnection'
+]
+
+API_HOST = 'api.ovh.com'
+API_ROOT = '/1.0'
+LOCATIONS = {
+ 'SBG1': {'id': 'SBG1', 'name': 'Strasbourg 1', 'country': 'FR'},
+ 'BHS1': {'id': 'BHS1', 'name': 'Montreal 1', 'country': 'CA'},
+ 'GRA1': {'id': 'GRA1', 'name': 'Gravelines 1', 'country': 'FR'}
+}
+DEFAULT_ACCESS_RULES = [
+ {'method': 'GET', 'path': '/*'},
+ {'method': 'POST', 'path': '/*'},
+ {'method': 'PUT', 'path': '/*'},
+ {'method': 'DELETE', 'path': '/*'},
+]
+
+
+class OvhException(Exception):
+ pass
+
+
+class OvhResponse(JsonResponse):
+ def parse_error(self):
+ response = super(OvhResponse, self).parse_body()
+ response = response or {}
+
+ if response.get('errorCode', None) == 'INVALID_SIGNATURE':
+ raise InvalidCredsError('Signature validation failed, probably '
+ 'using invalid credentials')
+
+ return self.body
+
+
+class OvhConnection(ConnectionUserAndKey):
+ """
+ A connection to the Ovh API
+
+ Wraps SSL connections to the Ovh API, automagically injecting the
+ parameters that the API needs for each request.
+ """
+ host = API_HOST
+ request_path = API_ROOT
+ responseCls = OvhResponse
+ timestamp = None
+ ua = []
+ LOCATIONS = LOCATIONS
+ _timedelta = None
+
+ allow_insecure = True
+
+ def __init__(self, user_id, *args, **kwargs):
+ self.consumer_key = kwargs.pop('ex_consumer_key', None)
+ if self.consumer_key is None:
+ consumer_key_json = self.request_consumer_key(user_id)
+ msg = ("Your consumer key isn't validated, "
+ "go to '%(validationUrl)s' for valid it. After instantiate "
+ "your driver with \"ex_consumer_key='%(consumerKey)s'\"." %
+ consumer_key_json)
+ raise OvhException(msg)
+ super(OvhConnection, self).__init__(user_id, *args, **kwargs)
+
+ def request_consumer_key(self, user_id):
+ action = self.request_path + '/auth/credential'
+ data = json.dumps({
+ 'accessRules': DEFAULT_ACCESS_RULES,
+ 'redirection': 'http://ovh.com',
+ })
+ headers = {
+ 'Content-Type': 'application/json',
+ 'X-Ovh-Application': user_id,
+ }
+ httpcon = LibcloudHTTPSConnection(self.host)
+ httpcon.request(method='POST', url=action, body=data, headers=headers)
+ response = httpcon.getresponse()
+
+ if response.status == httplib.UNAUTHORIZED:
+ raise InvalidCredsError()
+
+ body = response.read()
+ json_response = json.loads(body)
+ httpcon.close()
+ return json_response
+
+ def get_timestamp(self):
+ if not self._timedelta:
+ url = 'https://%s%s/auth/time' % (API_HOST, API_ROOT)
+ response = get_response_object(url=url, method='GET', headers={})
+ if not response or not response.body:
+ raise Exception('Failed to get current time from Ovh API')
+
+ timestamp = int(response.body)
+ self._timedelta = timestamp - int(time.time())
+ return int(time.time()) + self._timedelta
+
+ def make_signature(self, method, action, params, data, timestamp):
+ full_url = 'https://%s%s' % (API_HOST, action)
+ if params:
+ full_url += '?'
+ for key, value in params.items():
+ full_url += '%s=%s&' % (key, value)
+ else:
+ full_url = full_url[:-1]
+ sha1 = hashlib.sha1()
+ base_signature = "+".join([
+ self.key,
+ self.consumer_key,
+ method.upper(),
+ full_url,
+ data if data else '',
+ str(timestamp),
+ ])
+ sha1.update(base_signature.encode())
+ signature = '$1$' + sha1.hexdigest()
+ return signature
+
+ def add_default_params(self, params):
+ return params
+
+ def add_default_headers(self, headers):
+ headers.update({
+ 'X-Ovh-Application': self.user_id,
+ 'X-Ovh-Consumer': self.consumer_key,
+ 'Content-type': 'application/json',
+ })
+ return headers
+
+ def request(self, action, params=None, data=None, headers=None,
+ method='GET', raw=False):
+ data = json.dumps(data) if data else None
+ timestamp = self.get_timestamp()
+ signature = self.make_signature(method, action, params, data,
+ timestamp)
+ headers = headers or {}
+ headers.update({
+ 'X-Ovh-Timestamp': timestamp,
+ 'X-Ovh-Signature': signature
+ })
+ return super(OvhConnection, self)\
+ .request(action, params=params, data=data, headers=headers,
+ method=method, raw=raw)
http://git-wip-us.apache.org/repos/asf/libcloud/blob/d8100961/libcloud/common/runabove.py
----------------------------------------------------------------------
diff --git a/libcloud/common/runabove.py b/libcloud/common/runabove.py
deleted file mode 100644
index 0f08b59..0000000
--- a/libcloud/common/runabove.py
+++ /dev/null
@@ -1,165 +0,0 @@
-# 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.
-
-
-import hashlib
-import time
-
-try:
- import simplejson as json
-except ImportError:
- import json
-
-from libcloud.utils.py3 import httplib
-from libcloud.utils.connection import get_response_object
-from libcloud.common.types import InvalidCredsError
-from libcloud.common.base import ConnectionUserAndKey, JsonResponse
-from libcloud.httplib_ssl import LibcloudHTTPSConnection
-
-__all__ = [
- 'RunAboveResponse',
- 'RunAboveConnection'
-]
-
-API_HOST = 'api.runabove.com'
-API_ROOT = '/1.0'
-LOCATIONS = {
- 'SBG-1': {'id': 'SBG-1', 'name': 'Strasbourg 1', 'country': 'FR'},
- 'BHS-1': {'id': 'BHS-1', 'name': 'Montreal 1', 'country': 'CA'}
-}
-DEFAULT_ACCESS_RULES = [
- {'method': 'GET', 'path': '/*'},
- {'method': 'POST', 'path': '/*'},
- {'method': 'PUT', 'path': '/*'},
- {'method': 'DELETE', 'path': '/*'},
-]
-
-
-class RunAboveException(Exception):
- pass
-
-
-class RunAboveResponse(JsonResponse):
- def parse_error(self):
- response = super(RunAboveResponse, self).parse_body()
- response = response or {}
-
- if response.get('errorCode', None) == 'INVALID_SIGNATURE':
- raise InvalidCredsError('Signature validation failed, probably '
- 'using invalid credentials')
-
- return self.body
-
-
-class RunAboveConnection(ConnectionUserAndKey):
- """
- A connection to the RunAbove API
-
- Wraps SSL connections to the RunAbove API, automagically injecting the
- parameters that the API needs for each request.
- """
- host = API_HOST
- request_path = API_ROOT
- responseCls = RunAboveResponse
- timestamp = None
- ua = []
- LOCATIONS = LOCATIONS
- _timedelta = None
-
- allow_insecure = True
-
- def __init__(self, user_id, *args, **kwargs):
- self.consumer_key = kwargs.pop('ex_consumer_key', None)
- if self.consumer_key is None:
- consumer_key_json = self.request_consumer_key(user_id)
- msg = ("Your consumer key isn't validated, "
- "go to '%(validationUrl)s' for valid it. After instantiate "
- "your driver with \"ex_consumer_key='%(consumerKey)s'\"." %
- consumer_key_json)
- raise RunAboveException(msg)
- super(RunAboveConnection, self).__init__(user_id, *args, **kwargs)
-
- def request_consumer_key(self, user_id):
- action = self.request_path + '/auth/credential'
- data = json.dumps({
- 'accessRules': DEFAULT_ACCESS_RULES,
- 'redirection': 'http://runabove.com',
- })
- headers = {
- 'Content-Type': 'application/json',
- 'X-Ra-Application': user_id,
- }
- httpcon = LibcloudHTTPSConnection(self.host)
- httpcon.request(method='POST', url=action, body=data, headers=headers)
- response = httpcon.getresponse()
-
- if response.status == httplib.UNAUTHORIZED:
- raise InvalidCredsError()
-
- body = response.read()
- json_response = json.loads(body)
- httpcon.close()
- return json_response
-
- def get_timestamp(self):
- if not self._timedelta:
- url = 'https://%s/%s/auth/time' % (API_HOST, API_ROOT)
- response = get_response_object(url=url, method='GET', headers={})
- if not response or not response.body:
- raise Exception('Failed to get current time from RunAbove API')
-
- timestamp = int(response.body)
- self._timedelta = timestamp - int(time.time())
- return int(time.time()) + self._timedelta
-
- def make_signature(self, method, action, data, timestamp):
- full_url = 'https://%s%s' % (API_HOST, action)
- sha1 = hashlib.sha1()
- base_signature = "+".join([
- self.key,
- self.consumer_key,
- method.upper(),
- full_url,
- data if data else '',
- str(timestamp),
- ])
- sha1.update(base_signature.encode())
- signature = '$1$' + sha1.hexdigest()
- return signature
-
- def add_default_params(self, params):
- return params
-
- def add_default_headers(self, headers):
- headers.update({
- 'X-Ra-Application': self.user_id,
- 'X-Ra-Consumer': self.consumer_key,
- 'Content-type': 'application/json',
- })
- return headers
-
- def request(self, action, params=None, data=None, headers=None,
- method='GET', raw=False):
- data = json.dumps(data) if data else None
- timestamp = self.get_timestamp()
- signature = self.make_signature(method, action, data, timestamp)
- headers = headers or {}
- headers.update({
- 'X-Ra-Timestamp': timestamp,
- 'X-Ra-Signature': signature
- })
- return super(RunAboveConnection, self)\
- .request(action, params=params, data=data, headers=headers,
- method=method, raw=raw)
http://git-wip-us.apache.org/repos/asf/libcloud/blob/d8100961/libcloud/compute/drivers/ovh.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/drivers/ovh.py b/libcloud/compute/drivers/ovh.py
new file mode 100644
index 0000000..a103650
--- /dev/null
+++ b/libcloud/compute/drivers/ovh.py
@@ -0,0 +1,455 @@
+# 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.
+"""
+Ovh driver
+"""
+from libcloud.common.ovh import API_ROOT, OvhConnection
+from libcloud.compute.base import NodeDriver, NodeSize, Node, NodeLocation
+from libcloud.compute.base import NodeImage, StorageVolume
+from libcloud.compute.types import Provider, StorageVolumeState
+from libcloud.compute.drivers.openstack import OpenStackNodeDriver
+from libcloud.compute.drivers.openstack import OpenStackKeyPair
+
+
+class OvhNodeDriver(NodeDriver):
+ """
+ Libcloud driver for the Ovh API
+
+ For more information on the Ovh API, read the official reference:
+
+ https://api.ovh.com/console/
+ """
+ type = Provider.OVH
+ name = "Ovh"
+ website = 'https://www.ovh.com/'
+ connectionCls = OvhConnection
+ features = {'create_node': ['ssh_key']}
+ api_name = 'ovh'
+
+ NODE_STATE_MAP = OpenStackNodeDriver.NODE_STATE_MAP
+ VOLUME_STATE_MAP = OpenStackNodeDriver.VOLUME_STATE_MAP
+
+ def __init__(self, key, secret, ex_project_id, ex_consumer_key=None):
+ """
+ Instantiate the driver with the given API credentials.
+
+ :param key: Your application key (required)
+ :type key: ``str``
+
+ :param secret: Your application secret (required)
+ :type secret: ``str``
+
+ :param ex_project_id: Your project ID
+ :type ex_project_id: ``str``
+
+ :param ex_consumer_key: Your consumer key (required)
+ :type ex_consumer_key: ``str``
+
+ :rtype: ``None``
+ """
+ self.datacenter = None
+ self.project_id = ex_project_id
+ self.consumer_key = ex_consumer_key
+ NodeDriver.__init__(self, key, secret, ex_consumer_key=ex_consumer_key)
+
+ def list_nodes(self, location=None):
+ """
+ List all nodes.
+
+ :keyword location: Location (region) used as filter
+ :type location: :class:`NodeLocation`
+
+ :return: List of node objects
+ :rtype: ``list`` of :class:`Node`
+ """
+ action = '%s/cloud/project/%s/instance' % (API_ROOT, self.project_id)
+ data = {}
+ if location:
+ data['region'] = location.id
+ response = self.connection.request(action, data=data)
+ return self._to_nodes(response.object)
+
+ def ex_get_node(self, node_id):
+ """
+ Get a individual node.
+
+ :keyword node_id: Node's ID
+ :type node_id: ``str``
+
+ :return: Created node
+ :rtype : :class:`Node`
+ """
+ action = '%s/cloud/project/%s/instance/%s' % (
+ API_ROOT, self.project_id, node_id)
+ response = self.connection.request(action, method='GET')
+ return self._to_node(response.object)
+
+ def create_node(self, name, image, size, location, ex_keyname=None):
+ """
+ Create a new node
+
+ :keyword name: Name of created node
+ :type name: ``str``
+
+ :keyword image: Image used for node
+ :type image: :class:`NodeImage`
+
+ :keyword size: Size (flavor) used for node
+ :type size: :class:`NodeSize`
+
+ :keyword location: Location (region) where to create node
+ :type location: :class:`NodeLocation`
+
+ :keyword ex_keyname: Name of SSH key used
+ :type ex_keyname: ``str``
+
+ :return: Created node
+ :rtype : :class:`Node`
+ """
+ action = '%s/cloud/project/%s/instance' % (API_ROOT, self.project_id)
+ data = {
+ 'name': name,
+ 'imageId': image.id,
+ 'flavorId': size.id,
+ 'region': location.id,
+ }
+ if ex_keyname:
+ key_id = self.get_key_pair(ex_keyname, location).extra['id']
+ data['sshKeyId'] = key_id
+ response = self.connection.request(action, data=data, method='POST')
+ return self._to_node(response.object)
+
+ def destroy_node(self, node):
+ action = '%s/cloud/project/%s/instance/%s' % (
+ API_ROOT, self.project_id, node.id)
+ self.connection.request(action, method='DELETE')
+ return True
+
+ def list_sizes(self, location=None):
+ action = '%s/cloud/project/%s/flavor' % (API_ROOT, self.project_id)
+ params = {}
+ if location:
+ params['region'] = location.id
+ response = self.connection.request(action, params=params)
+ return self._to_sizes(response.object)
+
+ def ex_get_size(self, size_id):
+ """
+ Get an individual size (flavor).
+
+ :keyword size_id: Size's ID
+ :type size_id: ``str``
+
+ :return: Size
+ :rtype: :class:`NodeSize`
+ """
+ action = '%s/cloud/project/%s/flavor/%s' % (
+ API_ROOT, self.project_id, size_id)
+ response = self.connection.request(action)
+ return self._to_size(response.object)
+
+ def list_images(self, location=None, ex_size=None):
+ """
+ List available images
+
+ :keyword location: Location (region) used as filter
+ :type location: :class:`NodeLocation`
+
+ :keyword ex_size: Exclude images which are uncompatible with given size
+ :type ex_size: :class:`NodeImage`
+
+ :return: List of images
+ :rtype : ``list`` of :class:`NodeImage`
+ """
+ action = '%s/cloud/project/%s/image' % (API_ROOT, self.project_id)
+ params = {}
+ if location:
+ params['region'] = location.id
+ if ex_size:
+ params['flavorId'] = ex_size.id
+ response = self.connection.request(action, params=params)
+ return self._to_images(response.object)
+
+ def get_image(self, image_id):
+ action = '%s/cloud/project/%s/image/%s' % (
+ API_ROOT, self.project_id, image_id)
+ response = self.connection.request(action)
+ return self._to_image(response.object)
+
+ def list_locations(self):
+ action = '%s/cloud/project/%s/region' % (API_ROOT, self.project_id)
+ data = self.connection.request(action)
+ return self._to_locations(data.object)
+
+ def list_key_pairs(self, location=None):
+ """
+ List available SSH public keys.
+
+ :keyword location: Location (region) used as filter
+ :type location: :class:`NodeLocation`
+
+ :return: Public keys
+ :rtype: ``list``of :class:`KeyPair`
+ """
+ action = '%s/cloud/project/%s/sshkey' % (API_ROOT, self.project_id)
+ params = {}
+ if location:
+ params['region'] = location.id
+ response = self.connection.request(action, params=params)
+ return self._to_key_pairs(response.object)
+
+ def get_key_pair(self, name, location):
+ """
+ Get an individual SSH public key by its name and location.
+
+ :keyword name: SSH key name
+ :type name: str
+
+ :keyword location: Key's region
+ :type location: :class:`NodeLocation`
+
+ :return: Public key
+ :rtype: :class:`KeyPair`
+ """
+ # Keys are indexed with ID
+ keys = [key for key in self.list_key_pairs(location)
+ if key.name == name]
+ if not keys:
+ raise Exception("No key named '%s'" % name)
+ return keys[0]
+
+ def import_key_pair_from_string(self, name, key_material, location):
+ """
+ Import a new public key from string.
+
+ :param name: Key pair name.
+ :type name: ``str``
+
+ :param key_material: Public key material.
+ :type key_material: ``str``
+
+ :return: Imported key pair object.
+ :rtype: :class:`KeyPair`
+ """
+ action = '%s/cloud/project/%s/sshkey' % (API_ROOT, self.project_id)
+ data = {'name': name, 'publicKey': key_material, 'region': location.id}
+ response = self.connection.request(action, data=data, method='POST')
+ return self._to_key_pair(response.object)
+
+ def delete_key_pair(self, name, location):
+ """
+ Delete an existing key pair.
+
+ :param name: Key pair name.
+ :type name: ``str``
+
+ :keyword location: Key's region
+ :type location: :class:`NodeLocation`
+
+ :return: True of False based on success of Keypair deletion
+ :rtype: ``bool``
+ """
+ action = '%s/cloud/project/%s/sshkey/%s' % (
+ API_ROOT, self.project_id, name)
+ params = {'name': name, 'region': location.id}
+ self.connection.request(action, params=params, method='DELETE')
+ return True
+
+ def create_volume(self, size, location, name=None,
+ ex_volume_type='classic', ex_description=None):
+ """
+ Create a volume.
+
+ :param size: Size of volume to create (in GB).
+ :type size: ``int``
+
+ :param name: Name of volume to create
+ :type name: ``str``
+
+ :keyword location: Location to create the volume in
+ :type location: :class:`NodeLocation` or ``None``
+
+ :keyword ex_volume_type: ``'classic'`` or ``'high-speed'``
+ :type ex_volume_type: ``str``
+
+ :keyword ex_description: Optionnal description of volume
+ :type ex_description: str
+
+ :return: Storage Volume object
+ :rtype: :class:`StorageVolume`
+ """
+ action = '%s/cloud/project/%s/volume' % (API_ROOT, self.project_id)
+ data = {
+ 'region': location.id,
+ 'size': size,
+ 'type': ex_volume_type,
+ }
+ if name:
+ data['name'] = name
+ if ex_description:
+ data['description'] = ex_description
+ response = self.connection.request(action, data=data, method='POST')
+ return self._to_volume(response.object)
+
+ def destroy_volume(self, volume):
+ action = '%s/cloud/project/%s/volume/%s' % (
+ API_ROOT, self.project_id, volume.id)
+ self.connection.request(action, method='DELETE')
+ return True
+
+ def list_volumes(self, location=None):
+ """
+ Return a list of volumes.
+
+ :keyword location: Location use for filter
+ :type location: :class:`NodeLocation` or ``None``
+
+ :return: A list of volume objects.
+ :rtype: ``list`` of :class:`StorageVolume`
+ """
+ action = '%s/cloud/project/%s/volume' % (API_ROOT, self.project_id)
+ data = {}
+ if location:
+ data['region'] = location.id
+ response = self.connection.request(action, data=data)
+ return self._to_volumes(response.object)
+
+ def ex_get_volume(self, volume_id):
+ """
+ Return a Volume object based on a volume ID.
+
+ :param volume_id: The ID of the volume
+ :type volume_id: ``int``
+
+ :return: A StorageVolume object for the volume
+ :rtype: :class:`StorageVolume`
+ """
+ action = '%s/cloud/project/%s/volume/%s' % (
+ API_ROOT, self.project_id, volume_id)
+ response = self.connection.request(action)
+ return self._to_volume(response.object)
+
+ def attach_volume(self, node, volume, device=None):
+ """
+ Attach a volume to a node.
+
+ :param node: Node where to attach volume
+ :type node: :class:`Node`
+
+ :param volume: The ID of the volume
+ :type volume: :class:`StorageVolume`
+
+ :param device: Unsed parameter
+
+ :return: True or False representing operation successful
+ :rtype: ``bool``
+ """
+ action = '%s/cloud/project/%s/volume/%s/attach' % (
+ API_ROOT, self.project_id, volume.id)
+ data = {'instanceId': node.id, 'volumeId': volume.id}
+ self.connection.request(action, data=data, method='POST')
+ return True
+
+ def detach_volume(self, volume, ex_node=None):
+ """
+ Detach a volume to a node.
+
+ :param volume: The ID of the volume
+ :type volume: :class:`StorageVolume`
+
+ :param ex_node: Node to detach from (optionnal if volume is attached
+ to only one node)
+ :type ex_node: :class:`Node`
+
+ :return: True or False representing operation successful
+ :rtype: ``bool``
+
+ :raises: Exception: If ``ex_node`` is not provided and more than one
+ node is attached to the volume
+ """
+ action = '%s/cloud/project/%s/volume/%s/detach' % (
+ API_ROOT, self.project_id, volume.id)
+ if ex_node is None:
+ if len(volume.extra['attachedTo']) != 1:
+ err_msg = "Volume '%s' has more or less than one attached" \
+ "nodes, you must specify one."
+ raise Exception(err_msg)
+ ex_node = self.ex_get_node(volume.extra['attachedTo'][0])
+ data = {'instanceId': ex_node.id}
+ self.connection.request(action, data=data, method='POST')
+ return True
+
+ def _to_volume(self, obj):
+ extra = obj.copy()
+ extra.pop('id')
+ extra.pop('name')
+ extra.pop('size')
+ state = self.VOLUME_STATE_MAP.get(obj.pop('status', None),
+ StorageVolumeState.UNKNOWN)
+ return StorageVolume(id=obj['id'], name=obj['name'], size=obj['size'],
+ state=state, extra=extra, driver=self)
+
+ def _to_volumes(self, objs):
+ return [self._to_volume(obj) for obj in objs]
+
+ def _to_location(self, obj):
+ location = self.connection.LOCATIONS[obj]
+ return NodeLocation(driver=self, **location)
+
+ def _to_locations(self, objs):
+ return [self._to_location(obj) for obj in objs]
+
+ def _to_node(self, obj):
+ extra = obj.copy()
+ if 'ipAddresses' in extra:
+ public_ips = [ip['ip'] for ip in extra['ipAddresses']]
+ del extra['id']
+ del extra['name']
+ return Node(id=obj['id'], name=obj['name'],
+ state=self.NODE_STATE_MAP[obj['status']],
+ public_ips=public_ips, private_ips=[], driver=self,
+ extra=extra)
+
+ def _to_nodes(self, objs):
+ return [self._to_node(obj) for obj in objs]
+
+ def _to_size(self, obj):
+ extra = {'vcpus': obj['vcpus'], 'type': obj['type'],
+ 'region': obj['region']}
+ return NodeSize(id=obj['id'], name=obj['name'], ram=obj['ram'],
+ disk=obj['disk'], bandwidth=None, price=None,
+ driver=self, extra=extra)
+
+ def _to_sizes(self, objs):
+ return [self._to_size(obj) for obj in objs]
+
+ def _to_image(self, obj):
+ extra = {'region': obj['region'], 'visibility': obj['visibility']}
+ return NodeImage(id=obj['id'], name=obj['name'], driver=self,
+ extra=extra)
+
+ def _to_images(self, objs):
+ return [self._to_image(obj) for obj in objs]
+
+ def _to_key_pair(self, obj):
+ extra = {'regions': obj['regions'], 'id': obj['id']}
+ return OpenStackKeyPair(name=obj['name'], public_key=obj['publicKey'],
+ driver=self, fingerprint=None, extra=extra)
+
+ def _to_key_pairs(self, objs):
+ return [self._to_key_pair(obj) for obj in objs]
+
+ def _ex_connection_class_kwargs(self):
+ return {'ex_consumer_key': self.consumer_key}
http://git-wip-us.apache.org/repos/asf/libcloud/blob/d8100961/libcloud/compute/drivers/runabove.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/drivers/runabove.py b/libcloud/compute/drivers/runabove.py
deleted file mode 100644
index 72a45c6..0000000
--- a/libcloud/compute/drivers/runabove.py
+++ /dev/null
@@ -1,453 +0,0 @@
-# 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.
-"""
-RunAbove driver
-"""
-from libcloud.common.runabove import API_ROOT, RunAboveConnection
-from libcloud.compute.base import NodeDriver, NodeSize, Node, NodeLocation
-from libcloud.compute.base import NodeImage, StorageVolume
-from libcloud.compute.types import Provider, StorageVolumeState
-from libcloud.compute.drivers.openstack import OpenStackNodeDriver
-from libcloud.compute.drivers.openstack import OpenStackKeyPair
-
-
-class RunAboveNodeDriver(NodeDriver):
- """
- Libcloud driver for the RunAbove API
-
- For more information on the RunAbove API, read the official reference:
-
- https://api.runabove.com/console/
- """
- type = Provider.RUNABOVE
- name = "RunAbove"
- website = 'https://www.runabove.com/'
- connectionCls = RunAboveConnection
- features = {'create_node': ['ssh_key']}
- api_name = 'runabove'
-
- NODE_STATE_MAP = OpenStackNodeDriver.NODE_STATE_MAP
- VOLUME_STATE_MAP = OpenStackNodeDriver.VOLUME_STATE_MAP
-
- def __init__(self, key, secret, ex_consumer_key=None):
- """
- Instantiate the driver with the given API credentials.
-
- :param key: Your application key (required)
- :type key: ``str``
-
- :param secret: Your application secret (required)
- :type secret: ``str``
-
- :param ex_consumer_key: Your consumer key (required)
- :type ex_consumer_key: ``str``
-
- :rtype: ``None``
- """
- self.datacenter = None
- self.consumer_key = ex_consumer_key
- NodeDriver.__init__(self, key, secret, ex_consumer_key=ex_consumer_key)
-
- def list_nodes(self, location=None):
- """
- List all nodes.
-
- :keyword location: Location (region) used as filter
- :type location: :class:`NodeLocation`
-
- :return: List of node objects
- :rtype: ``list`` of :class:`Node`
- """
- action = API_ROOT + '/instance'
- data = {}
- if location:
- data['region'] = location.id
- response = self.connection.request(action, data=data)
- return self._to_nodes(response.object)
-
- def ex_get_node(self, node_id):
- """
- Get a individual node.
-
- :keyword node_id: Node's ID
- :type node_id: ``str``
-
- :return: Created node
- :rtype : :class:`Node`
- """
- action = API_ROOT + '/instance/' + node_id
- response = self.connection.request(action, method='GET')
- return self._to_node(response.object)
-
- def create_node(self, name, image, size, location, ex_keyname=None):
- """
- Create a new node
-
- :keyword name: Name of created node
- :type name: ``str``
-
- :keyword image: Image used for node
- :type image: :class:`NodeImage`
-
- :keyword size: Size (flavor) used for node
- :type size: :class:`NodeSize`
-
- :keyword location: Location (region) where to create node
- :type location: :class:`NodeLocation`
-
- :keyword ex_keyname: Name of SSH key used
- :type ex_keyname: ``str``
-
- :return: Created node
- :rtype : :class:`Node`
- """
- action = API_ROOT + '/instance'
- data = {
- 'name': name,
- 'imageId': image.id,
- 'flavorId': size.id,
- 'region': location.id,
- }
- if ex_keyname:
- data['sshKeyName'] = ex_keyname
- response = self.connection.request(action, data=data, method='POST')
- return self._to_node(response.object)
-
- def destroy_node(self, node):
- action = API_ROOT + '/instance/' + node.id
- self.connection.request(action, method='DELETE')
- return True
-
- def list_sizes(self, location=None):
- action = API_ROOT + '/flavor'
- data = {}
- if location:
- data['region'] = location.id
- response = self.connection.request(action, data=data)
- return self._to_sizes(response.object)
-
- def ex_get_size(self, size_id):
- """
- Get an individual size (flavor).
-
- :keyword size_id: Size's ID
- :type size_id: ``str``
-
- :return: Size
- :rtype: :class:`NodeSize`
- """
- action = API_ROOT + '/flavor/' + size_id
- response = self.connection.request(action)
- return self._to_size(response.object)
-
- def list_images(self, location=None, ex_size=None):
- """
- List available images
-
- :keyword location: Location (region) used as filter
- :type location: :class:`NodeLocation`
-
- :keyword ex_size: Exclude images which are uncompatible with given size
- :type ex_size: :class:`NodeImage`
-
- :return: List of images
- :rtype : ``list`` of :class:`NodeImage`
- """
- action = API_ROOT + '/image'
- data = {}
- if location:
- data['region'] = location.id
- if ex_size:
- data['flavorId'] = ex_size.id
- response = self.connection.request(action, data=data)
- return self._to_images(response.object)
-
- def get_image(self, image_id):
- action = API_ROOT + '/image/' + image_id
- response = self.connection.request(action)
- return self._to_image(response.object)
-
- def list_locations(self):
- action = API_ROOT + '/region'
- data = self.connection.request(action)
- return self._to_locations(data.object)
-
- def list_key_pairs(self, location=None):
- """
- List available SSH public keys.
-
- :keyword location: Location (region) used as filter
- :type location: :class:`NodeLocation`
-
- :return: Public keys
- :rtype: ``list``of :class:`KeyPair`
- """
- action = API_ROOT + '/ssh'
- data = {}
- if location:
- data['region'] = location.id
- response = self.connection.request(action, data=data)
- return self._to_key_pairs(response.object)
-
- def get_key_pair(self, name, location):
- """
- Get an individual SSH public key by its name and location.
-
- :keyword name: SSH key name
- :type name: str
-
- :keyword location: Key's region
- :type location: :class:`NodeLocation`
-
- :return: Public key
- :rtype: :class:`KeyPair`
- """
- action = API_ROOT + '/ssh/' + name
- data = {'region': location.id}
- response = self.connection.request(action, data=data)
- return self._to_key_pair(response.object)
-
- def import_key_pair_from_string(self, name, key_material, location):
- """
- Import a new public key from string.
-
- :param name: Key pair name.
- :type name: ``str``
-
- :param key_material: Public key material.
- :type key_material: ``str``
-
- :return: Imported key pair object.
- :rtype: :class:`KeyPair`
- """
- action = API_ROOT + '/ssh'
- data = {'name': name, 'publicKey': key_material, 'region': location.id}
- response = self.connection.request(action, data=data, method='POST')
- return self._to_key_pair(response.object)
-
- def delete_key_pair(self, name, location):
- """
- Delete an existing key pair.
-
- :param name: Key pair name.
- :type name: ``str``
-
- :keyword location: Key's region
- :type location: :class:`NodeLocation`
-
- :return: True of False based on success of Keypair deletion
- :rtype: ``bool``
- """
- action = API_ROOT + '/ssh/' + name
- data = {'name': name, 'region': location.id}
- self.connection.request(action, data=data, method='DELETE')
- return True
-
- def create_volume(self, size, location, name=None,
- ex_volume_type='classic', ex_description=None):
- """
- Create a volume.
-
- :param size: Size of volume to create (in GB).
- :type size: ``int``
-
- :param name: Name of volume to create
- :type name: ``str``
-
- :keyword location: Location to create the volume in
- :type location: :class:`NodeLocation` or ``None``
-
- :keyword ex_volume_type: ``'classic'`` or ``'high-speed'``
- :type ex_volume_type: ``str``
-
- :keyword ex_description: Optionnal description of volume
- :type ex_description: str
-
- :return: Storage Volume object
- :rtype: :class:`StorageVolume`
- """
- action = API_ROOT + '/volume'
- data = {
- 'region': location.id,
- 'size': str(size),
- 'type': ex_volume_type,
- }
- if name:
- data['name'] = name
- if ex_description:
- data['description'] = ex_description
- response = self.connection.request(action, data=data, method='POST')
- return self._to_volume(response.object)
-
- def destroy_volume(self, volume):
- action = API_ROOT + '/volume/' + volume.id
- self.connection.request(action, method='DELETE')
- return True
-
- def list_volumes(self, location=None):
- """
- Return a list of volumes.
-
- :keyword location: Location use for filter
- :type location: :class:`NodeLocation` or ``None``
-
- :return: A list of volume objects.
- :rtype: ``list`` of :class:`StorageVolume`
- """
- action = API_ROOT + '/volume'
- data = {}
- if location:
- data['region'] = location.id
- response = self.connection.request(action, data=data)
- return self._to_volumes(response.object)
-
- def ex_get_volume(self, volume_id):
- """
- Return a Volume object based on a volume ID.
-
- :param volume_id: The ID of the volume
- :type volume_id: ``int``
-
- :return: A StorageVolume object for the volume
- :rtype: :class:`StorageVolume`
- """
- action = API_ROOT + '/volume/' + volume_id
- response = self.connection.request(action)
- return self._to_volume(response.object)
-
- def attach_volume(self, node, volume, device=None):
- """
- Attach a volume to a node.
-
- :param node: Node where to attach volume
- :type node: :class:`Node`
-
- :param volume: The ID of the volume
- :type volume: :class:`StorageVolume`
-
- :param device: Unsed parameter
-
- :return: True or False representing operation successful
- :rtype: ``bool``
- """
- action = '%s/volume/%s/attach' % (API_ROOT, volume.id)
- data = {'instanceId': node.id}
- self.connection.request(action, data=data, method='POST')
- return True
-
- def detach_volume(self, volume, ex_node=None):
- """
- Detach a volume to a node.
-
- :param volume: The ID of the volume
- :type volume: :class:`StorageVolume`
-
- :param ex_node: Node to detach from (optionnal if volume is attached
- to only one node)
- :type ex_node: :class:`Node`
-
- :return: True or False representing operation successful
- :rtype: ``bool``
-
- :raises: Exception: If ``ex_node`` is not provided and more than one
- node is attached to the volume
- """
- action = '%s/volume/%s/detach' % (API_ROOT, volume.id)
- if ex_node is None:
- if len(volume.extra['attachedTo']) != 1:
- err_msg = "Volume '%s' has more or less than one attached \
- nodes, you must specify one."
- raise Exception(err_msg)
- ex_node = self.ex_get_node(volume.extra['attachedTo'][0])
- data = {'instanceId': ex_node.id}
- self.connection.request(action, data=data, method='POST')
- return True
-
- def _to_volume(self, obj):
- extra = obj.copy()
- extra.pop('id')
- extra.pop('name')
- extra.pop('size')
- state = self.VOLUME_STATE_MAP.get(obj.pop('status', None),
- StorageVolumeState.UNKNOWN)
- return StorageVolume(id=obj['id'], name=obj['name'], size=obj['size'],
- state=state, extra=extra, driver=self)
-
- def _to_volumes(self, objs):
- return [self._to_volume(obj) for obj in objs]
-
- def _to_location(self, obj):
- location = self.connection.LOCATIONS[obj]
- return NodeLocation(driver=self, **location)
-
- def _to_locations(self, objs):
- return [self._to_location(obj) for obj in objs]
-
- def _to_node(self, obj):
- extra = obj.copy()
- if 'flavorId' in extra:
- public_ips = [obj.pop('ip')]
- else:
- ip = extra.pop('ipv4')
- public_ips = [ip] if ip else []
- del extra['instanceId']
- del extra['name']
- return Node(id=obj['instanceId'], name=obj['name'],
- state=self.NODE_STATE_MAP[obj['status']],
- public_ips=public_ips, private_ips=[], driver=self,
- extra=extra)
-
- def _to_nodes(self, objs):
- return [self._to_node(obj) for obj in objs]
-
- def _to_size(self, obj):
- extra = {'vcpus': obj['vcpus'], 'type': obj['type'],
- 'region': obj['region']}
- return NodeSize(id=obj['id'], name=obj['name'], ram=obj['ram'],
- disk=obj['disk'], bandwidth=None, price=None,
- driver=self, extra=extra)
-
- def _to_sizes(self, objs):
- return [self._to_size(obj) for obj in objs]
-
- def _to_image(self, obj):
- extra = {'region': obj['region'], 'visibility': obj['visibility'],
- 'deprecated': obj['deprecated']}
- return NodeImage(id=obj['id'], name=obj['name'], driver=self,
- extra=extra)
-
- def _to_images(self, objs):
- return [self._to_image(obj) for obj in objs]
-
- def _to_key_pair(self, obj):
- extra = {'region': obj['region']}
- return OpenStackKeyPair(name=obj['name'], public_key=obj['publicKey'],
- driver=self, fingerprint=obj['fingerPrint'],
- extra=extra)
-
- def _to_key_pairs(self, objs):
- return [self._to_key_pair(obj) for obj in objs]
-
- def _ex_connection_class_kwargs(self):
- return {'ex_consumer_key': self.consumer_key}
-
- def _add_required_headers(self, headers, method, action, data, timestamp):
- timestamp = self.connection.get_timestamp()
- signature = self.connection.make_signature(method, action, data,
- str(timestamp))
- headers.update({
- 'X-Ra-Timestamp': timestamp,
- 'X-Ra-Signature': signature
- })
http://git-wip-us.apache.org/repos/asf/libcloud/blob/d8100961/libcloud/compute/providers.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/providers.py b/libcloud/compute/providers.py
index 568c886..459335e 100644
--- a/libcloud/compute/providers.py
+++ b/libcloud/compute/providers.py
@@ -124,8 +124,8 @@ DRIVERS = {
('libcloud.compute.drivers.packet', 'PacketNodeDriver'),
Provider.ONAPP:
('libcloud.compute.drivers.onapp', 'OnAppNodeDriver'),
- Provider.RUNABOVE:
- ('libcloud.compute.drivers.runabove', 'RunAboveNodeDriver'),
+ Provider.OVH:
+ ('libcloud.compute.drivers.ovh', 'OvhNodeDriver'),
Provider.INTERNETSOLUTIONS:
('libcloud.compute.drivers.internetsolutions',
'InternetSolutionsNodeDriver'),
http://git-wip-us.apache.org/repos/asf/libcloud/blob/d8100961/libcloud/compute/types.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/types.py b/libcloud/compute/types.py
index 740b688..e66f1e7 100644
--- a/libcloud/compute/types.py
+++ b/libcloud/compute/types.py
@@ -144,12 +144,12 @@ class Provider(Type):
OPSOURCE = 'opsource'
OUTSCALE_INC = 'outscale_inc'
OUTSCALE_SAS = 'outscale_sas'
+ OVH = 'ovh'
PACKET = 'packet'
PROFIT_BRICKS = 'profitbricks'
RACKSPACE = 'rackspace'
RACKSPACE_FIRST_GEN = 'rackspace_first_gen'
RIMUHOSTING = 'rimuhosting'
- RUNABOVE = 'runabove'
SERVERLOVE = 'serverlove'
SKALICLOUD = 'skalicloud'
SOFTLAYER = 'softlayer'
http://git-wip-us.apache.org/repos/asf/libcloud/blob/d8100961/libcloud/test/common/test_ovh.py
----------------------------------------------------------------------
diff --git a/libcloud/test/common/test_ovh.py b/libcloud/test/common/test_ovh.py
new file mode 100644
index 0000000..946f907
--- /dev/null
+++ b/libcloud/test/common/test_ovh.py
@@ -0,0 +1,29 @@
+# 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.
+
+import re
+from libcloud.test import MockHttp
+
+FORMAT_URL = re.compile(r'[./-]')
+
+
+class BaseOvhMockHttp(MockHttp):
+
+ def _get_method_name(self, type, use_param, qs, path):
+ return "_json"
+
+ def _json(self, method, url, body, headers):
+ meth_name = '_json%s_%s' % (FORMAT_URL.sub('_', url), method.lower())
+ return getattr(self, meth_name)(method, url, body, headers)