261 lines
8.6 KiB
Python
261 lines
8.6 KiB
Python
# 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.
|
|
"""
|
|
A driver for cloudscale.ch.
|
|
"""
|
|
|
|
import json
|
|
|
|
from libcloud.utils.py3 import httplib
|
|
|
|
from libcloud.common.base import ConnectionKey, JsonResponse
|
|
from libcloud.compute.types import Provider, NodeState
|
|
from libcloud.common.types import InvalidCredsError
|
|
from libcloud.compute.base import NodeDriver
|
|
from libcloud.compute.base import Node, NodeImage, NodeSize
|
|
|
|
|
|
class CloudscaleResponse(JsonResponse):
|
|
valid_response_codes = [httplib.OK, httplib.ACCEPTED, httplib.CREATED,
|
|
httplib.NO_CONTENT]
|
|
|
|
def parse_error(self):
|
|
body = self.parse_body()
|
|
if self.status == httplib.UNAUTHORIZED:
|
|
raise InvalidCredsError(body['detail'])
|
|
else:
|
|
# We are taking the first issue here. There might be multiple ones,
|
|
# but that doesn't really matter. It's nicer if the error is just
|
|
# one error (because it's a Python API and there's only one
|
|
# exception.
|
|
return next(iter(body.values()))
|
|
|
|
def success(self):
|
|
return self.status in self.valid_response_codes
|
|
|
|
|
|
class CloudscaleConnection(ConnectionKey):
|
|
"""
|
|
Connection class for the cloudscale.ch driver.
|
|
"""
|
|
host = 'api.cloudscale.ch'
|
|
responseCls = CloudscaleResponse
|
|
|
|
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 CloudscaleNodeDriver(NodeDriver):
|
|
"""
|
|
Cloudscale's node driver.
|
|
"""
|
|
|
|
connectionCls = CloudscaleConnection
|
|
|
|
type = Provider.CLOUDSCALE
|
|
name = 'Cloudscale'
|
|
website = 'https://www.cloudscale.ch'
|
|
|
|
NODE_STATE_MAP = dict(
|
|
changing=NodeState.PENDING,
|
|
running=NodeState.RUNNING,
|
|
stopped=NodeState.STOPPED,
|
|
paused=NodeState.PAUSED,
|
|
)
|
|
|
|
def __init__(self, key, **kwargs):
|
|
super(CloudscaleNodeDriver, self).__init__(key, **kwargs)
|
|
|
|
def list_nodes(self):
|
|
"""
|
|
List all your existing compute nodes.
|
|
"""
|
|
return self._list_resources('/v1/servers', self._to_node)
|
|
|
|
def list_sizes(self):
|
|
"""
|
|
Lists all available sizes. On cloudscale these are known as flavors.
|
|
"""
|
|
return self._list_resources('/v1/flavors', self._to_size)
|
|
|
|
def list_images(self):
|
|
"""
|
|
List all images.
|
|
|
|
Images are identified by slugs on cloudscale.ch. This means that minor
|
|
version upgrades (e.g. Ubuntu 16.04.1 to Ubuntu 16.04.2) will be
|
|
possible within the same id ``ubuntu-16.04``.
|
|
"""
|
|
return self._list_resources('/v1/images', self._to_image)
|
|
|
|
def create_node(self, name, size, image, location=None,
|
|
ex_create_attr=None):
|
|
"""
|
|
Create a node.
|
|
|
|
The `ex_create_attr` parameter can include the following dictionary
|
|
key and value pairs:
|
|
|
|
* `ssh_keys`: ``list`` of ``str`` ssh public keys
|
|
* `volume_size_gb`: ``int`` defaults to 10.
|
|
* `bulk_volume_size_gb`: defaults to None.
|
|
* `use_public_network`: ``bool`` defaults to True
|
|
* `use_private_network`: ``bool`` defaults to False
|
|
* `use_ipv6`: ``bool`` defaults to True
|
|
* `anti_affinity_with`: ``uuid`` of a server to create an anti-affinity
|
|
group with that server or add it to the same group as that server.
|
|
* `user_data`: ``str`` for optional cloud-config data
|
|
|
|
:keyword ex_create_attr: A dictionary of optional attributes for
|
|
droplet creation
|
|
:type ex_create_attr: ``dict``
|
|
|
|
:return: The newly created node.
|
|
:rtype: :class:`Node`
|
|
"""
|
|
ex_create_attr = ex_create_attr or {}
|
|
attr = dict(ex_create_attr)
|
|
attr.update(
|
|
name=name,
|
|
image=image.id,
|
|
flavor=size.id,
|
|
)
|
|
result = self.connection.request(
|
|
'/v1/servers',
|
|
data=json.dumps(attr),
|
|
method='POST'
|
|
)
|
|
return self._to_node(result.object)
|
|
|
|
def reboot_node(self, node):
|
|
"""
|
|
Reboot a node. It's also possible to use ``node.reboot()``.
|
|
"""
|
|
return self._action(node, 'reboot')
|
|
|
|
def start_node(self, node):
|
|
"""
|
|
Start a node. This is only possible if the node is stopped.
|
|
"""
|
|
return self._action(node, 'start')
|
|
|
|
def stop_node(self, node):
|
|
"""
|
|
Stop a specific node. Similar to ``shutdown -h now``. This is only
|
|
possible if the node is running.
|
|
"""
|
|
return self._action(node, 'stop')
|
|
|
|
def ex_start_node(self, node):
|
|
# NOTE: This method is here for backward compatibility reasons after
|
|
# this method was promoted to be part of the standard compute API in
|
|
# Libcloud v2.7.0
|
|
return self.start_node(node=node)
|
|
|
|
def ex_stop_node(self, node):
|
|
# NOTE: This method is here for backward compatibility reasons after
|
|
# this method was promoted to be part of the standard compute API in
|
|
# Libcloud v2.7.0
|
|
return self.stop_node(node=node)
|
|
|
|
def ex_node_by_uuid(self, uuid):
|
|
"""
|
|
:param str ex_user_data: A valid uuid that references your exisiting
|
|
cloudscale.ch server.
|
|
:type ex_user_data: ``str``
|
|
|
|
:return: The server node you asked for.
|
|
:rtype: :class:`Node`
|
|
"""
|
|
res = self.connection.request(self._get_server_url(uuid))
|
|
return self._to_node(res.object)
|
|
|
|
def destroy_node(self, node):
|
|
"""
|
|
Delete a node. It's also possible to use ``node.destroy()``.
|
|
This will irreversibly delete the cloudscale.ch server and all its
|
|
volumes. So please be cautious.
|
|
"""
|
|
res = self.connection.request(
|
|
self._get_server_url(node.id),
|
|
method='DELETE'
|
|
)
|
|
return res.status == httplib.NO_CONTENT
|
|
|
|
def _get_server_url(self, uuid):
|
|
return '/v1/servers/%s' % uuid
|
|
|
|
def _action(self, node, action_name):
|
|
response = self.connection.request(
|
|
self._get_server_url(node.id) + '/' + action_name,
|
|
method='POST'
|
|
)
|
|
return response.status == httplib.OK
|
|
|
|
def _list_resources(self, url, tranform_func):
|
|
data = self.connection.request(url, method='GET').object
|
|
return [tranform_func(obj) for obj in data]
|
|
|
|
def _to_node(self, data):
|
|
state = self.NODE_STATE_MAP.get(data['status'], NodeState.UNKNOWN)
|
|
extra_keys_exclude = ['uuid', 'name', 'status', 'flavor', 'image']
|
|
extra = {}
|
|
for k, v in data.items():
|
|
if k not in extra_keys_exclude:
|
|
extra[k] = v
|
|
|
|
public_ips = []
|
|
private_ips = []
|
|
for interface in data['interfaces']:
|
|
if interface['type'] == 'public':
|
|
ips = public_ips
|
|
else:
|
|
ips = private_ips
|
|
for address_obj in interface['addresses']:
|
|
ips.append(address_obj['address'])
|
|
|
|
return Node(
|
|
id=data['uuid'],
|
|
name=data['name'],
|
|
state=state,
|
|
public_ips=public_ips,
|
|
private_ips=private_ips,
|
|
extra=extra,
|
|
driver=self,
|
|
image=self._to_image(data['image']),
|
|
size=self._to_size(data['flavor']),
|
|
)
|
|
|
|
def _to_size(self, data):
|
|
extra = {'vcpu_count': data['vcpu_count']}
|
|
ram = data['memory_gb'] * 1024
|
|
|
|
return NodeSize(id=data['slug'], name=data['name'],
|
|
ram=ram, disk=10,
|
|
bandwidth=0, price=0,
|
|
extra=extra, driver=self)
|
|
|
|
def _to_image(self, data):
|
|
extra = {'operating_system': data['operating_system']}
|
|
return NodeImage(id=data['slug'], name=data['name'], extra=extra,
|
|
driver=self)
|