554 lines
18 KiB
Python
554 lines
18 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.
|
|
"""
|
|
Softlayer driver
|
|
"""
|
|
|
|
import time
|
|
try:
|
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
from cryptography.hazmat.backends import default_backend
|
|
from cryptography.hazmat.primitives import serialization
|
|
crypto = True
|
|
except ImportError:
|
|
crypto = False
|
|
|
|
from libcloud.common.softlayer import SoftLayerConnection, SoftLayerException
|
|
from libcloud.compute.types import Provider, NodeState
|
|
from libcloud.compute.base import NodeDriver, Node, NodeLocation, NodeSize, \
|
|
NodeImage, KeyPair
|
|
from libcloud.compute.types import KeyPairDoesNotExistError
|
|
|
|
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'])
|