| # 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. |
| """ |
| Softlayer driver |
| """ |
| |
| import time |
| |
| from libcloud.compute.base import Node, KeyPair, NodeSize, NodeImage, NodeDriver, NodeLocation |
| from libcloud.compute.types import Provider, NodeState, KeyPairDoesNotExistError |
| from libcloud.common.softlayer import SoftLayerException, SoftLayerConnection |
| |
| try: |
| from cryptography.hazmat.backends import default_backend |
| from cryptography.hazmat.primitives import serialization |
| from cryptography.hazmat.primitives.asymmetric import rsa |
| |
| crypto = True |
| except ImportError: |
| crypto = False |
| |
| |
| DEFAULT_DOMAIN = "example.com" |
| DEFAULT_CPU_SIZE = 1 |
| DEFAULT_RAM_SIZE = 2048 |
| DEFAULT_DISK_SIZE = 100 |
| |
| DATACENTERS = { |
| "hou02": {"country": "US"}, |
| "sea01": {"country": "US", "name": "Seattle - West Coast U.S."}, |
| "wdc01": {"country": "US", "name": "Washington, DC - East Coast U.S."}, |
| "dal01": {"country": "US"}, |
| "dal02": {"country": "US"}, |
| "dal04": {"country": "US"}, |
| "dal05": {"country": "US", "name": "Dallas - Central U.S."}, |
| "dal06": {"country": "US"}, |
| "dal07": {"country": "US"}, |
| "sjc01": {"country": "US", "name": "San Jose - West Coast U.S."}, |
| "sng01": {"country": "SG", "name": "Singapore - Southeast Asia"}, |
| "ams01": {"country": "NL", "name": "Amsterdam - Western Europe"}, |
| "tok02": {"country": "JP", "name": "Tokyo - Japan"}, |
| } |
| |
| NODE_STATE_MAP = { |
| "RUNNING": NodeState.RUNNING, |
| "HALTED": NodeState.UNKNOWN, |
| "PAUSED": NodeState.UNKNOWN, |
| "INITIATING": NodeState.PENDING, |
| } |
| |
| SL_BASE_TEMPLATES = [ |
| {"name": "1 CPU, 1GB ram, 25GB", "ram": 1024, "disk": 25, "cpus": 1}, |
| {"name": "1 CPU, 1GB ram, 100GB", "ram": 1024, "disk": 100, "cpus": 1}, |
| {"name": "1 CPU, 2GB ram, 100GB", "ram": 2 * 1024, "disk": 100, "cpus": 1}, |
| {"name": "1 CPU, 4GB ram, 100GB", "ram": 4 * 1024, "disk": 100, "cpus": 1}, |
| {"name": "2 CPU, 2GB ram, 100GB", "ram": 2 * 1024, "disk": 100, "cpus": 2}, |
| {"name": "2 CPU, 4GB ram, 100GB", "ram": 4 * 1024, "disk": 100, "cpus": 2}, |
| {"name": "2 CPU, 8GB ram, 100GB", "ram": 8 * 1024, "disk": 100, "cpus": 2}, |
| {"name": "4 CPU, 4GB ram, 100GB", "ram": 4 * 1024, "disk": 100, "cpus": 4}, |
| {"name": "4 CPU, 8GB ram, 100GB", "ram": 8 * 1024, "disk": 100, "cpus": 4}, |
| {"name": "6 CPU, 4GB ram, 100GB", "ram": 4 * 1024, "disk": 100, "cpus": 6}, |
| {"name": "6 CPU, 8GB ram, 100GB", "ram": 8 * 1024, "disk": 100, "cpus": 6}, |
| {"name": "8 CPU, 8GB ram, 100GB", "ram": 8 * 1024, "disk": 100, "cpus": 8}, |
| {"name": "8 CPU, 16GB ram, 100GB", "ram": 16 * 1024, "disk": 100, "cpus": 8}, |
| ] |
| |
| SL_TEMPLATES = {} |
| for i, template in enumerate(SL_BASE_TEMPLATES): |
| # Add local disk templates |
| local = template.copy() |
| local["local_disk"] = True |
| SL_TEMPLATES[i] = local |
| |
| |
| class SoftLayerNodeDriver(NodeDriver): |
| """ |
| SoftLayer node driver |
| |
| Extra node attributes: |
| - password: root password |
| - hourlyRecurringFee: hourly price (if applicable) |
| - recurringFee : flat rate (if applicable) |
| - recurringMonths : The number of months in which the recurringFee |
| will be incurred. |
| """ |
| |
| connectionCls = SoftLayerConnection |
| name = "SoftLayer" |
| website = "http://www.softlayer.com/" |
| type = Provider.SOFTLAYER |
| |
| features = {"create_node": ["generates_password", "ssh_key"]} |
| api_name = "softlayer" |
| |
| def _to_node(self, host): |
| try: |
| password = host["operatingSystem"]["passwords"][0]["password"] |
| except (IndexError, KeyError): |
| password = None |
| |
| hourlyRecurringFee = host.get("billingItem", {}).get("hourlyRecurringFee", 0) |
| recurringFee = host.get("billingItem", {}).get("recurringFee", 0) |
| recurringMonths = host.get("billingItem", {}).get("recurringMonths", 0) |
| createDate = host.get("createDate", None) |
| |
| # When machine is launching it gets state halted |
| # we change this to pending |
| state = NODE_STATE_MAP.get(host["powerState"]["keyName"], NodeState.UNKNOWN) |
| |
| if not password and state == NodeState.UNKNOWN: |
| state = NODE_STATE_MAP["INITIATING"] |
| |
| public_ips = [] |
| private_ips = [] |
| |
| if "primaryIpAddress" in host: |
| public_ips.append(host["primaryIpAddress"]) |
| |
| if "primaryBackendIpAddress" in host: |
| private_ips.append(host["primaryBackendIpAddress"]) |
| |
| image = ( |
| host.get("operatingSystem", {}) |
| .get("softwareLicense", {}) |
| .get("softwareDescription", {}) |
| .get("longDescription", None) |
| ) |
| |
| return Node( |
| id=host["id"], |
| name=host["fullyQualifiedDomainName"], |
| state=state, |
| public_ips=public_ips, |
| private_ips=private_ips, |
| driver=self, |
| extra={ |
| "hostname": host["hostname"], |
| "fullyQualifiedDomainName": host["fullyQualifiedDomainName"], |
| "password": password, |
| "maxCpu": host.get("maxCpu", None), |
| "datacenter": host.get("datacenter", {}).get("longName", None), |
| "maxMemory": host.get("maxMemory", None), |
| "image": image, |
| "hourlyRecurringFee": hourlyRecurringFee, |
| "recurringFee": recurringFee, |
| "recurringMonths": recurringMonths, |
| "created": createDate, |
| }, |
| ) |
| |
| def destroy_node(self, node): |
| self.connection.request("SoftLayer_Virtual_Guest", "deleteObject", id=node.id) |
| return True |
| |
| def reboot_node(self, node): |
| self.connection.request("SoftLayer_Virtual_Guest", "rebootSoft", id=node.id) |
| return True |
| |
| def start_node(self, node): |
| self.connection.request("SoftLayer_Virtual_Guest", "powerOn", id=node.id) |
| return True |
| |
| def stop_node(self, node): |
| self.connection.request("SoftLayer_Virtual_Guest", "powerOff", id=node.id) |
| return True |
| |
| 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 _get_order_information(self, node_id, timeout=1200, check_interval=5): |
| mask = { |
| "billingItem": "", |
| "powerState": "", |
| "operatingSystem": {"passwords": ""}, |
| "provisionDate": "", |
| } |
| |
| for i in range(0, timeout, check_interval): |
| res = self.connection.request( |
| "SoftLayer_Virtual_Guest", "getObject", id=node_id, object_mask=mask |
| ).object |
| |
| if res.get("provisionDate", None): |
| return res |
| |
| time.sleep(check_interval) |
| |
| raise SoftLayerException("Timeout on getting node details") |
| |
| def create_node( |
| self, |
| name, |
| size=None, |
| image=None, |
| location=None, |
| ex_domain=None, |
| ex_cpus=None, |
| ex_disk=None, |
| ex_ram=None, |
| ex_bandwidth=None, |
| ex_local_disk=None, |
| ex_datacenter=None, |
| ex_os=None, |
| ex_keyname=None, |
| ex_hourly=True, |
| ): |
| """Create a new SoftLayer node |
| |
| @inherits: :class:`NodeDriver.create_node` |
| |
| :keyword ex_domain: e.g. libcloud.org |
| :type ex_domain: ``str`` |
| :keyword ex_cpus: e.g. 2 |
| :type ex_cpus: ``int`` |
| :keyword ex_disk: e.g. 100 |
| :type ex_disk: ``int`` |
| :keyword ex_ram: e.g. 2048 |
| :type ex_ram: ``int`` |
| :keyword ex_bandwidth: e.g. 100 |
| :type ex_bandwidth: ``int`` |
| :keyword ex_local_disk: e.g. True |
| :type ex_local_disk: ``bool`` |
| :keyword ex_datacenter: e.g. Dal05 |
| :type ex_datacenter: ``str`` |
| :keyword ex_os: e.g. UBUNTU_LATEST |
| :type ex_os: ``str`` |
| :keyword ex_keyname: The name of the key pair |
| :type ex_keyname: ``str`` |
| """ |
| os = "DEBIAN_LATEST" |
| if ex_os: |
| os = ex_os |
| elif image: |
| os = image.id |
| |
| size = size or NodeSize( |
| id=123, |
| name="Custom", |
| ram=None, |
| disk=None, |
| bandwidth=None, |
| price=None, |
| driver=self.connection.driver, |
| ) |
| ex_size_data = SL_TEMPLATES.get(int(size.id)) or {} |
| # plan keys are ints |
| cpu_count = ex_cpus or ex_size_data.get("cpus") or DEFAULT_CPU_SIZE |
| ram = ex_ram or ex_size_data.get("ram") or DEFAULT_RAM_SIZE |
| bandwidth = ex_bandwidth or size.bandwidth or 10 |
| hourly = ex_hourly |
| |
| local_disk = "true" |
| if ex_size_data.get("local_disk") is False: |
| local_disk = "false" |
| |
| if ex_local_disk is False: |
| local_disk = "false" |
| |
| disk_size = DEFAULT_DISK_SIZE |
| if size.disk: |
| disk_size = size.disk |
| if ex_disk: |
| disk_size = ex_disk |
| |
| datacenter = "" |
| if ex_datacenter: |
| datacenter = ex_datacenter |
| elif location: |
| datacenter = location.id |
| |
| domain = ex_domain |
| if domain is None: |
| if name.find(".") != -1: |
| domain = name[name.find(".") + 1 :] |
| if domain is None: |
| # TODO: domain is a required argument for the Sofylayer API, but it |
| # it shouldn't be. |
| domain = DEFAULT_DOMAIN |
| |
| newCCI = { |
| "hostname": name, |
| "domain": domain, |
| "startCpus": cpu_count, |
| "maxMemory": ram, |
| "networkComponents": [{"maxSpeed": bandwidth}], |
| "hourlyBillingFlag": hourly, |
| "operatingSystemReferenceCode": os, |
| "localDiskFlag": local_disk, |
| "blockDevices": [{"device": "0", "diskImage": {"capacity": disk_size}}], |
| } |
| |
| if datacenter: |
| newCCI["datacenter"] = {"name": datacenter} |
| |
| if ex_keyname: |
| newCCI["sshKeys"] = [{"id": self._key_name_to_id(ex_keyname)}] |
| |
| res = self.connection.request("SoftLayer_Virtual_Guest", "createObject", newCCI).object |
| |
| node_id = res["id"] |
| raw_node = self._get_order_information(node_id) |
| |
| return self._to_node(raw_node) |
| |
| def list_key_pairs(self): |
| result = self.connection.request("SoftLayer_Account", "getSshKeys").object |
| elems = [x for x in result] |
| key_pairs = self._to_key_pairs(elems=elems) |
| return key_pairs |
| |
| def get_key_pair(self, name): |
| key_id = self._key_name_to_id(name=name) |
| result = self.connection.request( |
| "SoftLayer_Security_Ssh_Key", "getObject", id=key_id |
| ).object |
| return self._to_key_pair(result) |
| |
| # TODO: Check this with the libcloud guys, |
| # can we create new dependencies? |
| def create_key_pair(self, name, ex_size=4096): |
| if crypto is False: |
| raise NotImplementedError("create_key_pair needs" "the cryptography library") |
| key = rsa.generate_private_key( |
| public_exponent=65537, key_size=4096, backend=default_backend() |
| ) |
| public_key = key.public_key().public_bytes( |
| encoding=serialization.Encoding.OpenSSH, |
| format=serialization.PublicFormat.OpenSSH, |
| ) |
| new_key = { |
| "key": public_key, |
| "label": name, |
| "notes": "", |
| } |
| result = self.connection.request( |
| "SoftLayer_Security_Ssh_Key", "createObject", new_key |
| ).object |
| result["private"] = key.private_bytes( |
| encoding=serialization.Encoding.PEM, |
| format=serialization.PrivateFormat.TraditionalOpenSSL, |
| encryption_algorithm=serialization.NoEncryption(), |
| ) |
| return self._to_key_pair(result) |
| |
| def import_key_pair_from_string(self, name, key_material): |
| new_key = { |
| "key": key_material, |
| "label": name, |
| "notes": "", |
| } |
| result = self.connection.request( |
| "SoftLayer_Security_Ssh_Key", "createObject", new_key |
| ).object |
| |
| key_pair = self._to_key_pair(result) |
| return key_pair |
| |
| def delete_key_pair(self, key_pair): |
| key = self._key_name_to_id(key_pair) |
| result = self.connection.request( |
| "SoftLayer_Security_Ssh_Key", "deleteObject", id=key |
| ).object |
| return result |
| |
| def _to_image(self, img): |
| return NodeImage( |
| id=img["template"]["operatingSystemReferenceCode"], |
| name=img["itemPrice"]["item"]["description"], |
| driver=self.connection.driver, |
| ) |
| |
| def list_images(self, location=None): |
| result = self.connection.request("SoftLayer_Virtual_Guest", "getCreateObjectOptions").object |
| return [self._to_image(i) for i in result["operatingSystems"]] |
| |
| def get_image(self, image_id): |
| """ |
| Gets an image based on an image_id. |
| |
| :param image_id: Image identifier |
| :type image_id: ``str`` |
| |
| :return: A NodeImage object |
| :rtype: :class:`NodeImage` |
| |
| """ |
| images = self.list_images() |
| images = [image for image in images if image.id == image_id] |
| if len(images) < 1: |
| raise SoftLayerException("could not find the image with id %s" % image_id) |
| image = images[0] |
| return image |
| |
| def _to_size(self, id, size): |
| return NodeSize( |
| id=id, |
| name=size["name"], |
| ram=size["ram"], |
| disk=size["disk"], |
| bandwidth=size.get("bandwidth"), |
| price=self._get_size_price(str(id)), |
| driver=self.connection.driver, |
| ) |
| |
| def list_sizes(self, location=None): |
| return [self._to_size(id, s) for id, s in SL_TEMPLATES.items()] |
| |
| def _to_loc(self, loc): |
| country = "UNKNOWN" |
| loc_id = loc["template"]["datacenter"]["name"] |
| name = loc_id |
| |
| if loc_id in DATACENTERS: |
| country = DATACENTERS[loc_id]["country"] |
| name = DATACENTERS[loc_id].get("name", loc_id) |
| return NodeLocation(id=loc_id, name=name, country=country, driver=self) |
| |
| def list_locations(self): |
| res = self.connection.request("SoftLayer_Virtual_Guest", "getCreateObjectOptions").object |
| return [self._to_loc(loc) for loc in res["datacenters"]] |
| |
| def list_nodes(self): |
| mask = { |
| "virtualGuests": { |
| "powerState": "", |
| "hostname": "", |
| "maxMemory": "", |
| "datacenter": "", |
| "operatingSystem": {"passwords": ""}, |
| "billingItem": "", |
| }, |
| } |
| res = self.connection.request( |
| "SoftLayer_Account", "getVirtualGuests", object_mask=mask |
| ).object |
| return [self._to_node(h) for h in res] |
| |
| def _to_key_pairs(self, elems): |
| key_pairs = [self._to_key_pair(elem=elem) for elem in elems] |
| return key_pairs |
| |
| def _to_key_pair(self, elem): |
| key_pair = KeyPair( |
| name=elem["label"], |
| public_key=elem["key"], |
| fingerprint=elem["fingerprint"], |
| private_key=elem.get("private", None), |
| driver=self, |
| extra={"id": elem["id"]}, |
| ) |
| return key_pair |
| |
| def _key_name_to_id(self, name): |
| result = self.connection.request("SoftLayer_Account", "getSshKeys").object |
| key_id = [x for x in result if x["label"] == name] |
| if len(key_id) == 0: |
| raise KeyPairDoesNotExistError(name, self) |
| else: |
| return int(key_id[0]["id"]) |