Source code for libcloud.compute.drivers.digitalocean

# 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.
"""
DigitalOcean Driver
"""
import json
import warnings

from libcloud.utils.iso8601 import parse_date
from libcloud.utils.py3 import httplib

from libcloud.common.digitalocean import DigitalOcean_v1_BaseDriver
from libcloud.common.digitalocean import DigitalOcean_v2_BaseDriver
from libcloud.common.types import InvalidCredsError
from libcloud.compute.types import Provider, NodeState
from libcloud.compute.base import NodeImage, NodeSize, NodeLocation, KeyPair
from libcloud.compute.base import Node, NodeDriver

__all__ = [
    'DigitalOceanNodeDriver',
    'DigitalOcean_v1_NodeDriver',
    'DigitalOcean_v2_NodeDriver'
]


[docs]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) :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' or secret is not None: if secret is None: raise InvalidCredsError( 'secret missing for v1 authentication') if secret is not None and api_version == 'v2': raise InvalidCredsError( 'secret not accepted for v2 authentication') 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)
# TODO Implement v1 driver using KeyPair class SSHKey(object): def __init__(self, id, name, pub_key): self.id = id self.name = name self.pub_key = pub_key def __repr__(self): return (('<SSHKey: id=%s, name=%s, pub_key=%s>') % (self.id, self.name, self.pub_key))
[docs]class DigitalOcean_v1_NodeDriver(DigitalOcean_v1_BaseDriver, DigitalOceanNodeDriver): """ DigitalOcean NodeDriver using v1 of the API. """ NODE_STATE_MAP = {'new': NodeState.PENDING, 'off': NodeState.REBOOTING, 'active': NodeState.RUNNING}
[docs] def list_nodes(self): data = self.connection.request('/v1/droplets').object['droplets'] return list(map(self._to_node, data))
[docs] def list_locations(self): data = self.connection.request('/v1/regions').object['regions'] return list(map(self._to_location, data))
[docs] def list_images(self): data = self.connection.request('/v1/images').object['images'] return list(map(self._to_image, data))
[docs] def list_sizes(self): data = self.connection.request('/v1/sizes').object['sizes'] return list(map(self._to_size, data))
[docs] def create_node(self, name, size, image, location, ex_ssh_key_ids=None): """ 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_id': size.id, 'image_id': image.id, 'region_id': location.id} if ex_ssh_key_ids: params['ssh_key_ids'] = ','.join(ex_ssh_key_ids) data = self.connection.request('/v1/droplets/new', params=params) # TODO: Handle this in the response class status = data.object.get('status', 'OK') if status == 'ERROR': message = data.object.get('message', None) error_message = data.object.get('error_message', message) raise ValueError('Failed to create node: %s' % (error_message)) return self._to_node(data=data.object['droplet'])
[docs] def reboot_node(self, node): res = self.connection.request('/v1/droplets/%s/reboot/' % (node.id)) return res.status == httplib.OK
[docs] def destroy_node(self, node): params = {'scrub_data': '1'} res = self.connection.request('/v1/droplets/%s/destroy/' % (node.id), params=params) return res.status == httplib.OK
[docs] def ex_rename_node(self, node, name): params = {'name': name} res = self.connection.request('/v1/droplets/%s/rename/' % (node.id), params=params) return res.status == httplib.OK
[docs] def list_key_pairs(self): """ List all the available SSH keys. :return: Available SSH keys. :rtype: ``list`` of :class:`KeyPair` """ data = self.connection.request('/v1/ssh_keys').object['ssh_keys'] return list(map(self._to_key_pair, data))
[docs] def ex_list_ssh_keys(self): """ List all the available SSH keys. :return: Available SSH keys. :rtype: ``list`` of :class:`SSHKey` """ warnings.warn("This method has been deprecated in " "favor of the list_key_pairs method") data = self.connection.request('/v1/ssh_keys').object['ssh_keys'] return list(map(self._to_ssh_key, data))
[docs] 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('/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.
[docs] def create_key_pair(self, name, ssh_key_pub): """ Create a new SSH key. :param name: Key name (required) :type name: ``str`` :param name: Valid public key string (required) :type name: ``str`` """ params = {'name': name, 'ssh_pub_key': ssh_key_pub} data = self.connection.request('/v1/ssh_keys/new/', method='GET', params=params).object assert 'ssh_key' in data # 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'])
[docs] def ex_create_ssh_key(self, name, ssh_key_pub): """ Create a new SSH key. :param name: Key name (required) :type name: ``str`` :param name: Valid public key string (required) :type name: ``str`` """ warnings.warn("This method has been deprecated in " "favor of the create_key_pair method") params = {'name': name, 'ssh_pub_key': ssh_key_pub} 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'])
[docs] def delete_key_pair(self, key_pair): """ Delete an existing key pair. :param key_pair: Key pair object. :type key_pair: :class:`.KeyPair` """ 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
[docs] def ex_destroy_ssh_key(self, key_id): """ Delete an existing SSH key. :param key_id: SSH key id (required) :type key_id: ``str`` """ warnings.warn( "This method has been deprecated in " "favor of the delete_key_pair method") res = self.connection.request('/v1/ssh_keys/%s/destroy/' % (key_id)) return res.status == httplib.OK
def _to_node(self, data): extra_keys = ['backups_active', 'region_id', 'image_id', 'size_id'] if 'status' in data: state = self.NODE_STATE_MAP.get(data['status'], NodeState.UNKNOWN) else: state = NodeState.UNKNOWN if 'ip_address' in data and data['ip_address'] is not None: public_ips = [data['ip_address']] else: public_ips = [] 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=None, extra=extra, driver=self) return node def _to_image(self, data): extra = {'distribution': data['distribution']} return NodeImage(id=data['id'], name=data['name'], extra=extra, driver=self) def _to_location(self, data): return NodeLocation(id=data['id'], name=data['name'], country=None, driver=self) def _to_size(self, data): ram = data['name'].lower() if 'mb' in ram: ram = int(ram.replace('mb', '')) elif 'gb' in ram: ram = int(ram.replace('gb', '')) * 1024 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))
[docs]class DigitalOcean_v2_NodeDriver(DigitalOcean_v2_BaseDriver, DigitalOceanNodeDriver): """ DigitalOcean NodeDriver using v2 of the API. """ NODE_STATE_MAP = {'new': NodeState.PENDING, 'off': NodeState.STOPPED, 'active': NodeState.RUNNING, 'archive': NodeState.TERMINATED} EX_CREATE_ATTRIBUTES = ['backups', 'ipv6', 'private_networking', 'ssh_keys']
[docs] def list_images(self): data = self._paginated_request('/v2/images', 'images') return list(map(self._to_image, data))
[docs] def list_key_pairs(self): """ List all the available SSH keys. :return: Available SSH keys. :rtype: ``list`` of :class:`KeyPair` """ data = self._paginated_request('/v2/account/keys', 'ssh_keys') return list(map(self._to_key_pair, data))
[docs] def list_locations(self): data = self._paginated_request('/v2/regions', 'regions') return list(map(self._to_location, data))
[docs] def list_nodes(self): data = self._paginated_request('/v2/droplets', 'droplets') return list(map(self._to_node, data))
[docs] def list_sizes(self): data = self._paginated_request('/v2/sizes', 'sizes') return list(map(self._to_size, data))
[docs] def create_node(self, name, size, image, location, ex_create_attr=None, ex_ssh_key_ids=None, ex_user_data=None): """ Create a node. The `ex_create_attr` parameter can include the following dictionary key and value pairs: * `backups`: ``bool`` defaults to False * `ipv6`: ``bool`` defaults to False * `private_networking`: ``bool`` defaults to False * `user_data`: ``str`` for cloud-config data * `ssh_keys`: ``list`` of ``int`` key ids or ``str`` fingerprints `ex_create_attr['ssh_keys']` will override `ex_ssh_key_ids` assignment. :keyword ex_create_attr: A dictionary of optional attributes for droplet creation :type ex_create_attr: ``dict`` :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 ``int`` key ids or ``str`` key fingerprints :keyword ex_user_data: User data to be added to the node on create. (optional) :type ex_user_data: ``str`` :return: The newly created node. :rtype: :class:`Node` """ attr = {'name': name, 'size': size.name, 'image': image.id, 'region': location.id, 'user_data': ex_user_data} if ex_ssh_key_ids: warnings.warn("The ex_ssh_key_ids parameter has been deprecated in" " favor of the ex_create_attr parameter.") attr['ssh_keys'] = ex_ssh_key_ids ex_create_attr = ex_create_attr or {} for key in ex_create_attr.keys(): if key in self.EX_CREATE_ATTRIBUTES: attr[key] = ex_create_attr[key] res = self.connection.request('/v2/droplets', data=json.dumps(attr), method='POST') data = res.object['droplet'] # TODO: Handle this in the response class status = res.object.get('status', 'OK') if status == 'ERROR': message = res.object.get('message', None) error_message = res.object.get('error_message', message) raise ValueError('Failed to create node: %s' % (error_message)) return self._to_node(data=data)
[docs] def destroy_node(self, node): res = self.connection.request('/v2/droplets/%s' % (node.id), method='DELETE') return res.status == httplib.NO_CONTENT
[docs] def reboot_node(self, node): attr = {'type': 'reboot'} res = self.connection.request('/v2/droplets/%s/actions' % (node.id), data=json.dumps(attr), method='POST') return res.status == httplib.CREATED
[docs] def create_image(self, node, name): """ Create an image from 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`` """ attr = {'type': 'snapshot', 'name': name} res = self.connection.request('/v2/droplets/%s/actions' % (node.id), data=json.dumps(attr), method='POST') return res.status == httplib.CREATED
[docs] 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
[docs] 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` """ data = self._paginated_request('/v2/images/%s' % (image_id), 'image') return self._to_image(data)
[docs] def ex_rename_node(self, node, name): attr = {'type': 'rename', 'name': name} res = self.connection.request('/v2/droplets/%s/actions' % (node.id), data=json.dumps(attr), method='POST') return res.status == httplib.CREATED
[docs] def ex_shutdown_node(self, node): attr = {'type': 'shutdown'} res = self.connection.request('/v2/droplets/%s/actions' % (node.id), data=json.dumps(attr), method='POST') return res.status == httplib.CREATED
[docs] def ex_power_on_node(self, node): attr = {'type': 'power_on'} res = self.connection.request('/v2/droplets/%s/actions' % (node.id), data=json.dumps(attr), method='POST') return res.status == httplib.CREATED
[docs] 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`` """ attr = {'name': name, 'public_key': public_key} res = self.connection.request('/v2/account/keys', method='POST', data=json.dumps(attr)) data = res.object['ssh_key'] return self._to_key_pair(data=data)
[docs] 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
[docs] 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 _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 created = parse_date(data['created_at']) 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, created_at=created, driver=self, extra=extra) 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'], driver=self, extra=extra) 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_pair(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)