386 lines
11 KiB
Python
386 lines
11 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.
|
|
|
|
"""
|
|
Enomaly ECP driver
|
|
"""
|
|
import time
|
|
import os
|
|
import socket
|
|
import binascii
|
|
|
|
from libcloud.utils.py3 import httplib
|
|
from libcloud.utils.py3 import b
|
|
from libcloud.utils.py3 import base64_encode_string
|
|
|
|
# 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.utils.networking 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_encode_string(
|
|
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: ``list`` of :class:`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 node is not 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: :class:`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: :class:`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 application 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 appliances aka images.
|
|
|
|
@inherits: :class:`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: :class:`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: ``list`` of :class:`NodeLocation`
|
|
"""
|
|
return [NodeLocation(id=1,
|
|
name="Cloud",
|
|
country='',
|
|
driver=self),
|
|
]
|
|
|
|
def create_node(self, name, size, image):
|
|
"""
|
|
Creates a virtual machine.
|
|
|
|
:keyword name: String with a name for this new node (required)
|
|
:type name: ``str``
|
|
|
|
:keyword size: The size of resources allocated to this node .
|
|
(required)
|
|
:type size: :class:`NodeSize`
|
|
|
|
:keyword image: OS Image to boot on node. (required)
|
|
:type image: :class:`NodeImage`
|
|
|
|
:rtype: :class:`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(name),
|
|
'package': str(image.id),
|
|
'hardware': str(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
|