diff --git a/docs/_static/images/provider_logos/runabove.png b/docs/_static/images/provider_logos/runabove.png new file mode 100644 index 0000000000..c249888929 Binary files /dev/null and b/docs/_static/images/provider_logos/runabove.png differ diff --git a/docs/compute/drivers/runabove.rst b/docs/compute/drivers/runabove.rst new file mode 100644 index 0000000000..adf1b9d309 --- /dev/null +++ b/docs/compute/drivers/runabove.rst @@ -0,0 +1,72 @@ +Cloudwatt Compute Driver Documentation +====================================== + +`RunAbove`_ is a public cloud offer created by OVH Group with datacenters +in North America and Europe. + +.. figure:: /_static/images/provider_logos/runabove.png + :align: center + :width: 300 + :target: https://www.runabove.com/index.xml + +RunAbove driver uses the OVH/RunAbove API so for more information about +that, please refer to `RunAbove knowledge base`_ page and `API console`_. + +Instantiating a driver +---------------------- + +When you instantiate a driver you need to pass the following arguments to the +driver constructor: + +* ``user_id`` - Application key +* ``secret`` - Application secret +* ``ex_consumer_key`` - Consumer key + +For get application key and secret, you must first register an application +at https://api.runabove.com/createApp/. Next step, create a consumer key with +following command: :: + + curl -X POST \ + -H 'X-Ra-Application: youApplicationKey' \ + -H 'Content-Type: application/json' \ + -d '{ + "accessRules": + [ + {"method":"GET","path":"/*"}, + {"method":"POST","path":"/*"}, + {"method":"DELETE","path":"/*"}, + {"method":"PUT","path":"/*"}, + ], + "redirection":"http://runabove.com" + }' \ + "https://api.runabove.com/1.0/auth/credential" + +This will answer a JSON like below with inside your Consumer Key and +``validationUrl``. Follow this link for valid your key. :: + + { + "validationUrl":"https://api.runabove.com/login/?credentialToken=fIDK6KCVHfEMuSTP3LV84D3CsHTq4T3BhOrmEEdd2hQ0CNcfVgGVWZRqIlolDJ3W", + "consumerKey":"y7epYeHCIqoO17BzBgxluvB4XLedpba9", + "state":"pendingValidation" + } + +Now you have and can use you credentials with Libcloud. + +Examples +-------- + +Create instance +~~~~~~~~~~~~~~~ + +.. literalinclude:: /examples/compute/runabove/create_node.py + +API Docs +-------- + +.. autoclass:: libcloud.compute.drivers.runabove.RunAboveNodeDriver + :members: + :inherited-members: + +.. _`Runabove`: https://www.runabove.com/index.xml +.. _`RunAbove knowledge base`: https://community.runabove.com/kb/ +.. _`API console`: https://api.runabove.com/console/#/ diff --git a/docs/examples/compute/runabove/create_node.py b/docs/examples/compute/runabove/create_node.py new file mode 100644 index 0000000000..7137ff4e9c --- /dev/null +++ b/docs/examples/compute/runabove/create_node.py @@ -0,0 +1,12 @@ +from libcloud.compute.types import Provider +from libcloud.compute.providers import get_driver + +RunAbove = get_driver(Provider.RUNABOVE) +driver = RunAbove('yourAppKey', 'yourAppSecret', 'YourConsumerKey') + +image = [i for i in driver.list_images() if 'Debian 8' == i.name][0] +size = [s for s in driver.list_sizes() if s.name == 'ra.s'][0] +location = [l for l in driver.list_locations() if l.id == 'SBG-1'][0] + +node = driver.create_node(name='yournode', size=size, image=image, + location=location) diff --git a/libcloud/common/runabove.py b/libcloud/common/runabove.py new file mode 100644 index 0000000000..6345980042 --- /dev/null +++ b/libcloud/common/runabove.py @@ -0,0 +1,134 @@ +# 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.common.base import ConnectionUserAndKey, JsonResponse +from libcloud.httplib_ssl import LibcloudHTTPSConnection + +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 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 = JsonResponse + 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}' for valid it. After instantiate " \ + "your driver with \"ex_consumer_key='{consumerKey}'\"."\ + .format(**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().read() + json_response = json.loads(response) + httpcon.close() + return json_response + + def get_timestamp(self): + if not self._timedelta: + action = API_ROOT + '/auth/time' + response = self.connection.request('GET', action, headers={}) + timestamp = int(response) + self._time_delta = 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) diff --git a/libcloud/compute/drivers/runabove.py b/libcloud/compute/drivers/runabove.py new file mode 100644 index 0000000000..5e5252b883 --- /dev/null +++ b/libcloud/compute/drivers/runabove.py @@ -0,0 +1,295 @@ +# 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. + +from libcloud.common.runabove import API_ROOT, RunAboveConnection +from libcloud.compute.base import NodeDriver, NodeSize, Node, NodeLocation +from libcloud.compute.base import NodeImage +from libcloud.compute.types import Provider +from libcloud.compute.drivers.openstack import OpenStackNodeDriver +from libcloud.compute.drivers.openstack import OpenStackKeyPair + + +class RunAboveNodeDriver(NodeDriver): + """libcloud driver for the RunAbove API + + Rough mapping of which is which: + + list_nodes linode.list + reboot_node linode.reboot + destroy_node linode.delete + create_node linode.create, linode.update, + linode.disk.createfromdistribution, + linode.disk.create, linode.config.create, + linode.ip.addprivate, linode.boot + list_sizes avail.linodeplans + list_images avail.distributions + list_locations avail.datacenters + list_volumes linode.disk.list + destroy_volume linode.disk.delete + + For more information on the Linode API, be sure to read the reference: + + http://www.linode.com/api/ + """ + 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 + + def __init__(self, key, secret, ex_consumer_key=None): + """Instantiate the driver with the given API key + + :param key: the API key to use (required) + :type 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 _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 + }) + + def list_nodes(self, location=None): + """ + List all Linodes that the API key can access + + This call will return all Linodes that the API key in use has access + to. + If a node is in this list, rebooting will work; however, creation and + destruction are a separate grant. + + :return: List of node objects that the API key can access + :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): + action = API_ROOT + '/instance/' + node_id + response = self.connection.request(action, method='GET') + return self._to_node(response.object) + + def reboot_node(self, node): + raise NotImplementedError( + "reboot_node not implemented for this driver") + + def create_node(self, **kwargs): + action = API_ROOT + '/instance' + data = { + 'name': kwargs["name"], + 'imageId': kwargs["image"].id, + 'flavorId': kwargs["size"].id, + 'region': kwargs["location"].id, + } + if kwargs.get('ex_keyname'): + data['sshKeyName'] = kwargs['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): + """ + List available RunAbove flavors. + + :keyword location: the facility to retrieve plans in + :type location: :class:`NodeLocation` + + :rtype: ``list`` of :class:`NodeSize` + """ + 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): + action = API_ROOT + '/flavor/' + size_id + response = self.connection.request(action) + return self._to_size(response.object) + + def list_images(self, location=None, size=None): + """ + List available Linux distributions + + Retrieve all Linux distributions that can be deployed to a Linode. + + :rtype: ``list`` of :class:`NodeImage` + """ + action = API_ROOT + '/image' + data = {} + if location: + data['region'] = location.id + if size: + data['flavorId'] = 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): + """ + List available facilities for deployment + + Retrieve all facilities that a Linode can be deployed in. + + :rtype: ``list`` of :class:`NodeLocation` + """ + action = API_ROOT + '/region' + data = self.connection.request(action) + return self._to_locations(data.object) + + def list_key_pairs(self, location=None): + 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): + 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. + + :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 key_pair: Key pair object or ID. + :type key_pair: :class.KeyPair` or ``int`` + + :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, name): + raise NotImplementedError( + "create_volume not implemented for this driver") + + def destroy_volume(self, volume): + raise NotImplementedError( + "destroy_volume not implemented for this driver") + + def ex_list_volumes(self, node, disk_id=None): + raise NotImplementedError( + "list_volumes not implemented for this driver") + + def _to_volume(self, obj): + pass + + 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] diff --git a/libcloud/compute/providers.py b/libcloud/compute/providers.py index 25687c342e..8ef06bc8d6 100644 --- a/libcloud/compute/providers.py +++ b/libcloud/compute/providers.py @@ -163,6 +163,8 @@ ('libcloud.compute.drivers.cloudwatt', 'CloudwattNodeDriver'), Provider.PACKET: ('libcloud.compute.drivers.packet', 'PacketNodeDriver'), + Provider.RUNABOVE: + ('libcloud.compute.drivers.runabove', 'RunAboveNodeDriver'), # Deprecated Provider.CLOUDSIGMA_US: diff --git a/libcloud/compute/types.py b/libcloud/compute/types.py index f0455466b9..e3303ce167 100644 --- a/libcloud/compute/types.py +++ b/libcloud/compute/types.py @@ -134,6 +134,7 @@ class Provider(object): AURORACOMPUTE = 'aurora_compute' CLOUDWATT = 'cloudwatt' PACKET = 'packet' + RUNABOVE = 'runabove' # OpenStack based providers HPCLOUD = 'hpcloud' diff --git a/libcloud/test/common/test_runabove.py b/libcloud/test/common/test_runabove.py new file mode 100644 index 0000000000..aead6c767a --- /dev/null +++ b/libcloud/test/common/test_runabove.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 BaseRunAboveMockHttp(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) diff --git a/libcloud/test/compute/fixtures/runabove/auth_time_get.json b/libcloud/test/compute/fixtures/runabove/auth_time_get.json new file mode 100644 index 0000000000..f84eeec940 --- /dev/null +++ b/libcloud/test/compute/fixtures/runabove/auth_time_get.json @@ -0,0 +1 @@ +1437075564 diff --git a/libcloud/test/compute/fixtures/runabove/flavor_get.json b/libcloud/test/compute/fixtures/runabove/flavor_get.json new file mode 100644 index 0000000000..19ed7858a6 --- /dev/null +++ b/libcloud/test/compute/fixtures/runabove/flavor_get.json @@ -0,0 +1 @@ +[{"id":"foo-id","disk":80,"name":"ra.intel.ha.l","ram":8192,"vcpus":2,"region":"SBG-1","type":"ra.s"},{"id":"bar-id","disk":80,"name":"ra.intel.ha.l","ram":8192,"vcpus":2,"region":"SBG-1","type":"ra.s"}] diff --git a/libcloud/test/compute/fixtures/runabove/flavor_get_detail.json b/libcloud/test/compute/fixtures/runabove/flavor_get_detail.json new file mode 100644 index 0000000000..96958628f1 --- /dev/null +++ b/libcloud/test/compute/fixtures/runabove/flavor_get_detail.json @@ -0,0 +1 @@ +{"id":"foo-id","disk":80,"name":"ra.intel.ha.l","ram":8192,"vcpus":2,"region":"SBG-1","type":"ra.s"} diff --git a/libcloud/test/compute/fixtures/runabove/image_get.json b/libcloud/test/compute/fixtures/runabove/image_get.json new file mode 100644 index 0000000000..aa552d98ed --- /dev/null +++ b/libcloud/test/compute/fixtures/runabove/image_get.json @@ -0,0 +1 @@ +[{"id":"foo-id","name":"Debian 8","region":"SBG-1","visibility":"public","deprecated":false},{"id":"bar-id","name":"CentOs","region":"SBG-1","visibility":"public","deprecated":false}] diff --git a/libcloud/test/compute/fixtures/runabove/image_get_detail.json b/libcloud/test/compute/fixtures/runabove/image_get_detail.json new file mode 100644 index 0000000000..d398bac8c0 --- /dev/null +++ b/libcloud/test/compute/fixtures/runabove/image_get_detail.json @@ -0,0 +1,2 @@ +{"id":"foo-id","name":"Debian 8","region":"SBG-1","visibility":"public","deprecated":false} + diff --git a/libcloud/test/compute/fixtures/runabove/instance_get.json b/libcloud/test/compute/fixtures/runabove/instance_get.json new file mode 100644 index 0000000000..16f609e537 --- /dev/null +++ b/libcloud/test/compute/fixtures/runabove/instance_get.json @@ -0,0 +1 @@ +[{"instanceId":"foo","name":"test_vm","ip":"92.0.0.1","flavorId":"551dc104-4174-495a-af34-4aafe75f22ca","imageId":"1f3b49ad-3566-4838-93f8-b657a36b870f","keyName":"mykey","status":"ACTIVE","created":"2015-05-29T11:20:48Z","region":"SBG-1"}, {"instanceId":"bar","name":"test_vm2","ip":"92.0.0.2","flavorId":"551dc104-4174-495a-af34-4aafe75f22ca","imageId":"1f3b49ad-3566-4838-93f8-b657a36b870f","keyName":"mykey","status":"ACTIVE","created":"2015-05-29T11:40:48Z","region":"SBG-1"}] diff --git a/libcloud/test/compute/fixtures/runabove/instance_get_detail.json b/libcloud/test/compute/fixtures/runabove/instance_get_detail.json new file mode 100644 index 0000000000..9f8ab5b8f7 --- /dev/null +++ b/libcloud/test/compute/fixtures/runabove/instance_get_detail.json @@ -0,0 +1 @@ +{"instanceId":"c8de03b5-16cb-4eed-aa57-cfb1800faf00","name":"testvm","ipv4":"92.0.0.1","created":"2015-05-29T11:20:48Z","status":"ACTIVE","flavor":{"id":"551dc104-4174-495a-af34-4aafe75f22ca","disk":30,"name":"ra.intel.sb.l","ram":4096,"vcpus":1,"region":"SBG-1","type":"ra.sb"},"image":{"id":"1f3b49ad-3566-4838-93f8-b657a36b870f","name":"Debian 8","region":"SBG-1","visibility":"","deprecated":false},"sshKey":{"publicKey":"ssh-dss AAAAB3NzaC1kc3MAAACBALDA8LBVhtEDgns2AFtF4hO7BckyFWUyuwCUy1Frh8UZSkrhRtfNcrbir/qcoABCDEF8k1+qdqfq6RB/wYjXAqtoVABCDEFgh5D0aWKSveYH9F/zuwyH+CGcu+fBLo/q6JAqnXziFluK9jcnu1vam3FvTD9nAsx5UYSvdTo+axlbAAAAFQCFBEBKWuCabLfUQpAoiMQSNpCsFQAAAIAVHwW6+LyDPyS/o6aV6icX0Gw8K9AG4zjj+OZWd0HxJP/ABCDEFYrY63WC2vUAbmR0hrmoa3dS1Fw6ABCDEFs41z9xzLydW8jLiopI655LrDtnABCDEFYtr8idKq3j3IvmBfynvzhmb0r101agiMABCDEFjGQbRAbL42pej5ee5gAAAIAbdNorJ2iFrczABCDEF5Vjap+gqFGUYMwaHlta26WWf+ZHQdX/Kqsc4nABCDEF4jjoj7doiBjN7aRAr9f3JEDUboTatpdOzanU9gSrryCBykz5RK016ABCDEFxdmaar/U47qEKa4TYwmYyA5UH+KmbYmJExC5ovXlI25oAbc1eQg== user@host","name":"mykey","fingerPrint":"75:19:bf:48:dc:31:5b:91:b2:e5:ba:3b:f0:00:ba:ar","region":"SBG-1"},"region":"SBG-1"} diff --git a/libcloud/test/compute/fixtures/runabove/instance_post.json b/libcloud/test/compute/fixtures/runabove/instance_post.json new file mode 100644 index 0000000000..15a4075aa4 --- /dev/null +++ b/libcloud/test/compute/fixtures/runabove/instance_post.json @@ -0,0 +1 @@ +{"instanceId":"df4e55cb-90e6-4f20-a604-7084f311f498","name":"testvm","ipv4":"","created":"2015-07-16T22:35:55Z","status":"BUILD","flavor":{"id":"551dc104-4174-495a-af34-4aafe75f22ca","disk":30,"name":"ra.intel.sb.l","ram":4096,"vcpus":1,"region":"SBG-1","type":"ra.sb"},"image":{"id":"1f3b49ad-3566-4838-93f8-b657a36b870f","name":"Debian 8","region":"SBG-1","visibility":"","deprecated":false},"sshKey":{"publicKey":"ssh-dss AAAAB3NzaC1kc3MAAACBALDA8LBVhtEDgns2AFtF4hO7BckyFWUyuwCUy1Frh8UZSkrhRtfNcrbir/qcoql5psB8k1+qdqfq6RB/wYjXAqtoVNF+ydCgh5D0aWKSveYH9F/zuwyH+CGcu+fBLo/q6JAqnXziFluK9jcnu1vam3FvTD9nAsx5UYSvdTo+axlbAAAAFQCFBEBKWuCabLfUQpAoiMQSNpCsFQAAAIAVHwW6+LyDPyS/o6aV6icX0Gw8K9AG4zjj+OZWd0HxJP/Rn7ND1YrY63WC2vUAbmR0hrmoa3dS1Fw6553n+Ks41z9xzLydW8jLiopI655LrDtnMEzDpIYtr8idKq3j3IvmBfynvzhmb0r101agiMdEKsUJjGQbRAbL42pej5ee5gAAAIAbdNorJ2iFrcz8OyYi25Vjap+gqFGUYMwaHlta26WWf+ZHQdX/Kqsc4n4twkb94jjoj7doiBjN7aRAr9f3JEDUboTatpdOzanU9gSrryCBykz5RK016wwOU8fxdmaar/U47qEKa4TYwmYyA5UH+KmbYmJExC5ovXlI25oAbc1eQg== amonthe@amonthe","name":"mykey","fingerPrint":"75:19:bf:48:dc:31:5b:91:b2:e5:ba:3b:fa:dc:ef:a7","region":"BHS-1"},"region":"SBG-1"} diff --git a/libcloud/test/compute/fixtures/runabove/region_get.json b/libcloud/test/compute/fixtures/runabove/region_get.json new file mode 100644 index 0000000000..9ceda7fa6a --- /dev/null +++ b/libcloud/test/compute/fixtures/runabove/region_get.json @@ -0,0 +1 @@ +["SBG-1","BHS-1"] diff --git a/libcloud/test/compute/fixtures/runabove/ssh_get.json b/libcloud/test/compute/fixtures/runabove/ssh_get.json new file mode 100644 index 0000000000..47dccfc3ce --- /dev/null +++ b/libcloud/test/compute/fixtures/runabove/ssh_get.json @@ -0,0 +1,2 @@ + +[{"publicKey":"ssh-dss AAAAB3NzaC1kc3MAAACBALDA8LBVhtEDgns2AFtF4hO7BckyFWUyuwCUy1Frh8UZSkrhRtfNcrbir/qcoql5psB8k1+qdqfq6RB/wYjXAqtoVNF+ABCDE5D0aWKSveYH9F/zuwyABCDEu+fBLo/q6JAqnXziFluK9jcnu1vam3FvTABCDEx5UYSvdTo+axlbAAAAFQCFBEABCDEabLfUQpAoiMQSNpCsFQAAAIAVHwW6ABCDEyS/o6aV6icX0Gw8K9AG4zjj+OZWd0HxJP/RnABCDErY63WC2vUAbmR0hrmoa3dS1Fw6553n+Ks41z9xzLydWABCDEpI655LrDtnMEzDpIYtr8idKq3j3IvmBfynvzhmb0r101agiMdEKsUJjGQbRAbL42ABCDEe5gAAAIAbdNorJ2iFrcz8OyYi25Vjap+gqFGUYMwaHlta26WWf+ZHQABCDEsc4n4twkb94jjoj7doiBjN7aRABCDEJEDUboTatpdOzanU9gSrryCBykz5RK016wwOU8fxdmaar/U47qEKa4TYwmYyA5UH+KmbYmJExC5ovXlI25oAbc1eQg== user@host","name":"mykey","fingerPrint":"75:19:bf:48:dc:31:5b:91:b2:e5:ba:3b:f0:00:ba:ar","region":"SGB-1"}] diff --git a/libcloud/test/compute/fixtures/runabove/ssh_get_detail.json b/libcloud/test/compute/fixtures/runabove/ssh_get_detail.json new file mode 100644 index 0000000000..7373992533 --- /dev/null +++ b/libcloud/test/compute/fixtures/runabove/ssh_get_detail.json @@ -0,0 +1 @@ +{"publicKey":"ssh-dss AAAAB3NzaC1kc3MAAACBALDA8LBVhtEDgns2AFtF4hO7BckyFWUyuwCUy1Frh8UZSkrhRtfNcrbir/qcoql5psB8k1+qdqfq6RB/wYjXAqtoVNF+ABCDE5D0aWKSveYH9F/zuwyABCDEu+fBLo/q6JAqnXziFluK9jcnu1vam3FvTABCDEx5UYSvdTo+axlbAAAAFQCFBEABCDEabLfUQpAoiMQSNpCsFQAAAIAVHwW6ABCDEyS/o6aV6icX0Gw8K9AG4zjj+OZWd0HxJP/RnABCDErY63WC2vUAbmR0hrmoa3dS1Fw6553n+Ks41z9xzLydWABCDEpI655LrDtnMEzDpIYtr8idKq3j3IvmBfynvzhmb0r101agiMdEKsUJjGQbRAbL42ABCDEe5gAAAIAbdNorJ2iFrcz8OyYi25Vjap+gqFGUYMwaHlta26WWf+ZHQABCDEsc4n4twkb94jjoj7doiBjN7aRABCDEJEDUboTatpdOzanU9gSrryCBykz5RK016wwOU8fxdmaar/U47qEKa4TYwmYyA5UH+KmbYmJExC5ovXlI25oAbc1eQg== user@host","name":"mykey","fingerPrint":"75:19:bf:48:dc:31:5b:91:b2:e5:ba:3b:f0:00:ba:ar","region":"SGB-1"} diff --git a/libcloud/test/compute/test_runabove.py b/libcloud/test/compute/test_runabove.py new file mode 100644 index 0000000000..54e881e49a --- /dev/null +++ b/libcloud/test/compute/test_runabove.py @@ -0,0 +1,148 @@ +# 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 sys +import unittest +from mock import patch + +from libcloud.utils.py3 import httplib + +from libcloud.compute.drivers.runabove import RunAboveNodeDriver + +from libcloud.test.common.test_runabove import BaseRunAboveMockHttp +from libcloud.test.secrets import RUNABOVE_PARAMS +from libcloud.test.file_fixtures import ComputeFileFixtures + + +class RunAboveMockHttp(BaseRunAboveMockHttp): + """Fixtures needed for tests related to rating model""" + fixtures = ComputeFileFixtures('runabove') + + def _json_1_0_auth_time_get(self, method, url, body, headers): + body = self.fixtures.load('auth_time_get.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _json_1_0_region_get(self, method, url, body, headers): + body = self.fixtures.load('region_get.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _json_1_0_flavor_foo_id_get(self, method, url, body, headers): + body = self.fixtures.load('flavor_get_detail.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _json_1_0_flavor_get(self, method, url, body, headers): + body = self.fixtures.load('flavor_get.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _json_1_0_image_get(self, method, url, body, headers): + body = self.fixtures.load('image_get.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _json_1_0_image_foo_id_get(self, method, url, body, headers): + body = self.fixtures.load('image_get_detail.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _json_1_0_ssh_get(self, method, url, body, headers): + body = self.fixtures.load('ssh_get.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _json_1_0_ssh_post(self, method, url, body, headers): + body = self.fixtures.load('ssh_get_detail.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _json_1_0_ssh_mykey_get(self, method, url, body, headers): + body = self.fixtures.load('ssh_get_detail.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _json_1_0_instance_get(self, method, url, body, headers): + body = self.fixtures.load('instance_get.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _json_1_0_instance_foo_get(self, method, url, body, headers): + body = self.fixtures.load('instance_get_detail.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _json_1_0_instance_foo_delete(self, method, url, body, headers): + return (httplib.OK, '', {}, httplib.responses[httplib.OK]) + + def _json_1_0_instance_post(self, method, url, body, headers): + body = self.fixtures.load('instance_get_detail.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + +@patch('libcloud.common.runabove.RunAboveConnection._timedelta', 42) +class RunAboveTests(unittest.TestCase): + def setUp(self): + RunAboveNodeDriver.connectionCls.conn_classes = ( + RunAboveMockHttp, RunAboveMockHttp) + RunAboveMockHttp.type = None + self.driver = RunAboveNodeDriver(*RUNABOVE_PARAMS) + + def test_list_locations(self): + images = self.driver.list_locations() + self.assertTrue(len(images) > 0) + + def test_list_images(self): + images = self.driver.list_images() + self.assertTrue(len(images) > 0) + + def test_get_image(self): + image = self.driver.get_image('foo-id') + self.assertEqual(image.id, 'foo-id') + + def test_list_sizes(self): + sizes = self.driver.list_sizes() + self.assertTrue(len(sizes) > 0) + + def test_get_size(self): + size = self.driver.ex_get_size('foo-id') + self.assertEqual(size.id, 'foo-id') + + def test_list_key_pairs(self): + keys = self.driver.list_sizes() + self.assertTrue(len(keys) > 0) + + def test_get_key_pair(self): + location = self.driver.list_locations()[0] + key = self.driver.get_key_pair('mykey', location) + self.assertEqual(key.name, 'mykey') + + def test_import_key_pair_from_string(self): + location = self.driver.list_locations()[0] + key = self.driver.import_key_pair_from_string('mykey', 'material', + location) + self.assertEqual(key.name, 'mykey') + + def test_list_nodes(self): + nodes = self.driver.list_nodes() + self.assertTrue(len(nodes) > 0) + + def test_get_node(self): + node = self.driver.ex_get_node('foo') + self.assertEqual(node.name, 'testvm') + + def test_create_node(self): + location = self.driver.list_locations()[0] + image = self.driver.list_sizes(location)[0] + size = self.driver.list_sizes(location)[0] + node = self.driver.create_node(name='testvm', image=image, size=size, + location=location) + self.assertEqual(node.name, 'testvm') + + def test_destroy_node(self): + node = self.driver.list_nodes()[0] + self.driver.destroy_node(node) + +if __name__ == '__main__': + sys.exit(unittest.main()) diff --git a/libcloud/test/secrets.py-dist b/libcloud/test/secrets.py-dist index 83e28d1692..0a7b4864ee 100644 --- a/libcloud/test/secrets.py-dist +++ b/libcloud/test/secrets.py-dist @@ -32,6 +32,7 @@ OPENSTACK_PARAMS = ('user_name', 'api_key', False, 'host', 8774) OPENNEBULA_PARAMS = ('user', 'key') DIMENSIONDATA_PARAMS = ('user', 'password') OPSOURCE_PARAMS = ('user', 'password') +RUNABOVE_PARAMS = ('application_key', 'application_secret', 'consumer_key') RACKSPACE_PARAMS = ('user', 'key') RACKSPACE_NOVA_PARAMS = ('user_name', 'api_key', False, 'host', 8774) SLICEHOST_PARAMS = ('key',)