| # 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. |
| |
| """ |
| NephoScale Cloud driver (http://www.nephoscale.com) |
| API documentation: http://docs.nephoscale.com |
| Created by Markos Gogoulos (https://mist.io) |
| """ |
| |
| import os |
| import time |
| import base64 |
| import binascii |
| |
| from libcloud.utils.py3 import b, httplib, urlencode |
| from libcloud.common.base import JsonResponse, ConnectionUserAndKey |
| from libcloud.compute.base import Node, NodeSize, NodeImage, NodeDriver, NodeLocation |
| from libcloud.compute.types import NodeState, LibcloudError, InvalidCredsError |
| from libcloud.utils.networking import is_private_subnet |
| from libcloud.compute.providers import Provider |
| |
| API_HOST = "api.nephoscale.com" |
| |
| NODE_STATE_MAP = { |
| "on": NodeState.RUNNING, |
| "off": NodeState.UNKNOWN, |
| "unknown": NodeState.UNKNOWN, |
| } |
| |
| VALID_RESPONSE_CODES = [ |
| httplib.OK, |
| httplib.ACCEPTED, |
| httplib.CREATED, |
| httplib.NO_CONTENT, |
| ] |
| |
| # used in create_node and specifies how many times to get the list of nodes and |
| # check if the newly created node is there. This is because when a request is |
| # sent to create a node, NephoScale replies with the job id, and not the node |
| # itself thus we don't have the ip addresses, that are required in deploy_node |
| CONNECT_ATTEMPTS = 10 |
| |
| |
| class NodeKey: |
| def __init__(self, id, name, public_key=None, key_group=None, password=None): |
| self.id = id |
| self.name = name |
| self.key_group = key_group |
| self.password = password |
| self.public_key = public_key |
| |
| def __repr__(self): |
| return ("<NodeKey: id=%s, name=%s>") % (self.id, self.name) |
| |
| |
| class NephoscaleResponse(JsonResponse): |
| """ |
| Nephoscale API Response |
| """ |
| |
| def parse_error(self): |
| if self.status == httplib.UNAUTHORIZED: |
| raise InvalidCredsError("Authorization Failed") |
| if self.status == httplib.NOT_FOUND: |
| raise Exception("The resource you are looking for is not found.") |
| |
| return self.body |
| |
| def success(self): |
| return self.status in VALID_RESPONSE_CODES |
| |
| |
| class NephoscaleConnection(ConnectionUserAndKey): |
| """ |
| Nephoscale connection class. |
| Authenticates to the API through Basic Authentication |
| with username/password |
| """ |
| |
| host = API_HOST |
| responseCls = NephoscaleResponse |
| |
| allow_insecure = False |
| |
| def add_default_headers(self, headers): |
| """ |
| Add parameters that are necessary for every request |
| """ |
| user_b64 = base64.b64encode(b("{}:{}".format(self.user_id, self.key))) |
| headers["Authorization"] = "Basic %s" % (user_b64.decode("utf-8")) |
| return headers |
| |
| |
| class NephoscaleNodeDriver(NodeDriver): |
| """ |
| Nephoscale node driver class. |
| |
| >>> from libcloud.compute.providers import get_driver |
| >>> driver = get_driver('nephoscale') |
| >>> conn = driver('nepho_user','nepho_password') |
| >>> conn.list_nodes() |
| """ |
| |
| type = Provider.NEPHOSCALE |
| api_name = "nephoscale" |
| name = "NephoScale" |
| website = "http://www.nephoscale.com" |
| connectionCls = NephoscaleConnection |
| features = {"create_node": ["ssh_key"]} |
| |
| def list_locations(self): |
| """ |
| List available zones for deployment |
| |
| :rtype: ``list`` of :class:`NodeLocation` |
| """ |
| result = self.connection.request("/datacenter/zone/").object |
| locations = [] |
| for value in result.get("data", []): |
| location = NodeLocation( |
| id=value.get("id"), name=value.get("name"), country="US", driver=self |
| ) |
| locations.append(location) |
| return locations |
| |
| def list_images(self): |
| """ |
| List available images for deployment |
| |
| :rtype: ``list`` of :class:`NodeImage` |
| """ |
| result = self.connection.request("/image/server/").object |
| images = [] |
| for value in result.get("data", []): |
| extra = { |
| "architecture": value.get("architecture"), |
| "disks": value.get("disks"), |
| "billable_type": value.get("billable_type"), |
| "pcpus": value.get("pcpus"), |
| "cores": value.get("cores"), |
| "uri": value.get("uri"), |
| "storage": value.get("storage"), |
| } |
| image = NodeImage( |
| id=value.get("id"), |
| name=value.get("friendly_name"), |
| driver=self, |
| extra=extra, |
| ) |
| images.append(image) |
| return images |
| |
| def list_sizes(self): |
| """ |
| List available sizes containing prices |
| |
| :rtype: ``list`` of :class:`NodeSize` |
| """ |
| result = self.connection.request("/server/type/cloud/").object |
| sizes = [] |
| for value in result.get("data", []): |
| value_id = value.get("id") |
| size = NodeSize( |
| id=value_id, |
| name=value.get("friendly_name"), |
| ram=value.get("ram"), |
| disk=value.get("storage"), |
| bandwidth=None, |
| price=self._get_size_price(size_id=str(value_id)), |
| driver=self, |
| ) |
| sizes.append(size) |
| |
| return sorted(sizes, key=lambda k: k.price) |
| |
| def list_nodes(self): |
| """ |
| List available nodes |
| |
| :rtype: ``list`` of :class:`Node` |
| """ |
| result = self.connection.request("/server/cloud/").object |
| nodes = [self._to_node(value) for value in result.get("data", [])] |
| return nodes |
| |
| def rename_node(self, node, name, hostname=None): |
| """rename a cloud server, optionally specify hostname too""" |
| data = {"name": name} |
| if hostname: |
| data["hostname"] = hostname |
| params = urlencode(data) |
| result = self.connection.request( |
| "/server/cloud/%s/" % node.id, data=params, method="PUT" |
| ).object |
| return result.get("response") in VALID_RESPONSE_CODES |
| |
| def reboot_node(self, node): |
| """reboot a running node""" |
| result = self.connection.request( |
| "/server/cloud/%s/initiator/restart/" % node.id, method="POST" |
| ).object |
| return result.get("response") in VALID_RESPONSE_CODES |
| |
| def start_node(self, node): |
| """start a stopped node""" |
| result = self.connection.request( |
| "/server/cloud/%s/initiator/start/" % node.id, method="POST" |
| ).object |
| return result.get("response") in VALID_RESPONSE_CODES |
| |
| def stop_node(self, node): |
| """stop a running node""" |
| result = self.connection.request( |
| "/server/cloud/%s/initiator/stop/" % node.id, method="POST" |
| ).object |
| return result.get("response") in VALID_RESPONSE_CODES |
| |
| def destroy_node(self, node): |
| """destroy a node""" |
| result = self.connection.request("/server/cloud/%s/" % node.id, method="DELETE").object |
| return result.get("response") in VALID_RESPONSE_CODES |
| |
| 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_list_keypairs(self, ssh=False, password=False, key_group=None): |
| """ |
| List available console and server keys |
| There are two types of keys for NephoScale, ssh and password keys. |
| If run without arguments, lists all keys. Otherwise list only |
| ssh keys, or only password keys. |
| Password keys with key_group 4 are console keys. When a server |
| is created, it has two keys, one password or ssh key, and |
| one password console key. |
| |
| :keyword ssh: if specified, show ssh keys only (optional) |
| :type ssh: ``bool`` |
| |
| :keyword password: if specified, show password keys only (optional) |
| :type password: ``bool`` |
| |
| :keyword key_group: if specified, show keys with this key_group only |
| eg key_group=4 for console password keys (optional) |
| :type key_group: ``int`` |
| |
| :rtype: ``list`` of :class:`NodeKey` |
| """ |
| if ssh and password: |
| raise LibcloudError( |
| "You can only supply ssh or password. To \ |
| get all keys call with no arguments" |
| ) |
| if ssh: |
| result = self.connection.request("/key/sshrsa/").object |
| elif password: |
| result = self.connection.request("/key/password/").object |
| else: |
| result = self.connection.request("/key/").object |
| keys = [self._to_key(value) for value in result.get("data", [])] |
| |
| if key_group: |
| keys = [key for key in keys if key.key_group == key_group] |
| return keys |
| |
| def ex_create_keypair(self, name, public_key=None, password=None, key_group=None): |
| """Creates a key, ssh or password, for server or console |
| The group for the key (key_group) is 1 for Server and 4 for Console |
| Returns the id of the created key |
| """ |
| if public_key: |
| if not key_group: |
| key_group = 1 |
| data = {"name": name, "public_key": public_key, "key_group": key_group} |
| params = urlencode(data) |
| result = self.connection.request("/key/sshrsa/", data=params, method="POST").object |
| else: |
| if not key_group: |
| key_group = 4 |
| if not password: |
| password = self.random_password() |
| data = {"name": name, "password": password, "key_group": key_group} |
| params = urlencode(data) |
| result = self.connection.request("/key/password/", data=params, method="POST").object |
| return result.get("data", {}).get("id", "") |
| |
| def ex_delete_keypair(self, key_id, ssh=False): |
| """Delete an ssh key or password given it's id""" |
| if ssh: |
| result = self.connection.request("/key/sshrsa/%s/" % key_id, method="DELETE").object |
| else: |
| result = self.connection.request("/key/password/%s/" % key_id, method="DELETE").object |
| return result.get("response") in VALID_RESPONSE_CODES |
| |
| def create_node( |
| self, name, size, image, server_key=None, console_key=None, zone=None, **kwargs |
| ): |
| """Creates the node, and sets the ssh key, console key |
| NephoScale will respond with a 200-200 response after sending a valid |
| request. If nowait=True is specified in the args, we then ask a few |
| times until the server is created and assigned a public IP address, |
| so that deploy_node can be run |
| |
| >>> from libcloud.compute.providers import get_driver |
| >>> driver = get_driver('nephoscale') |
| >>> conn = driver('nepho_user','nepho_password') |
| >>> conn.list_nodes() |
| >>> name = 'staging-server' |
| >>> size = conn.list_sizes()[0] |
| <NodeSize: id=27, ...name=CS025 - 0.25GB, 10GB, ...> |
| >>> image = conn.list_images()[9] |
| <NodeImage: id=49, name=Linux Ubuntu Server 10.04 LTS 64-bit, ...> |
| >>> server_keys = conn.ex_list_keypairs(key_group=1)[0] |
| <NodeKey: id=71211, name=markos> |
| >>> server_key = conn.ex_list_keypairs(key_group=1)[0].id |
| 70867 |
| >>> console_keys = conn.ex_list_keypairs(key_group=4)[0] |
| <NodeKey: id=71213, name=mistio28434> |
| >>> console_key = conn.ex_list_keypairs(key_group=4)[0].id |
| 70907 |
| >>> node = conn.create_node(name=name, size=size, image=image, \ |
| console_key=console_key, server_key=server_key) |
| |
| We can also create an ssh key, plus a console key and |
| deploy node with them |
| >>> server_key = conn.ex_create_keypair(name, public_key='123') |
| 71211 |
| >>> console_key = conn.ex_create_keypair(name, key_group=4) |
| 71213 |
| |
| We can increase the number of connect attempts to wait until |
| the node is created, so that deploy_node has ip address to |
| deploy the script |
| We can also specify the location |
| >>> location = conn.list_locations()[0] |
| >>> node = conn.create_node(name=name, |
| >>> ... size=size, |
| >>> ... image=image, |
| >>> ... console_key=console_key, |
| >>> ... server_key=server_key, |
| >>> ... connect_attempts=10, |
| >>> ... nowait=True, |
| >>> ... zone=location.id) |
| """ |
| hostname = kwargs.get("hostname", name) |
| service_type = size.id |
| image = image.id |
| connect_attempts = int(kwargs.get("connect_attempts", CONNECT_ATTEMPTS)) |
| |
| data = { |
| "name": name, |
| "hostname": hostname, |
| "service_type": service_type, |
| "image": image, |
| "server_key": server_key, |
| "console_key": console_key, |
| "zone": zone, |
| } |
| |
| params = urlencode(data) |
| try: |
| node = self.connection.request("/server/cloud/", data=params, method="POST") |
| except Exception as e: |
| raise Exception("Failed to create node %s" % e) |
| node = Node( |
| id="", |
| name=name, |
| state=NodeState.UNKNOWN, |
| public_ips=[], |
| private_ips=[], |
| driver=self, |
| ) |
| |
| nowait = kwargs.get("ex_wait", False) |
| if not nowait: |
| return node |
| else: |
| # try to get the created node public ips, for use in deploy_node |
| # At this point we don't have the id of the newly created Node, |
| # so search name in nodes |
| created_node = False |
| while connect_attempts > 0: |
| nodes = self.list_nodes() |
| created_node = [c_node for c_node in nodes if c_node.name == name] |
| if created_node: |
| return created_node[0] |
| else: |
| time.sleep(60) |
| connect_attempts = connect_attempts - 1 |
| return node |
| |
| def _to_node(self, data): |
| """Convert node in Node instances""" |
| |
| state = NODE_STATE_MAP.get(data.get("power_status"), "4") |
| public_ips = [] |
| private_ips = [] |
| ip_addresses = data.get("ipaddresses", "") |
| # E.g. "ipaddresses": "198.120.14.6, 10.132.60.1" |
| if ip_addresses: |
| for ip in ip_addresses.split(","): |
| ip = ip.replace(" ", "") |
| if is_private_subnet(ip): |
| private_ips.append(ip) |
| else: |
| public_ips.append(ip) |
| extra = { |
| "zone_data": data.get("zone"), |
| "zone": data.get("zone", {}).get("name"), |
| "image": data.get("image", {}).get("friendly_name"), |
| "create_time": data.get("create_time"), |
| "network_ports": data.get("network_ports"), |
| "is_console_enabled": data.get("is_console_enabled"), |
| "service_type": data.get("service_type", {}).get("friendly_name"), |
| "hostname": data.get("hostname"), |
| } |
| |
| node = Node( |
| id=data.get("id"), |
| name=data.get("name"), |
| state=state, |
| public_ips=public_ips, |
| private_ips=private_ips, |
| driver=self, |
| extra=extra, |
| ) |
| return node |
| |
| def _to_key(self, data): |
| return NodeKey( |
| id=data.get("id"), |
| name=data.get("name"), |
| password=data.get("password"), |
| key_group=data.get("key_group"), |
| public_key=data.get("public_key"), |
| ) |
| |
| def random_password(self, size=8): |
| value = os.urandom(size) |
| password = binascii.hexlify(value).decode("ascii") |
| return password[:size] |