| # 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. |
| |
| """ |
| Joyent Cloud (http://www.joyentcloud.com) driver. |
| """ |
| |
| import base64 |
| |
| from libcloud.utils.py3 import b, httplib |
| from libcloud.common.base import JsonResponse, ConnectionUserAndKey |
| from libcloud.common.types import LibcloudError |
| from libcloud.compute.base import Node, NodeSize, NodeImage, NodeDriver |
| from libcloud.compute.types import NodeState, InvalidCredsError |
| from libcloud.utils.networking import is_private_subnet |
| from libcloud.compute.providers import Provider |
| |
| try: |
| import simplejson as json |
| except Exception: |
| import json |
| |
| |
| API_HOST_SUFFIX = ".api.joyentcloud.com" |
| API_VERSION = "~6.5" |
| |
| |
| NODE_STATE_MAP = { |
| "provisioning": NodeState.PENDING, |
| "running": NodeState.RUNNING, |
| "stopping": NodeState.TERMINATED, |
| "stopped": NodeState.TERMINATED, |
| "deleted": NodeState.TERMINATED, |
| } |
| |
| VALID_REGIONS = [ |
| "us-east-1", |
| "us-east-2", |
| "us-east-3", |
| "us-west-1", |
| "us-sw-1", |
| "eu-ams-1", |
| ] |
| DEFAULT_REGION = "us-east-1" |
| |
| |
| class JoyentResponse(JsonResponse): |
| """ |
| Joyent response class. |
| """ |
| |
| valid_response_codes = [ |
| httplib.OK, |
| httplib.ACCEPTED, |
| httplib.CREATED, |
| httplib.NO_CONTENT, |
| ] |
| |
| def parse_error(self): |
| if self.status == httplib.UNAUTHORIZED: |
| data = self.parse_body() |
| raise InvalidCredsError(data["code"] + ": " + data["message"]) |
| return self.body |
| |
| def success(self): |
| return self.status in self.valid_response_codes |
| |
| |
| class JoyentConnection(ConnectionUserAndKey): |
| """ |
| Joyent connection class. |
| """ |
| |
| responseCls = JoyentResponse |
| |
| allow_insecure = False |
| |
| def add_default_headers(self, headers): |
| headers["Accept"] = "application/json" |
| headers["Content-Type"] = "application/json; charset=UTF-8" |
| headers["X-Api-Version"] = API_VERSION |
| |
| user_b64 = base64.b64encode(b("{}:{}".format(self.user_id, self.key))) |
| headers["Authorization"] = "Basic %s" % (user_b64.decode("utf-8")) |
| return headers |
| |
| |
| class JoyentNodeDriver(NodeDriver): |
| """ |
| Joyent node driver class. |
| """ |
| |
| type = Provider.JOYENT |
| name = "Joyent" |
| website = "http://www.joyentcloud.com" |
| connectionCls = JoyentConnection |
| features = {"create_node": ["generates_password"]} |
| |
| def __init__( |
| self, |
| key, |
| secret=None, |
| secure=True, |
| host=None, |
| port=None, |
| region=DEFAULT_REGION, |
| **kwargs, |
| ): |
| # Location is here for backward compatibility reasons |
| if "location" in kwargs: |
| region = kwargs["location"] |
| |
| if region not in VALID_REGIONS: |
| msg = 'Invalid region: "%s". Valid region: %s' |
| raise LibcloudError(msg % (region, ", ".join(VALID_REGIONS)), driver=self) |
| |
| super().__init__( |
| key=key, |
| secret=secret, |
| secure=secure, |
| host=host, |
| port=port, |
| region=region, |
| **kwargs, |
| ) |
| self.connection.host = region + API_HOST_SUFFIX |
| |
| def list_images(self): |
| result = self.connection.request("/my/datasets").object |
| |
| images = [] |
| for value in result: |
| extra = { |
| "type": value["type"], |
| "urn": value["urn"], |
| "os": value["os"], |
| "default": value["default"], |
| } |
| image = NodeImage( |
| id=value["id"], |
| name=value["name"], |
| driver=self.connection.driver, |
| extra=extra, |
| ) |
| images.append(image) |
| |
| return images |
| |
| def list_sizes(self): |
| result = self.connection.request("/my/packages").object |
| |
| sizes = [] |
| for value in result: |
| size = NodeSize( |
| id=value["name"], |
| name=value["name"], |
| ram=value["memory"], |
| disk=value["disk"], |
| bandwidth=None, |
| price=0.0, |
| driver=self.connection.driver, |
| ) |
| sizes.append(size) |
| |
| return sizes |
| |
| def list_nodes(self): |
| result = self.connection.request("/my/machines").object |
| |
| nodes = [] |
| for value in result: |
| node = self._to_node(value) |
| nodes.append(node) |
| |
| return nodes |
| |
| def reboot_node(self, node): |
| data = json.dumps({"action": "reboot"}) |
| result = self.connection.request("/my/machines/%s" % (node.id), data=data, method="POST") |
| return result.status == httplib.ACCEPTED |
| |
| def destroy_node(self, node): |
| result = self.connection.request("/my/machines/%s" % (node.id), method="DELETE") |
| return result.status == httplib.NO_CONTENT |
| |
| def create_node(self, name, size, image): |
| data = json.dumps({"name": name, "package": size.id, "dataset": image.id}) |
| result = self.connection.request("/my/machines", data=data, method="POST") |
| return self._to_node(result.object) |
| |
| def start_node(self, node): |
| """ |
| Start node |
| |
| :param node: The node to be stopped |
| :type node: :class:`Node` |
| |
| :rtype: ``bool`` |
| """ |
| data = json.dumps({"action": "start"}) |
| result = self.connection.request("/my/machines/%s" % (node.id), data=data, method="POST") |
| return result.status == httplib.ACCEPTED |
| |
| def stop_node(self, node): |
| """ |
| Stop node |
| |
| :param node: The node to be stopped |
| :type node: :class:`Node` |
| |
| :rtype: ``bool`` |
| """ |
| data = json.dumps({"action": "stop"}) |
| result = self.connection.request("/my/machines/%s" % (node.id), data=data, method="POST") |
| return result.status == httplib.ACCEPTED |
| |
| 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_get_node(self, node_id): |
| """ |
| Return a Node object based on a node ID. |
| |
| :param node_id: ID of the node |
| :type node_id: ``str`` |
| |
| :return: A Node object for the node |
| :rtype: :class:`Node` |
| """ |
| result = self.connection.request("/my/machines/%s" % (node_id)) |
| return self._to_node(result.object) |
| |
| def _to_node(self, data): |
| state = NODE_STATE_MAP[data["state"]] |
| public_ips = [] |
| private_ips = [] |
| extra = {} |
| |
| for ip in data["ips"]: |
| if is_private_subnet(ip): |
| private_ips.append(ip) |
| else: |
| public_ips.append(ip) |
| |
| if "credentials" in data["metadata"]: |
| extra["password"] = data["metadata"]["credentials"]["root"] |
| |
| node = Node( |
| id=data["id"], |
| name=data["name"], |
| state=state, |
| public_ips=public_ips, |
| private_ips=private_ips, |
| driver=self.connection.driver, |
| extra=extra, |
| ) |
| return node |