blob: 18b581bf61b5b74a7ccf115fbb79cbf099d05880 [file] [log] [blame]
# 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.
"""
Base driver for the providers based on the ElasticStack platform -
http://www.elasticstack.com.
"""
import re
import time
import base64
from libcloud.utils.py3 import httplib
from libcloud.utils.py3 import b
try:
import simplejson as json
except ImportError:
import json
from libcloud.common.base import ConnectionUserAndKey, JsonResponse
from libcloud.common.types import InvalidCredsError
from libcloud.compute.types import NodeState
from libcloud.compute.base import NodeDriver, NodeSize, Node
from libcloud.compute.base import NodeImage
from libcloud.compute.deployment import ScriptDeployment, SSHKeyDeployment
from libcloud.compute.deployment import MultiStepDeployment
NODE_STATE_MAP = {
'active': NodeState.RUNNING,
'dead': NodeState.TERMINATED,
'dumped': NodeState.TERMINATED,
}
# Default timeout (in seconds) for the drive imaging process
IMAGING_TIMEOUT = 10 * 60
# ElasticStack doesn't specify special instance types, so I just specified
# some plans based on the other provider offerings.
#
# Basically for CPU any value between 500Mhz and 20000Mhz should work,
# 256MB to 8192MB for ram and 1GB to 2TB for disk.
INSTANCE_TYPES = {
'small': {
'id': 'small',
'name': 'Small instance',
'cpu': 2000,
'memory': 1700,
'disk': 160,
'bandwidth': None,
},
'medium': {
'id': 'medium',
'name': 'Medium instance',
'cpu': 3000,
'memory': 4096,
'disk': 500,
'bandwidth': None,
},
'large': {
'id': 'large',
'name': 'Large instance',
'cpu': 4000,
'memory': 7680,
'disk': 850,
'bandwidth': None,
},
'extra-large': {
'id': 'extra-large',
'name': 'Extra Large instance',
'cpu': 8000,
'memory': 8192,
'disk': 1690,
'bandwidth': None,
},
'high-cpu-medium': {
'id': 'high-cpu-medium',
'name': 'High-CPU Medium instance',
'cpu': 5000,
'memory': 1700,
'disk': 350,
'bandwidth': None,
},
'high-cpu-extra-large': {
'id': 'high-cpu-extra-large',
'name': 'High-CPU Extra Large instance',
'cpu': 20000,
'memory': 7168,
'disk': 1690,
'bandwidth': None,
},
}
class ElasticStackException(Exception):
def __str__(self):
return self.args[0]
def __repr__(self):
return "<ElasticStackException '%s'>" % (self.args[0])
class ElasticStackResponse(JsonResponse):
def success(self):
if self.status == 401:
raise InvalidCredsError()
return self.status >= 200 and self.status <= 299
def parse_error(self):
error_header = self.headers.get('x-elastic-error', '')
return 'X-Elastic-Error: %s (%s)' % (error_header, self.body.strip())
class ElasticStackNodeSize(NodeSize):
def __init__(self, id, name, cpu, ram, disk, bandwidth, price, driver):
self.id = id
self.name = name
self.cpu = cpu
self.ram = ram
self.disk = disk
self.bandwidth = bandwidth
self.price = price
self.driver = driver
def __repr__(self):
return (('<NodeSize: id=%s, name=%s, cpu=%s, ram=%s '
'disk=%s bandwidth=%s price=%s driver=%s ...>')
% (self.id, self.name, self.cpu, self.ram,
self.disk, self.bandwidth, self.price, self.driver.name))
class ElasticStackBaseConnection(ConnectionUserAndKey):
"""
Base connection class for the ElasticStack driver
"""
host = None
responseCls = ElasticStackResponse
def add_default_headers(self, headers):
headers['Accept'] = 'application/json'
headers['Content-Type'] = 'application/json'
headers['Authorization'] = \
('Basic %s' % (base64.b64encode(b('%s:%s' % (self.user_id,
self.key))))
.decode('utf-8'))
return headers
class ElasticStackBaseNodeDriver(NodeDriver):
website = 'http://www.elasticstack.com'
connectionCls = ElasticStackBaseConnection
features = {"create_node": ["generates_password"]}
def reboot_node(self, node):
# Reboots the node
response = self.connection.request(
action='/servers/%s/reset' % (node.id),
method='POST'
)
return response.status == 204
def destroy_node(self, node):
# Kills the server immediately
response = self.connection.request(
action='/servers/%s/destroy' % (node.id),
method='POST'
)
return response.status == 204
def list_images(self, location=None):
# Returns a list of available pre-installed system drive images
images = []
for key, value in self._standard_drives.items():
image = NodeImage(
id=value['uuid'],
name=value['description'],
driver=self.connection.driver,
extra={
'size_gunzipped': value['size_gunzipped']
}
)
images.append(image)
return images
def list_sizes(self, location=None):
sizes = []
for key, value in INSTANCE_TYPES.items():
size = ElasticStackNodeSize(
id=value['id'],
name=value['name'], cpu=value['cpu'], ram=value['memory'],
disk=value['disk'], bandwidth=value['bandwidth'],
price=self._get_size_price(size_id=value['id']),
driver=self.connection.driver
)
sizes.append(size)
return sizes
def list_nodes(self):
# Returns a list of active (running) nodes
response = self.connection.request(action='/servers/info').object
nodes = []
for data in response:
node = self._to_node(data)
nodes.append(node)
return nodes
def create_node(self, **kwargs):
"""Creates an ElasticStack instance
@inherits: :class:`NodeDriver.create_node`
:keyword name: String with a name for this new node (required)
:type name: ``str``
:keyword smp: Number of virtual processors or None to calculate
based on the cpu speed
:type smp: ``int``
:keyword nic_model: e1000, rtl8139 or virtio
(if not specified, e1000 is used)
:type nic_model: ``str``
:keyword vnc_password: If set, the same password is also used for
SSH access with user toor,
otherwise VNC access is disabled and
no SSH login is possible.
:type vnc_password: ``str``
"""
size = kwargs['size']
image = kwargs['image']
smp = kwargs.get('smp', 'auto')
nic_model = kwargs.get('nic_model', 'e1000')
vnc_password = ssh_password = kwargs.get('vnc_password', None)
if nic_model not in ('e1000', 'rtl8139', 'virtio'):
raise ElasticStackException('Invalid NIC model specified')
# check that drive size is not smaller than pre installed image size
# First we create a drive with the specified size
drive_data = {}
drive_data.update({'name': kwargs['name'],
'size': '%sG' % (kwargs['size'].disk)})
response = self.connection.request(action='/drives/create',
data=json.dumps(drive_data),
method='POST').object
if not response:
raise ElasticStackException('Drive creation failed')
drive_uuid = response['drive']
# Then we image the selected pre-installed system drive onto it
response = self.connection.request(
action='/drives/%s/image/%s/gunzip' % (drive_uuid, image.id),
method='POST'
)
if response.status not in (200, 204):
raise ElasticStackException('Drive imaging failed')
# We wait until the drive is imaged and then boot up the node
# (in most cases, the imaging process shouldn't take longer
# than a few minutes)
response = self.connection.request(
action='/drives/%s/info' % (drive_uuid)
).object
imaging_start = time.time()
while 'imaging' in response:
response = self.connection.request(
action='/drives/%s/info' % (drive_uuid)
).object
elapsed_time = time.time() - imaging_start
if ('imaging' in response and elapsed_time >= IMAGING_TIMEOUT):
raise ElasticStackException('Drive imaging timed out')
time.sleep(1)
node_data = {}
node_data.update({'name': kwargs['name'],
'cpu': size.cpu,
'mem': size.ram,
'ide:0:0': drive_uuid,
'boot': 'ide:0:0',
'smp': smp})
node_data.update({'nic:0:model': nic_model, 'nic:0:dhcp': 'auto'})
if vnc_password:
node_data.update({'vnc': 'auto', 'vnc:password': vnc_password})
response = self.connection.request(
action='/servers/create', data=json.dumps(node_data),
method='POST'
).object
if isinstance(response, list):
nodes = [self._to_node(node, ssh_password) for node in response]
else:
nodes = self._to_node(response, ssh_password)
return nodes
# Extension methods
def ex_set_node_configuration(self, node, **kwargs):
"""
Changes the configuration of the running server
:param node: Node which should be used
:type node: :class:`Node`
:param kwargs: keyword arguments
:type kwargs: ``dict``
:rtype: ``bool``
"""
valid_keys = ('^name$', '^parent$', '^cpu$', '^smp$', '^mem$',
'^boot$', '^nic:0:model$', '^nic:0:dhcp',
'^nic:1:model$', '^nic:1:vlan$', '^nic:1:mac$',
'^vnc:ip$', '^vnc:password$', '^vnc:tls',
'^ide:[0-1]:[0-1](:media)?$',
'^scsi:0:[0-7](:media)?$', '^block:[0-7](:media)?$')
invalid_keys = []
keys = list(kwargs.keys())
for key in keys:
matches = False
for regex in valid_keys:
if re.match(regex, key):
matches = True
break
if not matches:
invalid_keys.append(key)
if invalid_keys:
raise ElasticStackException(
'Invalid configuration key specified: %s'
% (',' .join(invalid_keys))
)
response = self.connection.request(
action='/servers/%s/set' % (node.id), data=json.dumps(kwargs),
method='POST'
)
return (response.status == httplib.OK and response.body != '')
def deploy_node(self, **kwargs):
"""
Create a new node, and start deployment.
@inherits: :class:`NodeDriver.deploy_node`
:keyword enable_root: If true, root password will be set to
vnc_password (this will enable SSH access)
and default 'toor' account will be deleted.
:type enable_root: ``bool``
"""
image = kwargs['image']
vnc_password = kwargs.get('vnc_password', None)
enable_root = kwargs.get('enable_root', False)
if not vnc_password:
raise ValueError('You need to provide vnc_password argument '
'if you want to use deployment')
if (image in self._standard_drives and
not self._standard_drives[image]['supports_deployment']):
raise ValueError('Image %s does not support deployment'
% (image.id))
if enable_root:
script = ("unset HISTFILE;"
"echo root:%s | chpasswd;"
"sed -i '/^toor.*$/d' /etc/passwd /etc/shadow;"
"history -c") % vnc_password
root_enable_script = ScriptDeployment(script=script,
delete=True)
deploy = kwargs.get('deploy', None)
if deploy:
if (isinstance(deploy, ScriptDeployment) or
isinstance(deploy, SSHKeyDeployment)):
deployment = MultiStepDeployment([deploy,
root_enable_script])
elif isinstance(deploy, MultiStepDeployment):
deployment = deploy
deployment.add(root_enable_script)
else:
deployment = root_enable_script
kwargs['deploy'] = deployment
if not kwargs.get('ssh_username', None):
kwargs['ssh_username'] = 'toor'
return super(ElasticStackBaseNodeDriver, self).deploy_node(**kwargs)
def ex_shutdown_node(self, node):
"""
Sends the ACPI power-down event
:param node: Node which should be used
:type node: :class:`Node`
:rtype: ``bool``
"""
response = self.connection.request(
action='/servers/%s/shutdown' % (node.id),
method='POST'
)
return response.status == 204
def ex_destroy_drive(self, drive_uuid):
"""
Deletes a drive
:param drive_uuid: Drive uuid which should be used
:type drive_uuid: ``str``
:rtype: ``bool``
"""
response = self.connection.request(
action='/drives/%s/destroy' % (drive_uuid),
method='POST'
)
return response.status == 204
# Helper methods
def _to_node(self, data, ssh_password=None):
try:
state = NODE_STATE_MAP[data['status']]
except KeyError:
state = NodeState.UNKNOWN
if 'nic:0:dhcp:ip' in data:
if isinstance(data['nic:0:dhcp:ip'], list):
public_ip = data['nic:0:dhcp:ip']
else:
public_ip = [data['nic:0:dhcp:ip']]
else:
public_ip = []
extra = {'cpu': data['cpu'],
'mem': data['mem']}
if 'started' in data:
extra['started'] = data['started']
if 'smp' in data:
extra['smp'] = data['smp']
if 'vnc:ip' in data:
extra['vnc:ip'] = data['vnc:ip']
if 'vnc:password' in data:
extra['vnc:password'] = data['vnc:password']
boot_device = data['boot']
if isinstance(boot_device, list):
for device in boot_device:
extra[device] = data[device]
else:
extra[boot_device] = data[boot_device]
if ssh_password:
extra.update({'password': ssh_password})
node = Node(id=data['server'], name=data['name'], state=state,
public_ips=public_ip, private_ips=None,
driver=self.connection.driver,
extra=extra)
return node