Index: test/test_ecp.py =================================================================== --- test/test_ecp.py (revision 0) +++ test/test_ecp.py (revision 0) @@ -0,0 +1,127 @@ +# 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. +# libcloud.org 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. +import sys +import unittest + +from libcloud.drivers.ecp import ECPNodeDriver +from libcloud.types import NodeState +from test import MockHttp, TestCaseMixin +from test.file_fixtures import FileFixtures + +import httplib + +from secrets import ECP_USER_NAME, ECP_PASSWORD + +class ECPTests(unittest.TestCase, TestCaseMixin): + + def setUp(self): + ECPNodeDriver.connectionCls.conn_classes = (None, + ECPMockHttp) + self.driver = ECPNodeDriver(ECP_USER_NAME, ECP_PASSWORD) + + + def test_list_nodes(self): + nodes = self.driver.list_nodes() + self.assertEqual(len(nodes),2) + node = nodes[0] + self.assertEqual(node.id, 1) + self.assertEqual(node.name, 'dummy-1') + self.assertEqual(node.public_ip[0], "42.78.124.75") + self.assertEqual(node.state, NodeState.RUNNING) + + + def test_list_sizes(self): + sizes = self.driver.list_sizes() + self.assertEqual(len(sizes),3) + size = sizes[0] + self.assertEqual(size.id,'1') + self.assertEqual(size.ram,512) + self.assertEqual(size.disk,0) + self.assertEqual(size.bandwidth,0) + self.assertEqual(size.price,0) + + def test_list_images(self): + images = self.driver.list_images() + self.assertEqual(len(images),2) + self.assertEqual(images[0].name,"centos54: AUTO import from /opt/enomalism2/repo/5d407a68-c76c-11de-86e5-000475cb7577.xvm2") + self.assertEqual(images[0].id, "1") + self.assertEqual(images[1].name,"centos54 two: AUTO import from /opt/enomalism2/repo/5d407a68-c76c-11de-86e5-000475cb7577.xvm2") + self.assertEqual(images[1].id, "2") + + def test_reboot_node(self): + # Raises exception on failure + node = self.driver.list_nodes()[0] + self.driver.reboot_node(node) + + def test_destroy_node(self): + # Raises exception on failure + node = self.driver.list_nodes()[0] + self.driver.destroy_node(node) + + def test_create_node(self): + # Raises exception on failure + size = self.driver.list_sizes()[0] + image = self.driver.list_images()[0] + node = self.driver.create_node(name="api.ivan.net.nz", image=image, size=size) + self.assertEqual(node.name, "api.ivan.net.nz") + self.assertEqual(node.id, "1234") + +class ECPMockHttp(MockHttp): + + fixtures = FileFixtures('ecp') + + def _modules_hosting(self, method, url, body, headers): + headers = {} + headers['set-cookie'] = 'vcloud-token=testtoken' + body = 'Anything' + return (httplib.OK, body, headers, httplib.responses[httplib.OK]) + + def _rest_hosting_vm_1(self, method, url, body, headers): + if method == 'GET': + body = self.fixtures.load('vm_1_get.json') + if method == 'POST': + if body.find('delete',0): + body = self.fixtures.load('vm_1_action_delete.json') + if body.find('stop',0): + body = self.fixtures.load('vm_1_action_stop.json') + if body.find('start',0): + body = self.fixtures.load('vm_1_action_start.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _rest_hosting_vm(self, method, url, body, headers): + if method == 'PUT': + body = self.fixtures.load('vm_put.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _rest_hosting_vm_list(self, method, url, body, headers): + body = self.fixtures.load('vm_list.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _rest_hosting_htemplate_list(self, method, url, body, headers): + body = self.fixtures.load('htemplate_list.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _rest_hosting_network_list(self, method, url, body, headers): + body = self.fixtures.load('network_list.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _rest_hosting_ptemplate_list(self, method, url, body, headers): + body = self.fixtures.load('ptemplate_list.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + + +if __name__ == '__main__': + sys.exit(unittest.main()) Index: test/secrets.py-dist =================================================================== --- test/secrets.py-dist (revision 922317) +++ test/secrets.py-dist (working copy) @@ -41,3 +41,6 @@ VOXEL_KEY='' VOXEL_SECRET='' + +ECP_USER_NAME='' +ECP_PASSWORD='' \ No newline at end of file Index: test/fixtures/ecp/vm_put.json =================================================================== --- test/fixtures/ecp/vm_put.json (revision 0) +++ test/fixtures/ecp/vm_put.json (revision 0) @@ -0,0 +1 @@ +{"errno": 0, "message": "Success", "txid": "fc38963c-a9fa-11de-8c4b-001baaa56c51", "machine_id": "1234"} \ No newline at end of file Index: test/fixtures/ecp/vm_1_action_start.json =================================================================== --- test/fixtures/ecp/vm_1_action_start.json (revision 0) +++ test/fixtures/ecp/vm_1_action_start.json (revision 0) @@ -0,0 +1,3 @@ +{"errno": 0, "message": "Success", "vm": +{"vnc_enabled": true, "uuid": 1, "tags": [], "ip_address": "42.78.124.75", "interfaces": [{"ip": "42.78.124.75", "mac": "00:16:e9:d6:40:c6", "network_name": "Default", "uuid": "479b9823-2ded-11df-94e8-0015174e564c", "network": "fc38963c-a9fa-11de-8c4b-001b63a56c51"}], "vnc_port": "5900", "name": "dummy-1", "state": "unkown", "trusted": null, "os": "unknown", "vnc_password": "jBs5UT00", "vnc_ip_address": "192.168.1.12", "hardware_profile_uuid": "bcaff710-2914-11de-836c-001a929face2"} +} \ No newline at end of file Index: test/fixtures/ecp/vm_list.json =================================================================== --- test/fixtures/ecp/vm_list.json (revision 0) +++ test/fixtures/ecp/vm_list.json (revision 0) @@ -0,0 +1,10 @@ +{"errno": 0, "message": "Success", "vms": +[ +{"vnc_enabled": true, "uuid": 1, "tags": [], "ip_address": "42.78.124.75", "interfaces": [{"ip": "42.78.124.75", "mac": "00:16:e9:d6:40:c6", "network_name": "Default", "uuid": "479b9823-2ded-11df-94e8-0015174e564c", "network": "fc38963c-a9fa-11de-8c4b-001b63a56c51"}], "vnc_port": "5900", "name": "dummy-1", "state": "running", "trusted": null, "os": "unknown", "vnc_password": "jBs5UT00", "vnc_ip_address": "192.168.1.111", "hardware_profile_uuid": "bcaff710-2914-11de-836c-001a929face2"}, + +{"vnc_enabled": true, "uuid": 2, "tags": [], "ip_address": "42.78.124.75", "interfaces": [{"ip": "42.78.124.75", "mac": "00:16:72:b4:71:21", "network_name": "Default", "uuid": "c76edd61-2dfd-11df-84ca-0015174e564c", "network": "fc38963c-a9fa-11de-8c4b-001b63a56c51"}], "vnc_port": "5902", "name": "dummy-2", "state": "running", "trusted": null, "os": "unknown", "vnc_password": "zoiZW31T", "vnc_ip_address": "192.168.1.111", "hardware_profile_uuid": "bcaff710-2914-11de-836c-001a929face2"}, + +{"vnc_enabled": true, "uuid": 3, "tags": [], "ip_address": "42.78.124.75", "interfaces": [{"ip": "42.78.124.75", "mac": "00:16:e9:d6:40:c6", "network_name": "Default", "uuid": "479b9823-2ded-11df-94e8-0015174e564c", "network": "fc38963c-a9fa-11de-8c4b-001b63a56c51"}], "vnc_port": "5900", "name": "dummy-1", "state": "stopped", "trusted": null, "os": "unknown", "vnc_password": "jBs5UT00", "vnc_ip_address": "192.168.1.111", "hardware_profile_uuid": "bcaff710-2914-11de-836c-001a929face2"} + +] +} \ No newline at end of file Index: test/fixtures/ecp/htemplate_list.json =================================================================== --- test/fixtures/ecp/htemplate_list.json (revision 0) +++ test/fixtures/ecp/htemplate_list.json (revision 0) @@ -0,0 +1,9 @@ +{"templates": [ + +{"uuid": "1", "hypervisor_name": "kvm-hvm", "cpus": 1, "memory": 512, "arch": "i686", "id": 1, "name": "Small"}, + +{"uuid": "2", "hypervisor_name": "kvm-hvm", "cpus": 2, "memory": 1024, "arch": "i686", "id": 2, "name": "Medium"}, + +{"uuid": "3", "hypervisor_name": "kvm-hvm", "cpus": 3, "memory": 2048, "arch": "x86_64", "id": 3, "name": "Large"} + +], "errno": 0, "message": "Success"} \ No newline at end of file Index: test/fixtures/ecp/vm_1_action_delete.json =================================================================== --- test/fixtures/ecp/vm_1_action_delete.json (revision 0) +++ test/fixtures/ecp/vm_1_action_delete.json (revision 0) @@ -0,0 +1 @@ +{"errno": 0, "message": "Success"} \ No newline at end of file Index: test/fixtures/ecp/vm_1_get.json =================================================================== --- test/fixtures/ecp/vm_1_get.json (revision 0) +++ test/fixtures/ecp/vm_1_get.json (revision 0) @@ -0,0 +1,3 @@ +{"errno": 0, "message": "Success", "vm": +{"vnc_enabled": true, "uuid": 1, "tags": [], "ip_address": "42.78.124.75", "interfaces": [{"ip": "42.78.124.75", "mac": "00:16:e9:d6:40:c6", "network_name": "Default", "uuid": "479b9823-2ded-11df-94e8-0015174e564c", "network": "fc38963c-a9fa-11de-8c4b-001b63a56c51"}], "vnc_port": "5900", "name": "dummy-1", "state": "off", "trusted": null, "os": "unknown", "vnc_password": "jBs5UT00", "vnc_ip_address": "192.168.1.111", "hardware_profile_uuid": "bcaff710-2914-11de-836c-001a929face2"} +} \ No newline at end of file Index: test/fixtures/ecp/network_list.json =================================================================== --- test/fixtures/ecp/network_list.json (revision 0) +++ test/fixtures/ecp/network_list.json (revision 0) @@ -0,0 +1 @@ +{"errno": 0, "message": "Success", "networks": [{"uuid": "1", "vlan_id": null, "name": "Default"}]} \ No newline at end of file Index: test/fixtures/ecp/vm_1_action_stop.json =================================================================== --- test/fixtures/ecp/vm_1_action_stop.json (revision 0) +++ test/fixtures/ecp/vm_1_action_stop.json (revision 0) @@ -0,0 +1,3 @@ +{"errno": 0, "message": "Success", "vm": +{"vnc_enabled": true, "uuid": 1, "tags": [], "ip_address": "42.78.124.75", "interfaces": [{"ip": "42.78.124.75", "mac": "00:16:e9:d6:40:c6", "network_name": "Default", "uuid": "479b9823-2ded-11df-94e8-0015174e564c", "network": "fc38963c-a9fa-11de-8c4b-001b63a56c51"}], "vnc_port": "5900", "name": "dummy-1", "state": "unkown", "trusted": null, "os": "unknown", "vnc_password": "jBs5UT00", "vnc_ip_address": "192.168.1.111", "hardware_profile_uuid": "bcaff710-2914-11de-836c-001a929face2"} +} \ No newline at end of file Index: test/fixtures/ecp/ptemplate_list.json =================================================================== --- test/fixtures/ecp/ptemplate_list.json (revision 0) +++ test/fixtures/ecp/ptemplate_list.json (revision 0) @@ -0,0 +1,6 @@ +{"errno": 0, "message": "Success", "packages": [ + +{"os": "unknown", "description": "AUTO import from /opt/enomalism2/repo/5d407a68-c76c-11de-86e5-000475cb7577.xvm2", "storage": 20480, "uuid": "1", "name": "centos54"}, + +{"os": "unknown", "description": "AUTO import from /opt/enomalism2/repo/5d407a68-c76c-11de-86e5-000475cb7577.xvm2", "storage": 20480, "uuid": "2", "name": "centos54 two"} +]} \ No newline at end of file Index: libcloud/providers.py =================================================================== --- libcloud/providers.py (revision 922317) +++ libcloud/providers.py (working copy) @@ -27,6 +27,8 @@ ('libcloud.drivers.ec2', 'EC2EUNodeDriver'), Provider.EC2_US_WEST: ('libcloud.drivers.ec2', 'EC2USWestNodeDriver'), + Provider.ECP: + ('libcloud.drivers.ecp', 'ECPNodeDriver'), Provider.GOGRID: ('libcloud.drivers.gogrid', 'GoGridNodeDriver'), Provider.RACKSPACE: Index: libcloud/types.py =================================================================== --- libcloud/types.py (revision 922317) +++ libcloud/types.py (working copy) @@ -47,6 +47,7 @@ EC2_US_WEST = 10 VOXEL = 11 SOFTLAYER = 12 + ECP = 13 class NodeState(object): """ Index: libcloud/drivers/ecp.py =================================================================== --- libcloud/drivers/ecp.py (revision 0) +++ libcloud/drivers/ecp.py (revision 0) @@ -0,0 +1,371 @@ +# 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. +# libcloud.org 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 +""" +from libcloud.interface import INodeDriver +from libcloud.base import NodeDriver, NodeSize, NodeLocation +from libcloud.base import NodeImage, Node +from libcloud.base import Response, ConnectionUserAndKey +from libcloud.types import Provider, NodeState, InvalidCredsException +from zope.interface import implements + +import uuid +import time +import base64 + +# 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 json +except: import simplejson as json + +#Defaults +API_HOST = '' +API_PORT = (80,443) +API_SECURE = True + +class ECPResponse(Response): + + #Interpret the json responses + def parse_body(self): + try: + return json.loads(self.body) + except ValueError, e: + raise Exception("%s: %s" % (e, self.error)) + + def getheaders(self): + return self.headers + +class ECPConnection(ConnectionUserAndKey): + + responseCls = ECPResponse + host = API_HOST + port = API_PORT + secure = API_SECURE + + def request(self, *args, **kwargs): + return super(ECPConnection, self).request(*args, **kwargs) + + def add_default_headers(self, headers): + #Authentication + username = self.user_id + password = self.key + base64string = base64.encodestring( + '%s:%s' % (username, password))[:-1] + authheader = "Basic %s" % base64string + headers['Authorization']= authheader + + return headers + + def _encode_multipart_formdata(self, fields): + BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$' + CRLF = '\r\n' + L = [] + for i in fields.keys(): + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"' % i) + L.append('') + L.append(fields[i]) + L.append('--' + BOUNDARY + '--') + L.append('') + body = CRLF.join(L) + content_type = 'multipart/form-data; boundary=%s' % BOUNDARY + header = {'Content-Type':content_type} + return header, body + + +class ECPNodeDriver(NodeDriver): + + name = "Enomaly Elastic Computing Platform" + type = Provider.ECP + + implements(INodeDriver) + + def __init__(self, user_name, password): + """ + Sets the username and password on creation. Also creates the connection + object + """ + self.user_name = user_name + self.password = password + self.connection = ECPConnection(self.user_name, self.password) + self.connection.driver = self + + def list_nodes(self): + """ + Returns a list of all running Nodes + """ + + #Make the call + res = self.connection.request('/rest/hosting/vm/list').parse_body() + + #Check for application level error + if not res['errno'] == 0: + raise Exception('Cannot retrieve nodes list.') + + #Put together a list of node objects + nodes=[] + for vm in res['vms']: + node = self._to_node(vm) + if not node == 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'] + + #Create the node object + n = Node( + id=vm['uuid'], + name=vm['name'], + state=NodeState.RUNNING, + public_ip=iplist, + private_ip=iplist, + driver=self, + ) + + return n + + def reboot_node(self, node): + """ + This works by black magic. + """ + + #Turn the VM off + #Black magic to make the POST requests work + d = self.connection._encode_multipart_formdata({'action':'stop'}) + response = self.connection.request( + '/rest/hosting/vm/%s' % node.id, + method='POST', + headers=d[0], + data=d[1] + ).parse_body() + + #Check for application level error + if response['errno'] == 0: + 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) + else: + raise Exception('Node reboot failed due to ECP error: %s' % \ + response['message']) + + + #Turn the VM back on. + #Black magic to make the POST requests work + d = self.connection._encode_multipart_formdata({'action':'start'}) + response = self.connection.request( + '/rest/hosting/vm/%s' % node.id, + method='POST', + headers=d[0], + data=d[1] + ).parse_body() + + #Check for application level error + if response['errno'] == 0: + node.state = NodeState.RUNNING + return True + else: + raise Exception('Node reboot failed due to ECP error: %s' % \ + response['message']) + + def destroy_node(self, node): + """ + Shuts down and deletes a VM. Also black magic. + """ + + #Shut down first + #Black magic to make the POST requests work + d = self.connection._encode_multipart_formdata({'action':'stop'}) + response = 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 + if response['errno'] == 0: + 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) + else: + raise Exception('Node destroy failed due to ECP error: %s' % \ + response['message']) + + #Delete the VM + #Black magic to make the POST requests work + d = self.connection._encode_multipart_formdata({'action':'delete'}) + response = self.connection.request( + '/rest/hosting/vm/%s' % (node.id), + method='POST', + headers=d[0], + data=d[1] + ).parse_body() + + #Ensure there was no applicaiton level error + if response['errno'] == 0: + return True + else: + raise Exception('Node destroy failed due to ECP error: %s' % \ + response['message']) + + def list_images(self, location=None): + """ + Returns a list of all package templates aka appiances aka images + """ + + #Make the call + response = self.connection.request( + '/rest/hosting/ptemplate/list').parse_body() + + #Ensure there was no applicaiton level error + if not response['errno'] == 0: + raise Exception('Cannot get images list. Error: %s' % \ + response['message']) + + #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 + """ + + #Make the call + response = self.connection.request( + '/rest/hosting/htemplate/list').parse_body() + + #Ensure there was no application level error + if not response['errno'] == 0: + raise Exception('Cannot get sizes list. Error: %s' % \ + response['message']) + + #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. + """ + return [ + NodeLocation(id=1, + name="Cloud", + country='', + driver=self), + ] + + def create_node(self, **kwargs): + """ + Creates a virtual machine. + + Parameterss: name (string), image (NodeImage), size (NodeSize) + """ + + #Find out what network to put the VM on. + res = self.connection.request('/rest/hosting/network/list').parse_body() + if not res['errno'] == 0: + raise Exception('Cannot get network list. Error: %s' % \ + res['message']) + + #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() + + #Check of application level error + if not response['errno'] == 0: + raise Exception('Cannot create Node. Error: %s' % \ + response['message']) + + #Create a node object and return it. + n = Node( + id=response['machine_id'], + name=data['name'], + state=NodeState.RUNNING, + public_ip=[], + private_ip=[], + driver=self, + ) + + return n \ No newline at end of file