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 <montheanth...@hotmail.com> Authored: Wed Oct 5 14:44:38 2016 -0400 Committer: Anthony Shaw <anthonys...@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)