| # 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. |
| |
| """ |
| Enomaly ECP driver |
| """ |
| import time |
| import base64 |
| import os |
| import socket |
| import binascii |
| |
| from libcloud.utils.py3 import httplib |
| from libcloud.utils.py3 import b |
| from libcloud.utils.py3 import u |
| |
| # JSON is included in the standard library starting with Python 2.6. For 2.5 |
| # and 2.4, there's a simplejson egg at: http://pypi.python.org/pypi/simplejson |
| try: |
| import simplejson as json |
| except ImportError: |
| import json |
| |
| from libcloud.common.base import Response, ConnectionUserAndKey |
| from libcloud.compute.base import NodeDriver, NodeSize, NodeLocation |
| from libcloud.compute.base import NodeImage, Node |
| from libcloud.compute.types import Provider, NodeState, InvalidCredsError |
| from libcloud.compute.base import is_private_subnet |
| |
| #Defaults |
| API_HOST = '' |
| API_PORT = (80, 443) |
| |
| |
| class ECPResponse(Response): |
| def success(self): |
| if self.status == httplib.OK or self.status == httplib.CREATED: |
| try: |
| j_body = json.loads(self.body) |
| except ValueError: |
| self.error = "JSON response cannot be decoded." |
| return False |
| if j_body['errno'] == 0: |
| return True |
| else: |
| self.error = "ECP error: %s" % j_body['message'] |
| return False |
| elif self.status == httplib.UNAUTHORIZED: |
| raise InvalidCredsError() |
| else: |
| self.error = "HTTP Error Code: %s" % self.status |
| return False |
| |
| def parse_error(self): |
| return self.error |
| |
| #Interpret the json responses - no error checking required |
| def parse_body(self): |
| return json.loads(self.body) |
| |
| def getheaders(self): |
| return self.headers |
| |
| |
| class ECPConnection(ConnectionUserAndKey): |
| """ |
| Connection class for the Enomaly ECP driver |
| """ |
| |
| responseCls = ECPResponse |
| host = API_HOST |
| port = API_PORT |
| |
| def add_default_headers(self, headers): |
| #Authentication |
| username = self.user_id |
| password = self.key |
| base64string = base64.encodestring( |
| b('%s:%s' % (username, password)))[:-1] |
| authheader = "Basic %s" % base64string |
| headers['Authorization'] = authheader |
| |
| return headers |
| |
| def _encode_multipart_formdata(self, fields): |
| """ |
| Based on Wade Leftwich's function: |
| http://code.activestate.com/recipes/146306/ |
| """ |
| #use a random boundary that does not appear in the fields |
| boundary = '' |
| while boundary in ''.join(fields): |
| boundary = binascii.hexlify(os.urandom(16)).decode('utf-8') |
| L = [] |
| for i in fields: |
| L.append('--' + boundary) |
| L.append('Content-Disposition: form-data; name="%s"' % i) |
| L.append('') |
| L.append(fields[i]) |
| L.append('--' + boundary + '--') |
| L.append('') |
| body = '\r\n'.join(L) |
| content_type = 'multipart/form-data; boundary=%s' % boundary |
| header = {'Content-Type': content_type} |
| return header, body |
| |
| |
| class ECPNodeDriver(NodeDriver): |
| """ |
| Enomaly ECP node driver |
| """ |
| |
| name = "Enomaly Elastic Computing Platform" |
| website = 'http://www.enomaly.com/' |
| type = Provider.ECP |
| connectionCls = ECPConnection |
| |
| def list_nodes(self): |
| """ |
| Returns a list of all running Nodes |
| |
| @rtype: C{list} of L{Node} |
| """ |
| |
| #Make the call |
| res = self.connection.request('/rest/hosting/vm/list').parse_body() |
| |
| #Put together a list of node objects |
| nodes = [] |
| for vm in res['vms']: |
| node = self._to_node(vm) |
| if not node is None: |
| nodes.append(node) |
| |
| #And return it |
| return nodes |
| |
| def _to_node(self, vm): |
| """ |
| Turns a (json) dictionary into a Node object. |
| This returns only running VMs. |
| """ |
| |
| #Check state |
| if not vm['state'] == "running": |
| return None |
| |
| #IPs |
| iplist = [interface['ip'] for interface in vm['interfaces'] if |
| interface['ip'] != '127.0.0.1'] |
| |
| public_ips = [] |
| private_ips = [] |
| for ip in iplist: |
| try: |
| socket.inet_aton(ip) |
| except socket.error: |
| # not a valid ip |
| continue |
| if is_private_subnet(ip): |
| private_ips.append(ip) |
| else: |
| public_ips.append(ip) |
| |
| #Create the node object |
| n = Node( |
| id=vm['uuid'], |
| name=vm['name'], |
| state=NodeState.RUNNING, |
| public_ips=public_ips, |
| private_ips=private_ips, |
| driver=self, |
| ) |
| |
| return n |
| |
| def reboot_node(self, node): |
| """ |
| Shuts down a VM and then starts it again. |
| |
| @inherits: L{NodeDriver.reboot_node} |
| """ |
| |
| #Turn the VM off |
| #Black magic to make the POST requests work |
| d = self.connection._encode_multipart_formdata({'action': 'stop'}) |
| self.connection.request( |
| '/rest/hosting/vm/%s' % node.id, |
| method='POST', |
| headers=d[0], |
| data=d[1] |
| ).parse_body() |
| |
| node.state = NodeState.REBOOTING |
| #Wait for it to turn off and then continue (to turn it on again) |
| while node.state == NodeState.REBOOTING: |
| #Check if it's off. |
| response = self.connection.request( |
| '/rest/hosting/vm/%s' % node.id |
| ).parse_body() |
| if response['vm']['state'] == 'off': |
| node.state = NodeState.TERMINATED |
| else: |
| time.sleep(5) |
| |
| #Turn the VM back on. |
| #Black magic to make the POST requests work |
| d = self.connection._encode_multipart_formdata({'action': 'start'}) |
| self.connection.request( |
| '/rest/hosting/vm/%s' % node.id, |
| method='POST', |
| headers=d[0], |
| data=d[1] |
| ).parse_body() |
| |
| node.state = NodeState.RUNNING |
| return True |
| |
| def destroy_node(self, node): |
| """ |
| Shuts down and deletes a VM. |
| |
| @inherits: L{NodeDriver.destroy_node} |
| """ |
| |
| #Shut down first |
| #Black magic to make the POST requests work |
| d = self.connection._encode_multipart_formdata({'action': 'stop'}) |
| self.connection.request( |
| '/rest/hosting/vm/%s' % node.id, |
| method='POST', |
| headers=d[0], |
| data=d[1] |
| ).parse_body() |
| |
| #Ensure there was no applicationl level error |
| node.state = NodeState.PENDING |
| #Wait for the VM to turn off before continuing |
| while node.state == NodeState.PENDING: |
| #Check if it's off. |
| response = self.connection.request( |
| '/rest/hosting/vm/%s' % node.id |
| ).parse_body() |
| if response['vm']['state'] == 'off': |
| node.state = NodeState.TERMINATED |
| else: |
| time.sleep(5) |
| |
| #Delete the VM |
| #Black magic to make the POST requests work |
| d = self.connection._encode_multipart_formdata({'action': 'delete'}) |
| self.connection.request( |
| '/rest/hosting/vm/%s' % (node.id), |
| method='POST', |
| headers=d[0], |
| data=d[1] |
| ).parse_body() |
| |
| return True |
| |
| def list_images(self, location=None): |
| """ |
| Returns a list of all package templates aka appiances aka images. |
| |
| @inherits: L{NodeDriver.list_images} |
| """ |
| |
| #Make the call |
| response = self.connection.request( |
| '/rest/hosting/ptemplate/list').parse_body() |
| |
| #Turn the response into an array of NodeImage objects |
| images = [] |
| for ptemplate in response['packages']: |
| images.append(NodeImage( |
| id=ptemplate['uuid'], |
| name='%s: %s' % (ptemplate['name'], ptemplate['description']), |
| driver=self,) |
| ) |
| |
| return images |
| |
| def list_sizes(self, location=None): |
| """ |
| Returns a list of all hardware templates |
| |
| @inherits: L{NodeDriver.list_sizes} |
| """ |
| |
| #Make the call |
| response = self.connection.request( |
| '/rest/hosting/htemplate/list').parse_body() |
| |
| #Turn the response into an array of NodeSize objects |
| sizes = [] |
| for htemplate in response['templates']: |
| sizes.append(NodeSize( |
| id=htemplate['uuid'], |
| name=htemplate['name'], |
| ram=htemplate['memory'], |
| disk=0, # Disk is independent of hardware template. |
| bandwidth=0, # There is no way to keep track of bandwidth. |
| price=0, # The billing system is external. |
| driver=self,) |
| ) |
| |
| return sizes |
| |
| def list_locations(self): |
| """ |
| This feature does not exist in ECP. Returns hard coded dummy location. |
| |
| @rtype: C{list} of L{NodeLocation} |
| """ |
| return [NodeLocation(id=1, |
| name="Cloud", |
| country='', |
| driver=self), |
| ] |
| |
| def create_node(self, **kwargs): |
| """ |
| Creates a virtual machine. |
| |
| @keyword name: String with a name for this new node (required) |
| @type name: C{str} |
| |
| @keyword size: The size of resources allocated to this node . |
| (required) |
| @type size: L{NodeSize} |
| |
| @keyword image: OS Image to boot on node. (required) |
| @type image: L{NodeImage} |
| |
| @rtype: L{Node} |
| """ |
| |
| #Find out what network to put the VM on. |
| res = self.connection.request( |
| '/rest/hosting/network/list').parse_body() |
| |
| #Use the first / default network because there is no way to specific |
| #which one |
| network = res['networks'][0]['uuid'] |
| |
| #Prepare to make the VM |
| data = { |
| 'name': str(kwargs['name']), |
| 'package': str(kwargs['image'].id), |
| 'hardware': str(kwargs['size'].id), |
| 'network_uuid': str(network), |
| 'disk': '' |
| } |
| |
| #Black magic to make the POST requests work |
| d = self.connection._encode_multipart_formdata(data) |
| response = self.connection.request( |
| '/rest/hosting/vm/', |
| method='PUT', |
| headers=d[0], |
| data=d[1] |
| ).parse_body() |
| |
| #Create a node object and return it. |
| n = Node( |
| id=response['machine_id'], |
| name=data['name'], |
| state=NodeState.PENDING, |
| public_ips=[], |
| private_ips=[], |
| driver=self, |
| ) |
| |
| return n |