blob: a3903524503d628d7b7d8bb4dda1210b1960df5e [file] [log] [blame]
# 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'
]
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))
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}
def list_nodes(self):
data = self.connection.request('/v1/droplets').object['droplets']
return list(map(self._to_node, data))
def list_locations(self):
data = self.connection.request('/v1/regions').object['regions']
return list(map(self._to_location, data))
def list_images(self):
data = self.connection.request('/v1/images').object['images']
return list(map(self._to_image, data))
def list_sizes(self):
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):
"""
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'])
def reboot_node(self, node):
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('/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('/v1/droplets/%s/rename/' % (node.id),
params=params)
return res.status == httplib.OK
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))
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))
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.
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'])
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'])
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
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))
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']
def list_images(self):
data = self._paginated_request('/v2/images', 'images')
return list(map(self._to_image, data))
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))
def list_locations(self):
data = self._paginated_request('/v2/regions', 'regions')
return list(map(self._to_location, data))
def list_nodes(self):
data = self._paginated_request('/v2/droplets', 'droplets')
return list(map(self._to_node, data))
def list_sizes(self):
data = self._paginated_request('/v2/sizes', 'sizes')
return list(map(self._to_size, data))
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)
def destroy_node(self, node):
res = self.connection.request('/v2/droplets/%s' % (node.id),
method='DELETE')
return res.status == httplib.NO_CONTENT
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
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
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_get_creation_time(self, node):
"""
Return the date and time that represent when the Instance was created.
:param node: Node instance
:type node: :class:`Node`
:return: ISO8601 combined date and time format string for when the
Droplet was created.
:rtype: ``str``
"""
return node.extra['created_at']
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)
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
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
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
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)
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 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)