DO NodeDriver uses DO BaseDriver - Support for v1 and v2 initialization with authentication secret - Updated v1 DigitalOceanNodeDriver to use updated KeyPair Management - Updated tests to reflect KeyPair Management changes
Signed-off-by: Tomaz Muraus <[email protected]> Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/9224e8e1 Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/9224e8e1 Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/9224e8e1 Branch: refs/heads/trunk Commit: 9224e8e180133f4af499a77366d3ca2d640afeda Parents: d1a8e22 Author: Javier Castillo II <[email protected]> Authored: Sun Apr 12 18:02:07 2015 +0000 Committer: Tomaz Muraus <[email protected]> Committed: Sun Jun 14 18:05:58 2015 +0800 ---------------------------------------------------------------------- libcloud/compute/drivers/digitalocean.py | 172 +++++++++------------ libcloud/test/compute/test_digitalocean_v1.py | 14 +- 2 files changed, 79 insertions(+), 107 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/libcloud/blob/9224e8e1/libcloud/compute/drivers/digitalocean.py ---------------------------------------------------------------------- diff --git a/libcloud/compute/drivers/digitalocean.py b/libcloud/compute/drivers/digitalocean.py index 32dd199..1cb209b 100644 --- a/libcloud/compute/drivers/digitalocean.py +++ b/libcloud/compute/drivers/digitalocean.py @@ -20,6 +20,8 @@ from libcloud.utils.py3 import httplib from libcloud.common.base import ConnectionUserAndKey, ConnectionKey from libcloud.common.base import JsonResponse +from libcloud.common.digitalocean import DigitalOcean_v1_BaseDriver +from libcloud.common.digitalocean import DigitalOcean_v2_BaseDriver from libcloud.compute.types import Provider, NodeState, InvalidCredsError from libcloud.compute.base import NodeDriver, Node from libcloud.compute.base import NodeImage, NodeSize, NodeLocation, KeyPair @@ -27,7 +29,7 @@ from libcloud.compute.base import NodeImage, NodeSize, NodeLocation, KeyPair __all__ = [ 'DigitalOceanNodeDriver', 'DigitalOcean_v1_NodeDriver', - 'DigitalOcean_v1_NodeDriver' + 'DigitalOcean_v2_NodeDriver' ] @@ -35,6 +37,15 @@ class DigitalOceanNodeDriver(NodeDriver): """ DigitalOcean NodeDriver defaulting to using APIv2. + :keyword key: Required for authentication. Used in both ``v1`` and + ``v2`` implementations. + :type key: ``str`` + + :keyword secret: Used in driver authentication with key. Defaults to + None and when set, will cause driver to use ``v1`` for + connection and response. (optional) + :type secret: ``str`` + :keyword api_version: Specifies the API version to use. ``v1`` and ``v2`` are the only valid options. Defaults to using ``v2`` (optional) @@ -46,7 +57,7 @@ class DigitalOceanNodeDriver(NodeDriver): def __new__(cls, key, secret=None, api_version='v2', **kwargs): if cls is DigitalOceanNodeDriver: - if api_version == 'v1': + if api_version == 'v1' or secret != None: cls = DigitalOcean_v1_NodeDriver elif api_version == 'v2': cls = DigitalOcean_v2_NodeDriver @@ -55,45 +66,7 @@ class DigitalOceanNodeDriver(NodeDriver): (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 - raise InvalidCredsError(self.body) - elif self.status == httplib.UNAUTHORIZED: - body = self.parse_body() - raise InvalidCredsError(body['message']) - else: - body = self.parse_body() - - if 'error_message' in body: - error = '%s (code: %s)' % (body['error_message'], self.status) - else: - error = body - 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 - - +# TODO Implement v1 driver using KeyPair class SSHKey(object): def __init__(self, id, name, pub_key): self.id = id @@ -105,52 +78,11 @@ class SSHKey(object): (self.id, self.name, self.pub_key)) -class DigitalOcean_v1_Connection(ConnectionUserAndKey): - """ - Connection class for the DigitalOcean (v1) driver. - """ - - host = 'api.digitalocean.com' - responseCls = DigitalOcean_v1_Response - - def add_default_params(self, params): - """ - Add parameters that are necessary for every request - - This method adds ``client_id`` and ``api_key`` to - the request. - """ - params['client_id'] = self.user_id - params['api_key'] = self.key - return params - - -class DigitalOcean_v2_Connection(ConnectionKey): - """ - Connection class for the DigitalOcean (v2) driver. - """ - - host = 'api.digitalocean.com' - responseCls = DigitalOcean_v2_Response - - 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): +class DigitalOcean_v1_NodeDriver(DigitalOcean_v1_BaseDriver, DigitalOceanNodeDriver): """ DigitalOcean NodeDriver using v1 of the API. """ - connectionCls = DigitalOcean_v1_Connection - NODE_STATE_MAP = {'new': NodeState.PENDING, 'off': NodeState.REBOOTING, 'active': NodeState.RUNNING} @@ -215,17 +147,33 @@ class DigitalOcean_v1_NodeDriver(DigitalOceanNodeDriver): params=params) return res.status == httplib.OK - def ex_list_ssh_keys(self): + def list_key_pairs(self): """ List all the available SSH keys. :return: Available SSH keys. - :rtype: ``list`` of :class:`SSHKey` + :rtype: ``list`` of :class:`KeyPair` """ data = self.connection.request('/v1/ssh_keys').object['ssh_keys'] - return list(map(self._to_ssh_key, data)) + return list(map(self._to_key_pair, data)) + + def get_key_pair(self, name): + """ + Retrieve a single key pair. + + :param name: Name of the key pair to retrieve. + :type name: ``str`` - def ex_create_ssh_key(self, name, ssh_key_pub): + :rtype: :class:`.KeyPair` + """ + qkey = [k for k in self.list_key_pairs() if k.name == name][0] + data = self.connection.request('/v1/ssh_keys/%s' % + qkey.extra['id']).object['ssh_key'] + return self._to_key_pair(data=data) + + #TODO: This adds the ssh_key_pub parameter. This puts the burden of making + # it within the function or on the API. The KeyPair API needs work. + def create_key_pair(self, name, ssh_key_pub): """ Create a new SSH key. @@ -239,16 +187,20 @@ class DigitalOcean_v1_NodeDriver(DigitalOceanNodeDriver): 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']) + #TODO: libcloud.compute.base.KeyPair.create_key_pair doesn't specify + # a return value. This looks like it should return a KeyPair + return self._to_key_pair(data=data['ssh_key']) - def ex_destroy_ssh_key(self, key_id): + def delete_key_pair(self, key_pair): """ - Delete an existing SSH key. + Delete an existing key pair. - :param key_id: SSH key id (required) - :type key_id: ``str`` + :param key_pair: Key pair object. + :type key_pair: :class:`.KeyPair` """ - res = self.connection.request('/v1/ssh_keys/%s/destroy/' % (key_id)) + res = self.connection.request('/v1/ssh_keys/%s/destroy/' % + key_pair.extra['id']) + #TODO: This looks like it should return bool like the other delete_* return res.status == httplib.OK def _to_node(self, data): @@ -293,18 +245,24 @@ class DigitalOcean_v1_NodeDriver(DigitalOceanNodeDriver): return NodeSize(id=data['id'], name=data['name'], ram=ram, disk=0, bandwidth=0, price=0, driver=self) + def _to_key_pair(self, data): + try: + pubkey = data['ssh_pub_key'] + except KeyError: + pubkey = None + return KeyPair(data['name'], public_key=pubkey, fingerprint=None, + driver=self, private_key=None, extra={'id':data['id']}) + 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): +class DigitalOcean_v2_NodeDriver(DigitalOcean_v2_BaseDriver, DigitalOceanNodeDriver): """ DigitalOcean NodeDriver using v2 of the API. """ - connectionCls = DigitalOcean_v2_Connection - NODE_STATE_MAP = {'new': NodeState.PENDING, 'off': NodeState.STOPPED, 'active': NodeState.RUNNING, @@ -440,8 +398,22 @@ class DigitalOcean_v2_NodeDriver(DigitalOceanNodeDriver): :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)) + data = self._paginated_request('/v2/account/keys', 'ssh_keys') + return list(map(self._to_key_pair, data)) + + def get_key_pair(self, name): + """ + Retrieve a single key pair. + + :param name: Name of the key pair to retrieve. + :type name: ``str`` + + :rtype: :class:`.KeyPair` + """ + qkey = [k for k in self.list_key_pairs() if k.name == name][0] + data = self.connection.request('/v2/account/keys/%s' % + qkey.extra['id']).object['ssh_key'] + return self._to_key_pair(data=data) def create_key_pair(self, name, public_key): """ @@ -456,7 +428,7 @@ class DigitalOcean_v2_NodeDriver(DigitalOceanNodeDriver): 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) + return self._to_key_pair(data=data) def delete_key_pair(self, key): """ @@ -543,7 +515,7 @@ class DigitalOcean_v2_NodeDriver(DigitalOceanNodeDriver): disk=data['disk'], bandwidth=data['transfer'], price=data['price_hourly'], driver=self, extra=extra) - def _to_key_pairs(self, data): + def _to_key_pair(self, data): extra = {'id': data['id']} return KeyPair(name=data['name'], fingerprint=data['fingerprint'], http://git-wip-us.apache.org/repos/asf/libcloud/blob/9224e8e1/libcloud/test/compute/test_digitalocean_v1.py ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/test_digitalocean_v1.py b/libcloud/test/compute/test_digitalocean_v1.py index 030e2ee..c8ff999 100644 --- a/libcloud/test/compute/test_digitalocean_v1.py +++ b/libcloud/test/compute/test_digitalocean_v1.py @@ -110,17 +110,17 @@ class DigitalOcean_v1_Tests(LibcloudTestCase): result = self.driver.ex_rename_node(node, 'fedora helios') self.assertTrue(result) - def test_ex_list_ssh_keys(self): - keys = self.driver.ex_list_ssh_keys() + def test_list_key_pairs(self): + keys = self.driver.list_key_pairs() self.assertEqual(len(keys), 1) - self.assertEqual(keys[0].id, 7717) + self.assertEqual(keys[0].extra['id'], 7717) self.assertEqual(keys[0].name, 'test1') - self.assertEqual(keys[0].pub_key, None) + self.assertEqual(keys[0].public_key, None) - def test_ex_destroy_ssh_key(self): - key = self.driver.ex_list_ssh_keys()[0] - result = self.driver.ex_destroy_ssh_key(key.id) + def test_delete_key_pair(self): + key = self.driver.list_key_pairs()[0] + result = self.driver.delete_key_pair(key) self.assertTrue(result)
