From 1be3b53b7e2eef246b76c58bd6f2afbc74b2f091 Mon Sep 17 00:00:00 2001 From: Andrew Starr-Bochicchio Date: Sun, 1 Feb 2015 13:14:08 -0500 Subject: [PATCH] Add support for DigitalOcean's APIv2 and use it by default while maintaining support for v1. --- libcloud/compute/drivers/digitalocean.py | 359 ++++++++++++++++-- .../fixtures/digitalocean/create_image.json | 12 + .../digitalocean/create_key_pair.json | 8 + .../compute/fixtures/digitalocean/error.json | 1 + .../digitalocean/ex_power_on_node.json | 12 + .../digitalocean/ex_shutdown_node.json | 12 + .../fixtures/digitalocean/get_image.json | 14 + .../fixtures/digitalocean/list_key_pairs.json | 13 + .../digitalocean/list_nodes_page_1.json | 95 +++++ .../create_node.json | 0 .../destroy_node.json | 0 .../error.txt | 0 .../error_invalid_image.json | 0 .../ex_create_ssh_key.json | 0 .../ex_destroy_ssh_key.json | 0 .../ex_list_ssh_keys.json | 0 .../ex_rename_node.json | 0 .../list_images.json | 0 .../list_locations.json | 0 .../list_nodes.json | 0 .../list_nodes_empty.json | 0 .../digitalocean_v1/list_nodes_page_2.json | 95 +++++ .../list_sizes.json | 0 .../reboot_node.json | 0 .../digitalocean_v2/create_image.json | 12 + .../digitalocean_v2/create_key_pair.json | 8 + .../fixtures/digitalocean_v2/create_node.json | 13 + .../fixtures/digitalocean_v2/error.json | 1 + .../digitalocean_v2/error_invalid_image.json | 1 + .../digitalocean_v2/ex_power_on_node.json | 12 + .../digitalocean_v2/ex_rename_node.json | 13 + .../digitalocean_v2/ex_shutdown_node.json | 12 + .../fixtures/digitalocean_v2/get_image.json | 14 + .../fixtures/digitalocean_v2/list_images.json | 44 +++ .../digitalocean_v2/list_key_pairs.json | 13 + .../digitalocean_v2/list_locations.json | 48 +++ .../fixtures/digitalocean_v2/list_nodes.json | 89 +++++ .../digitalocean_v2/list_nodes_empty.json | 7 + .../digitalocean_v2/list_nodes_page_1.json | 95 +++++ .../fixtures/digitalocean_v2/list_sizes.json | 35 ++ .../fixtures/digitalocean_v2/reboot_node.json | 13 + ...igitalocean.py => test_digitalocean_v1.py} | 31 +- libcloud/test/compute/test_digitalocean_v2.py | 268 +++++++++++++ libcloud/test/secrets.py-dist | 3 +- 44 files changed, 1312 insertions(+), 41 deletions(-) create mode 100644 libcloud/test/compute/fixtures/digitalocean/create_image.json create mode 100644 libcloud/test/compute/fixtures/digitalocean/create_key_pair.json create mode 100644 libcloud/test/compute/fixtures/digitalocean/error.json create mode 100644 libcloud/test/compute/fixtures/digitalocean/ex_power_on_node.json create mode 100644 libcloud/test/compute/fixtures/digitalocean/ex_shutdown_node.json create mode 100644 libcloud/test/compute/fixtures/digitalocean/get_image.json create mode 100644 libcloud/test/compute/fixtures/digitalocean/list_key_pairs.json create mode 100644 libcloud/test/compute/fixtures/digitalocean/list_nodes_page_1.json rename libcloud/test/compute/fixtures/{digitalocean => digitalocean_v1}/create_node.json (100%) rename libcloud/test/compute/fixtures/{digitalocean => digitalocean_v1}/destroy_node.json (100%) rename libcloud/test/compute/fixtures/{digitalocean => digitalocean_v1}/error.txt (100%) rename libcloud/test/compute/fixtures/{digitalocean => digitalocean_v1}/error_invalid_image.json (100%) rename libcloud/test/compute/fixtures/{digitalocean => digitalocean_v1}/ex_create_ssh_key.json (100%) rename libcloud/test/compute/fixtures/{digitalocean => digitalocean_v1}/ex_destroy_ssh_key.json (100%) rename libcloud/test/compute/fixtures/{digitalocean => digitalocean_v1}/ex_list_ssh_keys.json (100%) rename libcloud/test/compute/fixtures/{digitalocean => digitalocean_v1}/ex_rename_node.json (100%) rename libcloud/test/compute/fixtures/{digitalocean => digitalocean_v1}/list_images.json (100%) rename libcloud/test/compute/fixtures/{digitalocean => digitalocean_v1}/list_locations.json (100%) rename libcloud/test/compute/fixtures/{digitalocean => digitalocean_v1}/list_nodes.json (100%) rename libcloud/test/compute/fixtures/{digitalocean => digitalocean_v1}/list_nodes_empty.json (100%) create mode 100644 libcloud/test/compute/fixtures/digitalocean_v1/list_nodes_page_2.json rename libcloud/test/compute/fixtures/{digitalocean => digitalocean_v1}/list_sizes.json (100%) rename libcloud/test/compute/fixtures/{digitalocean => digitalocean_v1}/reboot_node.json (100%) create mode 100644 libcloud/test/compute/fixtures/digitalocean_v2/create_image.json create mode 100644 libcloud/test/compute/fixtures/digitalocean_v2/create_key_pair.json create mode 100644 libcloud/test/compute/fixtures/digitalocean_v2/create_node.json create mode 100644 libcloud/test/compute/fixtures/digitalocean_v2/error.json create mode 100644 libcloud/test/compute/fixtures/digitalocean_v2/error_invalid_image.json create mode 100644 libcloud/test/compute/fixtures/digitalocean_v2/ex_power_on_node.json create mode 100644 libcloud/test/compute/fixtures/digitalocean_v2/ex_rename_node.json create mode 100644 libcloud/test/compute/fixtures/digitalocean_v2/ex_shutdown_node.json create mode 100644 libcloud/test/compute/fixtures/digitalocean_v2/get_image.json create mode 100644 libcloud/test/compute/fixtures/digitalocean_v2/list_images.json create mode 100644 libcloud/test/compute/fixtures/digitalocean_v2/list_key_pairs.json create mode 100644 libcloud/test/compute/fixtures/digitalocean_v2/list_locations.json create mode 100644 libcloud/test/compute/fixtures/digitalocean_v2/list_nodes.json create mode 100644 libcloud/test/compute/fixtures/digitalocean_v2/list_nodes_empty.json create mode 100644 libcloud/test/compute/fixtures/digitalocean_v2/list_nodes_page_1.json create mode 100644 libcloud/test/compute/fixtures/digitalocean_v2/list_sizes.json create mode 100644 libcloud/test/compute/fixtures/digitalocean_v2/reboot_node.json rename libcloud/test/compute/{test_digitalocean.py => test_digitalocean_v1.py} (86%) create mode 100644 libcloud/test/compute/test_digitalocean_v2.py diff --git a/libcloud/compute/drivers/digitalocean.py b/libcloud/compute/drivers/digitalocean.py index 4ec6da1513..2d19512602 100644 --- a/libcloud/compute/drivers/digitalocean.py +++ b/libcloud/compute/drivers/digitalocean.py @@ -18,13 +18,39 @@ from libcloud.utils.py3 import httplib -from libcloud.common.base import ConnectionUserAndKey, JsonResponse +from libcloud.common.base import ConnectionUserAndKey, ConnectionKey +from libcloud.common.base import JsonResponse from libcloud.compute.types import Provider, NodeState, InvalidCredsError -from libcloud.compute.base import NodeDriver -from libcloud.compute.base import Node, NodeImage, NodeSize, NodeLocation +from libcloud.compute.base import NodeDriver, Node +from libcloud.compute.base import NodeImage, NodeSize, NodeLocation, KeyPair -class DigitalOceanResponse(JsonResponse): +class DigitalOceanNodeDriver(NodeDriver): + """ + DigitalOcean NodeDriver defaulting to using APIv2. + + :keyword api_version: Specifies the API version to use. ``v1`` and + ``v2`` are the only valid options. Defaults to + using ``v2`` (optional) + :type api_version: ``str`` + """ + type = Provider.DIGITAL_OCEAN + name = 'DigitalOcean' + website = 'https://www.digitalocean.com' + + def __new__(cls, key, secret=None, api_version='v2', **kwargs): + if cls is DigitalOceanNodeDriver: + if api_version == 'v1': + cls = DigitalOcean_v1_NodeDriver + elif api_version == 'v2': + cls = DigitalOcean_v2_NodeDriver + else: + raise NotImplementedError('Unsupported API version: %s' % + (api_version)) + return super(DigitalOceanNodeDriver, cls).__new__(cls, **kwargs) + + +class DigitalOcean_v1_Response(JsonResponse): def parse_error(self): if self.status == httplib.FOUND and '/api/error' in self.body: # Hacky, but DigitalOcean error responses are awful @@ -42,6 +68,26 @@ def parse_error(self): return error +class DigitalOcean_v2_Response(JsonResponse): + valid_response_codes = [httplib.OK, httplib.ACCEPTED, httplib.CREATED, + httplib.NO_CONTENT] + + def parse_error(self): + if self.status == httplib.UNAUTHORIZED: + body = self.parse_body() + raise InvalidCredsError(body['message']) + else: + body = self.parse_body() + if 'message' in body: + error = '%s (code: %s)' % (body['message'], self.status) + else: + error = body + return error + + def success(self): + return self.status in self.valid_response_codes + + class SSHKey(object): def __init__(self, id, name, pub_key): self.id = id @@ -53,13 +99,13 @@ def __repr__(self): (self.id, self.name, self.pub_key)) -class DigitalOceanConnection(ConnectionUserAndKey): +class DigitalOcean_v1_Connection(ConnectionUserAndKey): """ - Connection class for the DigitalOcean driver. + Connection class for the DigitalOcean (v1) driver. """ host = 'api.digitalocean.com' - responseCls = DigitalOceanResponse + responseCls = DigitalOcean_v1_Response def add_default_params(self, params): """ @@ -73,35 +119,50 @@ def add_default_params(self, params): return params -class DigitalOceanNodeDriver(NodeDriver): +class DigitalOcean_v2_Connection(ConnectionKey): """ - DigitalOceanNode node driver. + Connection class for the DigitalOcean (v2) driver. """ - connectionCls = DigitalOceanConnection + host = 'api.digitalocean.com' + responseCls = DigitalOcean_v2_Response - type = Provider.DIGITAL_OCEAN - name = 'Digital Ocean' - website = 'https://www.digitalocean.com' + def add_default_headers(self, headers): + """ + Add headers that are necessary for every request + + This method adds ``token`` to the request. + """ + headers['Authorization'] = 'Bearer %s' % (self.key) + headers['Content-Type'] = 'application/json' + return headers + + +class DigitalOcean_v1_NodeDriver(DigitalOceanNodeDriver): + """ + DigitalOcean NodeDriver using v1 of the API. + """ + + connectionCls = DigitalOcean_v1_Connection NODE_STATE_MAP = {'new': NodeState.PENDING, 'off': NodeState.REBOOTING, 'active': NodeState.RUNNING} def list_nodes(self): - data = self.connection.request('/droplets').object['droplets'] + data = self.connection.request('/v1/droplets').object['droplets'] return list(map(self._to_node, data)) def list_locations(self): - data = self.connection.request('/regions').object['regions'] + data = self.connection.request('/v1/regions').object['regions'] return list(map(self._to_location, data)) def list_images(self): - data = self.connection.request('/images').object['images'] + data = self.connection.request('/v1/images').object['images'] return list(map(self._to_image, data)) def list_sizes(self): - data = self.connection.request('/sizes').object['sizes'] + data = self.connection.request('/v1/sizes').object['sizes'] return list(map(self._to_size, data)) def create_node(self, name, size, image, location, ex_ssh_key_ids=None, @@ -122,22 +183,22 @@ def create_node(self, name, size, image, location, ex_ssh_key_ids=None, if ex_ssh_key_ids: params['ssh_key_ids'] = ','.join(ex_ssh_key_ids) - data = self.connection.request('/droplets/new', params=params).object - return self._to_node(data=data['droplet']) + data = self.connection.request('/v1/droplets/new', params=params) + return self._to_node(data=data.object['droplet']) def reboot_node(self, node): - res = self.connection.request('/droplets/%s/reboot/' % (node.id)) + res = self.connection.request('/v1/droplets/%s/reboot/' % (node.id)) return res.status == httplib.OK def destroy_node(self, node): params = {'scrub_data': '1'} - res = self.connection.request('/droplets/%s/destroy/' % (node.id), + res = self.connection.request('/v1/droplets/%s/destroy/' % (node.id), params=params) return res.status == httplib.OK def ex_rename_node(self, node, name): params = {'name': name} - res = self.connection.request('/droplets/%s/rename/' % (node.id), + res = self.connection.request('/v1/droplets/%s/rename/' % (node.id), params=params) return res.status == httplib.OK @@ -148,7 +209,7 @@ def ex_list_ssh_keys(self): :return: Available SSH keys. :rtype: ``list`` of :class:`SSHKey` """ - data = self.connection.request('/ssh_keys').object['ssh_keys'] + data = self.connection.request('/v1/ssh_keys').object['ssh_keys'] return list(map(self._to_ssh_key, data)) def ex_create_ssh_key(self, name, ssh_key_pub): @@ -162,7 +223,7 @@ def ex_create_ssh_key(self, name, ssh_key_pub): :type name: ``str`` """ params = {'name': name, 'ssh_pub_key': ssh_key_pub} - data = self.connection.request('/ssh_keys/new/', method='GET', + data = self.connection.request('/v1/ssh_keys/new/', method='GET', params=params).object assert 'ssh_key' in data return self._to_ssh_key(data=data['ssh_key']) @@ -174,7 +235,7 @@ def ex_destroy_ssh_key(self, key_id): :param key_id: SSH key id (required) :type key_id: ``str`` """ - res = self.connection.request('/ssh_keys/%s/destroy/' % (key_id)) + res = self.connection.request('/v1/ssh_keys/%s/destroy/' % (key_id)) return res.status == httplib.OK def _to_node(self, data): @@ -222,3 +283,251 @@ def _to_size(self, data): def _to_ssh_key(self, data): return SSHKey(id=data['id'], name=data['name'], pub_key=data.get('ssh_pub_key', None)) + + +class DigitalOcean_v2_NodeDriver(DigitalOceanNodeDriver): + """ + DigitalOcean NodeDriver using v2 of the API. + """ + + connectionCls = DigitalOcean_v2_Connection + + NODE_STATE_MAP = {'new': NodeState.PENDING, + 'off': NodeState.STOPPED, + 'active': NodeState.RUNNING, + 'archive': NodeState.TERMINATED} + + def list_nodes(self): + data = self._paginated_request('/v2/droplets', 'droplets') + return list(map(self._to_node, data)) + + def list_locations(self): + data = self.connection.request('/v2/regions').object['regions'] + return list(map(self._to_location, data)) + + def list_images(self): + data = self._paginated_request('/v2/images', 'images') + return list(map(self._to_image, data)) + + def list_sizes(self): + data = self.connection.request('/v2/sizes').object['sizes'] + return list(map(self._to_size, data)) + + def create_node(self, name, size, image, location, ex_ssh_key_ids=None, + **kwargs): + """ + Create a node. + + :keyword ex_ssh_key_ids: A list of ssh key ids which will be added + to the server. (optional) + :type ex_ssh_key_ids: ``list`` of ``str`` + + :return: The newly created node. + :rtype: :class:`Node` + """ + params = {'name': name, 'size': size.name, 'image': image.id, + 'region': location.id} + + if ex_ssh_key_ids: + params['ssh_key_ids'] = ','.join(ex_ssh_key_ids) + + data = self.connection.request('/v2/droplets', + params=params, method='POST').object + return self._to_node(data=data['droplet']) + + def reboot_node(self, node): + params = {'type': 'reboot'} + res = self.connection.request('/v2/droplets/%s/actions' % (node.id), + params=params, method='POST') + return res.status == httplib.CREATED + + def destroy_node(self, node): + res = self.connection.request('/v2/droplets/%s' % (node.id), + method='DELETE') + return res.status == httplib.NO_CONTENT + + def get_image(self, image_id): + """ + Get an image based on an image_id + + @inherits: :class:`NodeDriver.get_image` + + :param image_id: Image identifier + :type image_id: ``int`` + + :return: A NodeImage object + :rtype: :class:`NodeImage` + """ + res = self.connection.request('/v2/images/%s' % (image_id)) + data = res.object['image'] + return self._to_image(data) + + def create_image(self, node, name): + """ + Create an image fron a Node. + + @inherits: :class:`NodeDriver.create_image` + + :param node: Node to use as base for image + :type node: :class:`Node` + + :param node: Name for image + :type node: ``str`` + + :rtype: ``bool`` + """ + params = {'type': 'snapshot', 'name': name} + res = self.connection.request('/v2/droplets/%s/actions' % (node.id), + params=params, method='POST') + return res.status == httplib.CREATED + + def delete_image(self, image): + """Delete an image for node. + + @inherits: :class:`NodeDriver.delete_image` + + :param image: the image to be deleted + :type image: :class:`NodeImage` + + :rtype: ``bool`` + """ + res = self.connection.request('/v2/images/%s' % (image.id), + method='DELETE') + return res.status == httplib.NO_CONTENT + + def ex_rename_node(self, node, name): + params = {'type': 'rename', 'name': name} + res = self.connection.request('/v2/droplets/%s/actions' % (node.id), + params=params, method='POST') + return res.status == httplib.CREATED + + def ex_shutdown_node(self, node): + params = {'type': 'shutdown'} + res = self.connection.request('/v2/droplets/%s/actions' % (node.id), + params=params, method='POST') + return res.status == httplib.CREATED + + def ex_power_on_node(self, node): + params = {'type': 'power_on'} + res = self.connection.request('/v2/droplets/%s/actions' % (node.id), + params=params, method='POST') + return res.status == httplib.CREATED + + def list_key_pairs(self): + """ + List all the available SSH keys. + + :return: Available SSH keys. + :rtype: ``list`` of :class:`KeyPair` + """ + data = self.connection.request('/v2/account/keys').object['ssh_keys'] + return list(map(self._to_key_pairs, data)) + + def create_key_pair(self, name, public_key): + """ + Create a new SSH key. + + :param name: Key name (required) + :type name: ``str`` + + :param public_key: Valid public key string (required) + :type public_key: ``str`` + """ + params = {'name': name, 'public_key': public_key} + data = self.connection.request('/v2/account/keys', method='POST', + params=params).object['ssh_key'] + return self._to_key_pairs(data=data) + + def delete_key_pair(self, key): + """ + Delete an existing SSH key. + + :param key: SSH key (required) + :type key: :class:`KeyPair` + """ + key_id = key.extra['id'] + res = self.connection.request('/v2/account/keys/%s' % (key_id), + method='DELETE') + return res.status == httplib.NO_CONTENT + + def _paginated_request(self, url, obj): + """ + Perform multiple calls in order to have a full list of elements + when the API are paginated. + """ + params = {} + data = self.connection.request(url) + try: + pages = data.object['links']['pages']['last'].split('=')[-1] + values = data.object[obj] + for page in range(2, int(pages) + 1): + params.update({'page': page}) + new_data = self.connection.request(url, params=params) + + more_values = new_data.object[obj] + for value in more_values: + values.append(value) + data = values + except KeyError: # No pages. + data = data.object[obj] + + return data + + def _to_node(self, data): + extra_keys = ['memory', 'vcpus', 'disk', 'region', 'image', + 'size_slug', 'locked', 'created_at', 'networks', + 'kernel', 'backup_ids', 'snapshot_ids', 'features'] + if 'status' in data: + state = self.NODE_STATE_MAP.get(data['status'], NodeState.UNKNOWN) + else: + state = NodeState.UNKNOWN + + networks = data['networks'] + private_ips = [] + public_ips = [] + if networks: + for net in networks['v4']: + if net['type'] == 'private': + private_ips = [net['ip_address']] + if net['type'] == 'public': + public_ips = [net['ip_address']] + + extra = {} + for key in extra_keys: + if key in data: + extra[key] = data[key] + + node = Node(id=data['id'], name=data['name'], state=state, + public_ips=public_ips, private_ips=private_ips, + extra=extra, driver=self) + return node + + def _to_image(self, data): + extra = {'distribution': data['distribution'], + 'public': data['public'], + 'slug': data['slug'], + 'regions': data['regions'], + 'min_disk_size': data['min_disk_size'], + 'created_at': data['created_at']} + return NodeImage(id=data['id'], name=data['name'], extra=extra, + driver=self) + + def _to_location(self, data): + return NodeLocation(id=data['slug'], name=data['name'], country=None, + driver=self) + + def _to_size(self, data): + extra = {'vcpus': data['vcpus'], + 'regions': data['regions']} + return NodeSize(id=data['slug'], name=data['slug'], ram=data['memory'], + disk=data['disk'], bandwidth=data['transfer'], + price=data['price_hourly'], driver=self, extra=extra) + + def _to_key_pairs(self, data): + extra = {'id': data['id']} + return KeyPair(name=data['name'], + fingerprint=data['fingerprint'], + public_key=data['public_key'], + private_key=None, + driver=self, + extra=extra) diff --git a/libcloud/test/compute/fixtures/digitalocean/create_image.json b/libcloud/test/compute/fixtures/digitalocean/create_image.json new file mode 100644 index 0000000000..69d4fb44e1 --- /dev/null +++ b/libcloud/test/compute/fixtures/digitalocean/create_image.json @@ -0,0 +1,12 @@ +{ + "action": { + "id": 36805022, + "status": "in-progress", + "type": "snapshot", + "started_at": "2014-11-14T16:34:39Z", + "completed_at": null, + "resource_id": 3164450, + "resource_type": "droplet", + "region": "nyc3" + } +} diff --git a/libcloud/test/compute/fixtures/digitalocean/create_key_pair.json b/libcloud/test/compute/fixtures/digitalocean/create_key_pair.json new file mode 100644 index 0000000000..9802bc731a --- /dev/null +++ b/libcloud/test/compute/fixtures/digitalocean/create_key_pair.json @@ -0,0 +1,8 @@ +{ + "ssh_key": { + "id": 7717, + "fingerprint": "f5:d1:78:ed:28:72:5f:e1:ac:94:fd:1f:e0:a3:48:6d", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQsxRiUKn example", + "name": "test1" + } +} diff --git a/libcloud/test/compute/fixtures/digitalocean/error.json b/libcloud/test/compute/fixtures/digitalocean/error.json new file mode 100644 index 0000000000..08e22c292f --- /dev/null +++ b/libcloud/test/compute/fixtures/digitalocean/error.json @@ -0,0 +1 @@ +{"id":"","message":"Unable to authenticate you."} diff --git a/libcloud/test/compute/fixtures/digitalocean/ex_power_on_node.json b/libcloud/test/compute/fixtures/digitalocean/ex_power_on_node.json new file mode 100644 index 0000000000..d7ce8a6cd7 --- /dev/null +++ b/libcloud/test/compute/fixtures/digitalocean/ex_power_on_node.json @@ -0,0 +1,12 @@ +{ + "action": { + "id": 36804758, + "status": "in-progress", + "type": "power_on", + "started_at": "2014-11-14T16:31:19Z", + "completed_at": null, + "resource_id": 3164450, + "resource_type": "droplet", + "region": "nyc3" + } +} diff --git a/libcloud/test/compute/fixtures/digitalocean/ex_shutdown_node.json b/libcloud/test/compute/fixtures/digitalocean/ex_shutdown_node.json new file mode 100644 index 0000000000..f3507befc4 --- /dev/null +++ b/libcloud/test/compute/fixtures/digitalocean/ex_shutdown_node.json @@ -0,0 +1,12 @@ +{ + "action": { + "id": 36077293, + "status": "in-progress", + "type": "shutdown", + "started_at": "2014-11-04T17:08:03Z", + "completed_at": null, + "resource_id": 3067649, + "resource_type": "droplet", + "region": "nyc2" + } +} diff --git a/libcloud/test/compute/fixtures/digitalocean/get_image.json b/libcloud/test/compute/fixtures/digitalocean/get_image.json new file mode 100644 index 0000000000..225e5f342f --- /dev/null +++ b/libcloud/test/compute/fixtures/digitalocean/get_image.json @@ -0,0 +1,14 @@ +{ + "image": { + "id": 12345, + "name": "My snapshot", + "distribution": "Ubuntu", + "slug": null, + "public": false, + "regions": [ + "nyc2" + ], + "created_at": "2014-11-04T22:23:02Z", + "min_disk_size": 20 + } +} diff --git a/libcloud/test/compute/fixtures/digitalocean/list_key_pairs.json b/libcloud/test/compute/fixtures/digitalocean/list_key_pairs.json new file mode 100644 index 0000000000..ca155ed4bf --- /dev/null +++ b/libcloud/test/compute/fixtures/digitalocean/list_key_pairs.json @@ -0,0 +1,13 @@ +{ + "ssh_keys": [ + { + "id": 7717, + "fingerprint": "f5:d1:78:ed:28:72:5f:e1:ac:94:fd:1f:e0:a3:48:6d", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDGk5 example", + "name": "test1" + } + ], + "meta": { + "total": 1 + } +} diff --git a/libcloud/test/compute/fixtures/digitalocean/list_nodes_page_1.json b/libcloud/test/compute/fixtures/digitalocean/list_nodes_page_1.json new file mode 100644 index 0000000000..fb6fb08107 --- /dev/null +++ b/libcloud/test/compute/fixtures/digitalocean/list_nodes_page_1.json @@ -0,0 +1,95 @@ +{ + "droplets": [ + { + "id": 3164444, + "name": "example.com", + "memory": 512, + "vcpus": 1, + "disk": 20, + "locked": false, + "status": "active", + "kernel": { + "id": 2233, + "name": "Ubuntu 14.04 x64 vmlinuz-3.13.0-37-generic", + "version": "3.13.0-37-generic" + }, + "created_at": "2014-11-14T16:29:21Z", + "features": [ + "backups", + "ipv6", + "virtio" + ], + "backup_ids": [ + 7938002 + ], + "snapshot_ids": [ + + ], + "image": { + "id": 6918990, + "name": "14.04 x64", + "distribution": "Ubuntu", + "slug": "ubuntu-14-04-x64", + "public": true, + "regions": [ + "nyc1", + "ams1", + "sfo1", + "nyc2", + "ams2", + "sgp1", + "lon1", + "nyc3", + "ams3", + "nyc3" + ], + "created_at": "2014-10-17T20:24:33Z", + "min_disk_size": 20 + }, + "size_slug": "512mb", + "networks": { + "v4": [ + { + "ip_address": "104.236.32.182", + "netmask": "255.255.192.0", + "gateway": "104.236.0.1", + "type": "public" + } + ], + "v6": [ + { + "ip_address": "2604:A880:0800:0010:0000:0000:02DD:4001", + "netmask": 64, + "gateway": "2604:A880:0800:0010:0000:0000:0000:0001", + "type": "public" + } + ] + }, + "region": { + "name": "New York 3", + "slug": "nyc3", + "sizes": [ + + ], + "features": [ + "virtio", + "private_networking", + "backups", + "ipv6", + "metadata" + ], + "available": null + } + } + ], + "links": { + "pages": + { + "last":"https://api.digitalocean.com/v2/droplets?page=2", + "next":"https://api.digitalocean.com/v2/droplets?page=2" + } + }, + "meta": { + "total":2 + } +} diff --git a/libcloud/test/compute/fixtures/digitalocean/create_node.json b/libcloud/test/compute/fixtures/digitalocean_v1/create_node.json similarity index 100% rename from libcloud/test/compute/fixtures/digitalocean/create_node.json rename to libcloud/test/compute/fixtures/digitalocean_v1/create_node.json diff --git a/libcloud/test/compute/fixtures/digitalocean/destroy_node.json b/libcloud/test/compute/fixtures/digitalocean_v1/destroy_node.json similarity index 100% rename from libcloud/test/compute/fixtures/digitalocean/destroy_node.json rename to libcloud/test/compute/fixtures/digitalocean_v1/destroy_node.json diff --git a/libcloud/test/compute/fixtures/digitalocean/error.txt b/libcloud/test/compute/fixtures/digitalocean_v1/error.txt similarity index 100% rename from libcloud/test/compute/fixtures/digitalocean/error.txt rename to libcloud/test/compute/fixtures/digitalocean_v1/error.txt diff --git a/libcloud/test/compute/fixtures/digitalocean/error_invalid_image.json b/libcloud/test/compute/fixtures/digitalocean_v1/error_invalid_image.json similarity index 100% rename from libcloud/test/compute/fixtures/digitalocean/error_invalid_image.json rename to libcloud/test/compute/fixtures/digitalocean_v1/error_invalid_image.json diff --git a/libcloud/test/compute/fixtures/digitalocean/ex_create_ssh_key.json b/libcloud/test/compute/fixtures/digitalocean_v1/ex_create_ssh_key.json similarity index 100% rename from libcloud/test/compute/fixtures/digitalocean/ex_create_ssh_key.json rename to libcloud/test/compute/fixtures/digitalocean_v1/ex_create_ssh_key.json diff --git a/libcloud/test/compute/fixtures/digitalocean/ex_destroy_ssh_key.json b/libcloud/test/compute/fixtures/digitalocean_v1/ex_destroy_ssh_key.json similarity index 100% rename from libcloud/test/compute/fixtures/digitalocean/ex_destroy_ssh_key.json rename to libcloud/test/compute/fixtures/digitalocean_v1/ex_destroy_ssh_key.json diff --git a/libcloud/test/compute/fixtures/digitalocean/ex_list_ssh_keys.json b/libcloud/test/compute/fixtures/digitalocean_v1/ex_list_ssh_keys.json similarity index 100% rename from libcloud/test/compute/fixtures/digitalocean/ex_list_ssh_keys.json rename to libcloud/test/compute/fixtures/digitalocean_v1/ex_list_ssh_keys.json diff --git a/libcloud/test/compute/fixtures/digitalocean/ex_rename_node.json b/libcloud/test/compute/fixtures/digitalocean_v1/ex_rename_node.json similarity index 100% rename from libcloud/test/compute/fixtures/digitalocean/ex_rename_node.json rename to libcloud/test/compute/fixtures/digitalocean_v1/ex_rename_node.json diff --git a/libcloud/test/compute/fixtures/digitalocean/list_images.json b/libcloud/test/compute/fixtures/digitalocean_v1/list_images.json similarity index 100% rename from libcloud/test/compute/fixtures/digitalocean/list_images.json rename to libcloud/test/compute/fixtures/digitalocean_v1/list_images.json diff --git a/libcloud/test/compute/fixtures/digitalocean/list_locations.json b/libcloud/test/compute/fixtures/digitalocean_v1/list_locations.json similarity index 100% rename from libcloud/test/compute/fixtures/digitalocean/list_locations.json rename to libcloud/test/compute/fixtures/digitalocean_v1/list_locations.json diff --git a/libcloud/test/compute/fixtures/digitalocean/list_nodes.json b/libcloud/test/compute/fixtures/digitalocean_v1/list_nodes.json similarity index 100% rename from libcloud/test/compute/fixtures/digitalocean/list_nodes.json rename to libcloud/test/compute/fixtures/digitalocean_v1/list_nodes.json diff --git a/libcloud/test/compute/fixtures/digitalocean/list_nodes_empty.json b/libcloud/test/compute/fixtures/digitalocean_v1/list_nodes_empty.json similarity index 100% rename from libcloud/test/compute/fixtures/digitalocean/list_nodes_empty.json rename to libcloud/test/compute/fixtures/digitalocean_v1/list_nodes_empty.json diff --git a/libcloud/test/compute/fixtures/digitalocean_v1/list_nodes_page_2.json b/libcloud/test/compute/fixtures/digitalocean_v1/list_nodes_page_2.json new file mode 100644 index 0000000000..e6b89ba830 --- /dev/null +++ b/libcloud/test/compute/fixtures/digitalocean_v1/list_nodes_page_2.json @@ -0,0 +1,95 @@ +{ + "droplets": [ + { + "id": 3164445, + "name": "example1.com", + "memory": 512, + "vcpus": 1, + "disk": 20, + "locked": false, + "status": "active", + "kernel": { + "id": 2233, + "name": "Ubuntu 14.04 x64 vmlinuz-3.13.0-37-generic", + "version": "3.13.0-37-generic" + }, + "created_at": "2014-11-14T16:29:21Z", + "features": [ + "backups", + "ipv6", + "virtio" + ], + "backup_ids": [ + 7938002 + ], + "snapshot_ids": [ + + ], + "image": { + "id": 6918990, + "name": "14.04 x64", + "distribution": "Ubuntu", + "slug": "ubuntu-14-04-x64", + "public": true, + "regions": [ + "nyc1", + "ams1", + "sfo1", + "nyc2", + "ams2", + "sgp1", + "lon1", + "nyc3", + "ams3", + "nyc3" + ], + "created_at": "2014-10-17T20:24:33Z", + "min_disk_size": 20 + }, + "size_slug": "512mb", + "networks": { + "v4": [ + { + "ip_address": "104.236.32.182", + "netmask": "255.255.192.0", + "gateway": "104.236.0.1", + "type": "public" + } + ], + "v6": [ + { + "ip_address": "2604:A880:0800:0010:0000:0000:02DD:4001", + "netmask": 64, + "gateway": "2604:A880:0800:0010:0000:0000:0000:0001", + "type": "public" + } + ] + }, + "region": { + "name": "New York 3", + "slug": "nyc3", + "sizes": [ + + ], + "features": [ + "virtio", + "private_networking", + "backups", + "ipv6", + "metadata" + ], + "available": null + } + } + ], + "links": { + "pages": + { + "first":"https://api.digitalocean.com/v2/droplets?page=1", + "prev":"https://api.digitalocean.com/v2/droplets?page=1" + } + }, + "meta": { + "total":2 + } +} diff --git a/libcloud/test/compute/fixtures/digitalocean/list_sizes.json b/libcloud/test/compute/fixtures/digitalocean_v1/list_sizes.json similarity index 100% rename from libcloud/test/compute/fixtures/digitalocean/list_sizes.json rename to libcloud/test/compute/fixtures/digitalocean_v1/list_sizes.json diff --git a/libcloud/test/compute/fixtures/digitalocean/reboot_node.json b/libcloud/test/compute/fixtures/digitalocean_v1/reboot_node.json similarity index 100% rename from libcloud/test/compute/fixtures/digitalocean/reboot_node.json rename to libcloud/test/compute/fixtures/digitalocean_v1/reboot_node.json diff --git a/libcloud/test/compute/fixtures/digitalocean_v2/create_image.json b/libcloud/test/compute/fixtures/digitalocean_v2/create_image.json new file mode 100644 index 0000000000..69d4fb44e1 --- /dev/null +++ b/libcloud/test/compute/fixtures/digitalocean_v2/create_image.json @@ -0,0 +1,12 @@ +{ + "action": { + "id": 36805022, + "status": "in-progress", + "type": "snapshot", + "started_at": "2014-11-14T16:34:39Z", + "completed_at": null, + "resource_id": 3164450, + "resource_type": "droplet", + "region": "nyc3" + } +} diff --git a/libcloud/test/compute/fixtures/digitalocean_v2/create_key_pair.json b/libcloud/test/compute/fixtures/digitalocean_v2/create_key_pair.json new file mode 100644 index 0000000000..9802bc731a --- /dev/null +++ b/libcloud/test/compute/fixtures/digitalocean_v2/create_key_pair.json @@ -0,0 +1,8 @@ +{ + "ssh_key": { + "id": 7717, + "fingerprint": "f5:d1:78:ed:28:72:5f:e1:ac:94:fd:1f:e0:a3:48:6d", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQsxRiUKn example", + "name": "test1" + } +} diff --git a/libcloud/test/compute/fixtures/digitalocean_v2/create_node.json b/libcloud/test/compute/fixtures/digitalocean_v2/create_node.json new file mode 100644 index 0000000000..8e54e39c7c --- /dev/null +++ b/libcloud/test/compute/fixtures/digitalocean_v2/create_node.json @@ -0,0 +1,13 @@ +{ + "action": + { + "id":39290099, + "status":"completed", + "type":"create", + "started_at":"2014-12-19T19:14:36Z", + "completed_at":"2014-12-19T19:15:23Z", + "resource_id":12345, + "resource_type":"droplet", + "region":"nyc3" + } +} diff --git a/libcloud/test/compute/fixtures/digitalocean_v2/error.json b/libcloud/test/compute/fixtures/digitalocean_v2/error.json new file mode 100644 index 0000000000..08e22c292f --- /dev/null +++ b/libcloud/test/compute/fixtures/digitalocean_v2/error.json @@ -0,0 +1 @@ +{"id":"","message":"Unable to authenticate you."} diff --git a/libcloud/test/compute/fixtures/digitalocean_v2/error_invalid_image.json b/libcloud/test/compute/fixtures/digitalocean_v2/error_invalid_image.json new file mode 100644 index 0000000000..755cd48fc8 --- /dev/null +++ b/libcloud/test/compute/fixtures/digitalocean_v2/error_invalid_image.json @@ -0,0 +1 @@ +{"id":"unprocessable_entity","message":"You specified an invalid image for Droplet creation."} diff --git a/libcloud/test/compute/fixtures/digitalocean_v2/ex_power_on_node.json b/libcloud/test/compute/fixtures/digitalocean_v2/ex_power_on_node.json new file mode 100644 index 0000000000..d7ce8a6cd7 --- /dev/null +++ b/libcloud/test/compute/fixtures/digitalocean_v2/ex_power_on_node.json @@ -0,0 +1,12 @@ +{ + "action": { + "id": 36804758, + "status": "in-progress", + "type": "power_on", + "started_at": "2014-11-14T16:31:19Z", + "completed_at": null, + "resource_id": 3164450, + "resource_type": "droplet", + "region": "nyc3" + } +} diff --git a/libcloud/test/compute/fixtures/digitalocean_v2/ex_rename_node.json b/libcloud/test/compute/fixtures/digitalocean_v2/ex_rename_node.json new file mode 100644 index 0000000000..eafe147b94 --- /dev/null +++ b/libcloud/test/compute/fixtures/digitalocean_v2/ex_rename_node.json @@ -0,0 +1,13 @@ +{ + "action": + { + "id":918910, + "status":"in-progress", + "type":"rename", + "started_at":"2014-12-21T02:19:17Z", + "completed_at":null, + "resource_id":12345, + "resource_type":"droplet", + "region":"nyc3" + } +} diff --git a/libcloud/test/compute/fixtures/digitalocean_v2/ex_shutdown_node.json b/libcloud/test/compute/fixtures/digitalocean_v2/ex_shutdown_node.json new file mode 100644 index 0000000000..f3507befc4 --- /dev/null +++ b/libcloud/test/compute/fixtures/digitalocean_v2/ex_shutdown_node.json @@ -0,0 +1,12 @@ +{ + "action": { + "id": 36077293, + "status": "in-progress", + "type": "shutdown", + "started_at": "2014-11-04T17:08:03Z", + "completed_at": null, + "resource_id": 3067649, + "resource_type": "droplet", + "region": "nyc2" + } +} diff --git a/libcloud/test/compute/fixtures/digitalocean_v2/get_image.json b/libcloud/test/compute/fixtures/digitalocean_v2/get_image.json new file mode 100644 index 0000000000..225e5f342f --- /dev/null +++ b/libcloud/test/compute/fixtures/digitalocean_v2/get_image.json @@ -0,0 +1,14 @@ +{ + "image": { + "id": 12345, + "name": "My snapshot", + "distribution": "Ubuntu", + "slug": null, + "public": false, + "regions": [ + "nyc2" + ], + "created_at": "2014-11-04T22:23:02Z", + "min_disk_size": 20 + } +} diff --git a/libcloud/test/compute/fixtures/digitalocean_v2/list_images.json b/libcloud/test/compute/fixtures/digitalocean_v2/list_images.json new file mode 100644 index 0000000000..a21579bad1 --- /dev/null +++ b/libcloud/test/compute/fixtures/digitalocean_v2/list_images.json @@ -0,0 +1,44 @@ +{ + "images": [ + { + "id": 119192817, + "name": "14.04 x64", + "distribution": "Ubuntu", + "slug": "ubuntu-14-04-x64", + "public": true, + "regions": [ + "nyc1" + ], + "created_at": "2014-07-29T14:35:40Z", + "min_disk_size": 20 + }, + { + "id": 449676376, + "name": "14.04 x32", + "distribution": "Ubuntu", + "slug": "ubuntu-14-04-x32", + "public": true, + "regions": [ + "nyc1" + ], + "created_at": "2014-07-29T14:35:40Z", + "min_disk_size": 20 + }, + { + "id": 449676856, + "name": "My Snapshot", + "distribution": "Ubuntu", + "slug": "", + "public": false, + "regions": [ + "nyc1", + "nyc3" + ], + "created_at": "2014-08-18T16:35:40Z", + "min_disk_size": 40 + } + ], + "meta": { + "total": 3 + } +} diff --git a/libcloud/test/compute/fixtures/digitalocean_v2/list_key_pairs.json b/libcloud/test/compute/fixtures/digitalocean_v2/list_key_pairs.json new file mode 100644 index 0000000000..ca155ed4bf --- /dev/null +++ b/libcloud/test/compute/fixtures/digitalocean_v2/list_key_pairs.json @@ -0,0 +1,13 @@ +{ + "ssh_keys": [ + { + "id": 7717, + "fingerprint": "f5:d1:78:ed:28:72:5f:e1:ac:94:fd:1f:e0:a3:48:6d", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDGk5 example", + "name": "test1" + } + ], + "meta": { + "total": 1 + } +} diff --git a/libcloud/test/compute/fixtures/digitalocean_v2/list_locations.json b/libcloud/test/compute/fixtures/digitalocean_v2/list_locations.json new file mode 100644 index 0000000000..50a483109c --- /dev/null +++ b/libcloud/test/compute/fixtures/digitalocean_v2/list_locations.json @@ -0,0 +1,48 @@ +{ + "regions": [ + { + "slug": "nyc1", + "name": "New York 1", + "sizes": [ + "1gb", + "512mb" + ], + "available": false, + "features": [ + "virtio", + "private_networking", + "backups", + "ipv6" + ] + }, + { + "slug": "sfo1", + "name": "San Francisco 1", + "sizes": [ + "1gb", + "512mb" + ], + "available": true, + "features": [ + "virtio", + "backups" + ] + }, + { + "slug": "ams1", + "name": "Amsterdam 1", + "sizes": [ + "1gb", + "512mb" + ], + "available": true, + "features": [ + "virtio", + "backups" + ] + } + ], + "meta": { + "total": 3 + } +} diff --git a/libcloud/test/compute/fixtures/digitalocean_v2/list_nodes.json b/libcloud/test/compute/fixtures/digitalocean_v2/list_nodes.json new file mode 100644 index 0000000000..b14ae88030 --- /dev/null +++ b/libcloud/test/compute/fixtures/digitalocean_v2/list_nodes.json @@ -0,0 +1,89 @@ +{ + "droplets": [ + { + "id": 3164444, + "name": "example.com", + "memory": 512, + "vcpus": 1, + "disk": 20, + "locked": false, + "status": "active", + "kernel": { + "id": 2233, + "name": "Ubuntu 14.04 x64 vmlinuz-3.13.0-37-generic", + "version": "3.13.0-37-generic" + }, + "created_at": "2014-11-14T16:29:21Z", + "features": [ + "backups", + "ipv6", + "virtio" + ], + "backup_ids": [ + 7938002 + ], + "snapshot_ids": [ + + ], + "image": { + "id": 6918990, + "name": "14.04 x64", + "distribution": "Ubuntu", + "slug": "ubuntu-14-04-x64", + "public": true, + "regions": [ + "nyc1", + "ams1", + "sfo1", + "nyc2", + "ams2", + "sgp1", + "lon1", + "nyc3", + "ams3", + "nyc3" + ], + "created_at": "2014-10-17T20:24:33Z", + "min_disk_size": 20 + }, + "size_slug": "512mb", + "networks": { + "v4": [ + { + "ip_address": "104.236.32.182", + "netmask": "255.255.192.0", + "gateway": "104.236.0.1", + "type": "public" + } + ], + "v6": [ + { + "ip_address": "2604:A880:0800:0010:0000:0000:02DD:4001", + "netmask": 64, + "gateway": "2604:A880:0800:0010:0000:0000:0000:0001", + "type": "public" + } + ] + }, + "region": { + "name": "New York 3", + "slug": "nyc3", + "sizes": [ + + ], + "features": [ + "virtio", + "private_networking", + "backups", + "ipv6", + "metadata" + ], + "available": null + } + } + ], + "links": {}, + "meta": { + "total": 1 + } +} diff --git a/libcloud/test/compute/fixtures/digitalocean_v2/list_nodes_empty.json b/libcloud/test/compute/fixtures/digitalocean_v2/list_nodes_empty.json new file mode 100644 index 0000000000..75b843dae7 --- /dev/null +++ b/libcloud/test/compute/fixtures/digitalocean_v2/list_nodes_empty.json @@ -0,0 +1,7 @@ +{ + "droplets": [], + "links": {}, + "meta": { + "total": 0 + } +} diff --git a/libcloud/test/compute/fixtures/digitalocean_v2/list_nodes_page_1.json b/libcloud/test/compute/fixtures/digitalocean_v2/list_nodes_page_1.json new file mode 100644 index 0000000000..fb6fb08107 --- /dev/null +++ b/libcloud/test/compute/fixtures/digitalocean_v2/list_nodes_page_1.json @@ -0,0 +1,95 @@ +{ + "droplets": [ + { + "id": 3164444, + "name": "example.com", + "memory": 512, + "vcpus": 1, + "disk": 20, + "locked": false, + "status": "active", + "kernel": { + "id": 2233, + "name": "Ubuntu 14.04 x64 vmlinuz-3.13.0-37-generic", + "version": "3.13.0-37-generic" + }, + "created_at": "2014-11-14T16:29:21Z", + "features": [ + "backups", + "ipv6", + "virtio" + ], + "backup_ids": [ + 7938002 + ], + "snapshot_ids": [ + + ], + "image": { + "id": 6918990, + "name": "14.04 x64", + "distribution": "Ubuntu", + "slug": "ubuntu-14-04-x64", + "public": true, + "regions": [ + "nyc1", + "ams1", + "sfo1", + "nyc2", + "ams2", + "sgp1", + "lon1", + "nyc3", + "ams3", + "nyc3" + ], + "created_at": "2014-10-17T20:24:33Z", + "min_disk_size": 20 + }, + "size_slug": "512mb", + "networks": { + "v4": [ + { + "ip_address": "104.236.32.182", + "netmask": "255.255.192.0", + "gateway": "104.236.0.1", + "type": "public" + } + ], + "v6": [ + { + "ip_address": "2604:A880:0800:0010:0000:0000:02DD:4001", + "netmask": 64, + "gateway": "2604:A880:0800:0010:0000:0000:0000:0001", + "type": "public" + } + ] + }, + "region": { + "name": "New York 3", + "slug": "nyc3", + "sizes": [ + + ], + "features": [ + "virtio", + "private_networking", + "backups", + "ipv6", + "metadata" + ], + "available": null + } + } + ], + "links": { + "pages": + { + "last":"https://api.digitalocean.com/v2/droplets?page=2", + "next":"https://api.digitalocean.com/v2/droplets?page=2" + } + }, + "meta": { + "total":2 + } +} diff --git a/libcloud/test/compute/fixtures/digitalocean_v2/list_sizes.json b/libcloud/test/compute/fixtures/digitalocean_v2/list_sizes.json new file mode 100644 index 0000000000..9e62c4da33 --- /dev/null +++ b/libcloud/test/compute/fixtures/digitalocean_v2/list_sizes.json @@ -0,0 +1,35 @@ +{ + "sizes": [ + { + "slug": "512mb", + "memory": 512, + "vcpus": 1, + "disk": 20, + "transfer": 1, + "price_monthly": 5.0, + "price_hourly": 0.00744, + "regions": [ + "nyc1", + "ams1", + "sfo1" + ] + }, + { + "slug": "1gb", + "memory": 1024, + "vcpus": 2, + "disk": 30, + "transfer": 2, + "price_monthly": 10.0, + "price_hourly": 0.01488, + "regions": [ + "nyc1", + "ams1", + "sfo1" + ] + } + ], + "meta": { + "total": 2 + } +} diff --git a/libcloud/test/compute/fixtures/digitalocean_v2/reboot_node.json b/libcloud/test/compute/fixtures/digitalocean_v2/reboot_node.json new file mode 100644 index 0000000000..7e1d067b7e --- /dev/null +++ b/libcloud/test/compute/fixtures/digitalocean_v2/reboot_node.json @@ -0,0 +1,13 @@ +{ + "action": + { + "id":918910, + "status":"in-progress", + "type":"reboot", + "started_at":"2014-12-21T02:19:17Z", + "completed_at":null, + "resource_id":12345, + "resource_type":"droplet", + "region":"nyc3" + } +} diff --git a/libcloud/test/compute/test_digitalocean.py b/libcloud/test/compute/test_digitalocean_v1.py similarity index 86% rename from libcloud/test/compute/test_digitalocean.py rename to libcloud/test/compute/test_digitalocean_v1.py index ff4159b274..030e2ee07b 100644 --- a/libcloud/test/compute/test_digitalocean.py +++ b/libcloud/test/compute/test_digitalocean_v1.py @@ -28,17 +28,18 @@ from libcloud.test import LibcloudTestCase, MockHttpTestCase from libcloud.test.file_fixtures import ComputeFileFixtures -from libcloud.test.secrets import DIGITAL_OCEAN_PARAMS +from libcloud.test.secrets import DIGITALOCEAN_v1_PARAMS # class DigitalOceanTests(unittest.TestCase, TestCaseMixin): -class DigitalOceanTests(LibcloudTestCase): +class DigitalOcean_v1_Tests(LibcloudTestCase): def setUp(self): DigitalOceanNodeDriver.connectionCls.conn_classes = \ (None, DigitalOceanMockHttp) DigitalOceanMockHttp.type = None - self.driver = DigitalOceanNodeDriver(*DIGITAL_OCEAN_PARAMS) + self.driver = DigitalOceanNodeDriver(*DIGITALOCEAN_v1_PARAMS, + api_version='v1') def test_authentication(self): DigitalOceanMockHttp.type = 'UNAUTHORIZED_CLIENT' @@ -124,56 +125,56 @@ def test_ex_destroy_ssh_key(self): class DigitalOceanMockHttp(MockHttpTestCase): - fixtures = ComputeFileFixtures('digitalocean') + fixtures = ComputeFileFixtures('digitalocean_v1') - def _regions(self, method, url, body, headers): + def _v1_regions(self, method, url, body, headers): body = self.fixtures.load('list_locations.json') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _images(self, method, url, body, headers): + def _v1_images(self, method, url, body, headers): body = self.fixtures.load('list_images.json') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _sizes(self, method, url, body, headers): + def _v1_sizes(self, method, url, body, headers): body = self.fixtures.load('list_sizes.json') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _droplets(self, method, url, body, headers): + def _v1_droplets(self, method, url, body, headers): body = self.fixtures.load('list_nodes.json') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _droplets_new_INVALID_IMAGE(self, method, url, body, headers): + def _v1_droplets_new_INVALID_IMAGE(self, method, url, body, headers): # reboot_node body = self.fixtures.load('error_invalid_image.json') return (httplib.NOT_FOUND, body, {}, httplib.responses[httplib.NOT_FOUND]) - def _droplets_119461_reboot(self, method, url, body, headers): + def _v1_droplets_119461_reboot(self, method, url, body, headers): # reboot_node body = self.fixtures.load('reboot_node.json') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _droplets_119461_destroy(self, method, url, body, headers): + def _v1_droplets_119461_destroy(self, method, url, body, headers): # destroy_node self.assertUrlContainsQueryParams(url, {'scrub_data': '1'}) body = self.fixtures.load('destroy_node.json') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _droplets_119461_rename(self, method, url, body, headers): + def _v1_droplets_119461_rename(self, method, url, body, headers): # reboot_node self.assertUrlContainsQueryParams(url, {'name': 'fedora helios'}) body = self.fixtures.load('ex_rename_node.json') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _ssh_keys(self, method, url, body, headers): + def _v1_ssh_keys(self, method, url, body, headers): body = self.fixtures.load('ex_list_ssh_keys.json') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _ssh_keys_7717_destroy(self, method, url, body, headers): + def _v1_ssh_keys_7717_destroy(self, method, url, body, headers): # destroy_ssh_key body = self.fixtures.load('ex_destroy_ssh_key.json') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _droplets_UNAUTHORIZED_CLIENT(self, method, url, body, headers): + def _v1_droplets_UNAUTHORIZED_CLIENT(self, method, url, body, headers): body = self.fixtures.load('error.txt') return (httplib.FOUND, body, {}, httplib.responses[httplib.FOUND]) diff --git a/libcloud/test/compute/test_digitalocean_v2.py b/libcloud/test/compute/test_digitalocean_v2.py new file mode 100644 index 0000000000..127011a0ce --- /dev/null +++ b/libcloud/test/compute/test_digitalocean_v2.py @@ -0,0 +1,268 @@ +# 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 + +try: + import simplejson as json +except ImportError: + import json # NOQA + +from libcloud.utils.py3 import httplib + +from libcloud.common.types import InvalidCredsError +from libcloud.compute.base import NodeImage +from libcloud.compute.drivers.digitalocean import DigitalOceanNodeDriver + +from libcloud.test import LibcloudTestCase, MockHttpTestCase +from libcloud.test.file_fixtures import ComputeFileFixtures +from libcloud.test.secrets import DIGITALOCEAN_v2_PARAMS + + +# class DigitalOceanTests(unittest.TestCase, TestCaseMixin): +class DigitalOcean_v2_Tests(LibcloudTestCase): + + def setUp(self): + DigitalOceanNodeDriver.connectionCls.conn_classes = \ + (None, DigitalOceanMockHttp) + DigitalOceanMockHttp.type = None + self.driver = DigitalOceanNodeDriver(*DIGITALOCEAN_v2_PARAMS) + + def test_authentication(self): + DigitalOceanMockHttp.type = 'UNAUTHORIZED' + self.assertRaises(InvalidCredsError, self.driver.list_nodes) + + def test_list_images_success(self): + images = self.driver.list_images() + self.assertTrue(len(images) >= 1) + + image = images[0] + self.assertTrue(image.id is not None) + self.assertTrue(image.name is not None) + + def test_list_sizes_success(self): + sizes = self.driver.list_sizes() + self.assertTrue(len(sizes) >= 1) + + size = sizes[0] + self.assertTrue(size.id is not None) + self.assertEqual(size.name, '512mb') + self.assertEqual(size.ram, 512) + + size = sizes[1] + self.assertTrue(size.id is not None) + self.assertEqual(size.name, '1gb') + self.assertEqual(size.ram, 1024) + + def test_list_locations_success(self): + locations = self.driver.list_locations() + self.assertTrue(len(locations) >= 1) + + location = locations[0] + self.assertEqual(location.id, 'nyc1') + self.assertEqual(location.name, 'New York 1') + + def test_list_nodes_success(self): + nodes = self.driver.list_nodes() + self.assertEqual(len(nodes), 1) + self.assertEqual(nodes[0].name, 'example.com') + self.assertEqual(nodes[0].public_ips, ['104.236.32.182']) + self.assertEqual(nodes[0].extra['image']['id'], 6918990) + self.assertEqual(nodes[0].extra['size_slug'], '512mb') + + def test_create_node_invalid_size(self): + image = NodeImage(id='invalid', name=None, driver=self.driver) + size = self.driver.list_sizes()[0] + location = self.driver.list_locations()[0] + + DigitalOceanMockHttp.type = 'INVALID_IMAGE' + expected_msg = r'You specified an invalid image for Droplet creation. \(code: 404\)' + self.assertRaisesRegexp(Exception, expected_msg, + self.driver.create_node, + name='test', size=size, image=image, + location=location) + + def test_reboot_node_success(self): + node = self.driver.list_nodes()[0] + DigitalOceanMockHttp.type = 'REBOOT' + result = self.driver.reboot_node(node) + self.assertTrue(result) + + def test_create_image_success(self): + node = self.driver.list_nodes()[0] + DigitalOceanMockHttp.type = 'SNAPSHOT' + result = self.driver.create_image(node, 'My snapshot') + self.assertTrue(result) + + def test_get_image_success(self): + image = self.driver.get_image(12345) + self.assertEqual(image.name, 'My snapshot') + self.assertEqual(image.id, '12345') + self.assertEqual(image.extra['distribution'], 'Ubuntu') + + def test_delete_image_success(self): + image = self.driver.get_image(12345) + DigitalOceanMockHttp.type = 'DESTROY' + result = self.driver.delete_image(image) + self.assertTrue(result) + + def test_ex_power_on_node_success(self): + node = self.driver.list_nodes()[0] + DigitalOceanMockHttp.type = 'POWERON' + result = self.driver.ex_power_on_node(node) + self.assertTrue(result) + + def test_ex_shutdown_node_success(self): + node = self.driver.list_nodes()[0] + DigitalOceanMockHttp.type = 'SHUTDOWN' + result = self.driver.ex_shutdown_node(node) + self.assertTrue(result) + + def test_destroy_node_success(self): + node = self.driver.list_nodes()[0] + DigitalOceanMockHttp.type = 'DESTROY' + result = self.driver.destroy_node(node) + self.assertTrue(result) + + def test_ex_rename_node_success(self): + node = self.driver.list_nodes()[0] + DigitalOceanMockHttp.type = 'RENAME' + result = self.driver.ex_rename_node(node, 'fedora helios') + self.assertTrue(result) + + def test_list_key_pairs(self): + keys = self.driver.list_key_pairs() + self.assertEqual(len(keys), 1) + self.assertEqual(keys[0].extra['id'], 7717) + self.assertEqual(keys[0].name, 'test1') + self.assertEqual(keys[0].public_key, + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDGk5 example") + + def test_create_key_pair(self): + DigitalOceanMockHttp.type = 'CREATE' + key = self.driver.create_key_pair( + name="test1", + public_key="ssh-rsa AAAAB3NzaC1yc2EAAAADAQsxRiUKn example" + ) + self.assertEqual(key.name, "test1") + self.assertEqual(key.fingerprint, + "f5:d1:78:ed:28:72:5f:e1:ac:94:fd:1f:e0:a3:48:6d") + + def test_delete_key_pair(self): + key = self.driver.list_key_pairs()[0] + result = self.driver.delete_key_pair(key) + self.assertTrue(result) + + def test__paginated_request_single_page(self): + nodes = self.driver._paginated_request('/v2/droplets', 'droplets') + self.assertEqual(nodes[0]['name'], 'example.com') + self.assertEqual(nodes[0]['image']['id'], 6918990) + self.assertEqual(nodes[0]['size_slug'], '512mb') + + def test__paginated_request_two_pages(self): + DigitalOceanMockHttp.type = 'PAGE_ONE' + nodes = self.driver._paginated_request('/v2/droplets', 'droplets') + self.assertEqual(len(nodes), 2) + + +class DigitalOceanMockHttp(MockHttpTestCase): + fixtures = ComputeFileFixtures('digitalocean_v2') + + def _v2_regions(self, method, url, body, headers): + body = self.fixtures.load('list_locations.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v2_images(self, method, url, body, headers): + body = self.fixtures.load('list_images.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v2_sizes(self, method, url, body, headers): + body = self.fixtures.load('list_sizes.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v2_droplets(self, method, url, body, headers): + body = self.fixtures.load('list_nodes.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v2_droplets_INVALID_IMAGE(self, method, url, body, headers): + body = self.fixtures.load('error_invalid_image.json') + return (httplib.NOT_FOUND, body, {}, + httplib.responses[httplib.NOT_FOUND]) + + def _v2_droplets_3164444_actions_REBOOT(self, method, url, body, headers): + # reboot_node + body = self.fixtures.load('reboot_node.json') + return (httplib.CREATED, body, {}, httplib.responses[httplib.CREATED]) + + def _v2_droplets_3164444_DESTROY(self, method, url, body, headers): + # destroy_node + return (httplib.NO_CONTENT, body, {}, + httplib.responses[httplib.NO_CONTENT]) + + def _v2_droplets_3164444_actions_RENAME(self, method, url, body, headers): + # rename_node + body = self.fixtures.load('ex_rename_node.json') + return (httplib.CREATED, body, {}, httplib.responses[httplib.CREATED]) + + def _v2_droplets_3164444_actions_SNAPSHOT(self, method, url, body, headers): + # create_image + body = self.fixtures.load('create_image.json') + return (httplib.CREATED, body, {}, httplib.responses[httplib.CREATED]) + + def _v2_images_12345(self, method, url, body, headers): + # get_image + body = self.fixtures.load('get_image.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v2_images_12345_DESTROY(self, method, url, body, headers): + # delete_image + return (httplib.NO_CONTENT, body, {}, + httplib.responses[httplib.NO_CONTENT]) + + def _v2_droplets_3164444_actions_POWERON(self, method, url, body, headers): + # ex_power_on_node + body = self.fixtures.load('ex_power_on_node.json') + return (httplib.CREATED, body, {}, httplib.responses[httplib.CREATED]) + + def _v2_droplets_3164444_actions_SHUTDOWN(self, method, url, body, headers): + # ex_shutdown_node + body = self.fixtures.load('ex_shutdown_node.json') + return (httplib.CREATED, body, {}, httplib.responses[httplib.CREATED]) + + def _v2_account_keys(self, method, url, body, headers): + body = self.fixtures.load('list_key_pairs.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v2_account_keys_7717(self, method, url, body, headers): + # destroy_ssh_key + return (httplib.NO_CONTENT, body, {}, + httplib.responses[httplib.NO_CONTENT]) + + def _v2_account_keys_CREATE(self, method, url, body, headers): + # create_ssh_key + body = self.fixtures.load('create_key_pair.json') + return (httplib.CREATED, body, {}, httplib.responses[httplib.CREATED]) + + def _v2_droplets_UNAUTHORIZED(self, method, url, body, headers): + body = self.fixtures.load('error.json') + return (httplib.UNAUTHORIZED, body, {}, + httplib.responses[httplib.UNAUTHORIZED]) + + def _v2_droplets_PAGE_ONE(self, method, url, body, headers): + body = self.fixtures.load('list_nodes_page_1.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + +if __name__ == '__main__': + sys.exit(unittest.main()) diff --git a/libcloud/test/secrets.py-dist b/libcloud/test/secrets.py-dist index 3bac5aec79..cca493065c 100644 --- a/libcloud/test/secrets.py-dist +++ b/libcloud/test/secrets.py-dist @@ -42,7 +42,8 @@ JOYENT_PARAMS = ('user', 'key') VCL_PARAMS = ('user', 'pass', True, 'foo.bar.com') GRIDSPOT_PARAMS = ('key',) HOSTVIRTUAL_PARAMS = ('key',) -DIGITAL_OCEAN_PARAMS = ('user', 'key') +DIGITALOCEAN_v1_PARAMS = ('user', 'key') +DIGITALOCEAN_v2_PARAMS = ('token',) CLOUDFRAMES_PARAMS = ('key', 'secret', False, 'host', 8888) PROFIT_BRICKS_PARAMS = ('user', 'key') VULTR_PARAMS = ('key')