| # 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 JsonResponse, ConnectionKey |
| from libcloud.common.types import InvalidCredsError |
| from libcloud.compute.base import Node, NodeSize, NodeImage, NodeDriver |
| from libcloud.compute.types import Provider, NodeState |
| |
| |
| 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().__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) |