| # 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 |