You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@libcloud.apache.org by or...@apache.org on 2010/04/26 19:58:13 UTC

svn commit: r938156 - in /incubator/libcloud/trunk: libcloud/ libcloud/drivers/ test/ test/fixtures/ecp/

Author: oremj
Date: Mon Apr 26 17:58:12 2010
New Revision: 938156

URL: http://svn.apache.org/viewvc?rev=938156&view=rev
Log:
Add Enomaly ECP Driver.

Author:    Viktor Stanchev <vi...@enomaly.com>
Signed-off-by: Jeremy Orem <je...@gmail.com>

Added:
    incubator/libcloud/trunk/libcloud/drivers/ecp.py
    incubator/libcloud/trunk/test/fixtures/ecp/
    incubator/libcloud/trunk/test/fixtures/ecp/htemplate_list.json
    incubator/libcloud/trunk/test/fixtures/ecp/network_list.json
    incubator/libcloud/trunk/test/fixtures/ecp/ptemplate_list.json
    incubator/libcloud/trunk/test/fixtures/ecp/vm_1_action_delete.json
    incubator/libcloud/trunk/test/fixtures/ecp/vm_1_action_start.json
    incubator/libcloud/trunk/test/fixtures/ecp/vm_1_action_stop.json
    incubator/libcloud/trunk/test/fixtures/ecp/vm_1_get.json
    incubator/libcloud/trunk/test/fixtures/ecp/vm_list.json
    incubator/libcloud/trunk/test/fixtures/ecp/vm_put.json
    incubator/libcloud/trunk/test/test_ecp.py
Modified:
    incubator/libcloud/trunk/libcloud/providers.py
    incubator/libcloud/trunk/libcloud/types.py
    incubator/libcloud/trunk/test/secrets.py-dist

Added: incubator/libcloud/trunk/libcloud/drivers/ecp.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/libcloud/drivers/ecp.py?rev=938156&view=auto
==============================================================================
--- incubator/libcloud/trunk/libcloud/drivers/ecp.py (added)
+++ incubator/libcloud/trunk/libcloud/drivers/ecp.py Mon Apr 26 17:58:12 2010
@@ -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.
+        
+        Parameters: 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

Modified: incubator/libcloud/trunk/libcloud/providers.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/libcloud/providers.py?rev=938156&r1=938155&r2=938156&view=diff
==============================================================================
--- incubator/libcloud/trunk/libcloud/providers.py (original)
+++ incubator/libcloud/trunk/libcloud/providers.py Mon Apr 26 17:58:12 2010
@@ -27,6 +27,8 @@ DRIVERS = {
         ('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:

Modified: incubator/libcloud/trunk/libcloud/types.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/libcloud/types.py?rev=938156&r1=938155&r2=938156&view=diff
==============================================================================
--- incubator/libcloud/trunk/libcloud/types.py (original)
+++ incubator/libcloud/trunk/libcloud/types.py Mon Apr 26 17:58:12 2010
@@ -31,6 +31,7 @@ class Provider(object):
     @cvar LINODE: Linode.com
     @cvar VCLOUD: vmware vCloud
     @cvar RIMUHOSTING: RimuHosting.com
+    @cvar ECP: Enomaly
     """
     DUMMY = 0
     EC2 = 1  # deprecated name
@@ -48,6 +49,7 @@ class Provider(object):
     VOXEL = 11
     SOFTLAYER = 12
     EUCALYPTUS = 13
+    ECP = 14
 
 class NodeState(object):
     """

Added: incubator/libcloud/trunk/test/fixtures/ecp/htemplate_list.json
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/test/fixtures/ecp/htemplate_list.json?rev=938156&view=auto
==============================================================================
--- incubator/libcloud/trunk/test/fixtures/ecp/htemplate_list.json (added)
+++ incubator/libcloud/trunk/test/fixtures/ecp/htemplate_list.json Mon Apr 26 17:58:12 2010
@@ -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

Added: incubator/libcloud/trunk/test/fixtures/ecp/network_list.json
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/test/fixtures/ecp/network_list.json?rev=938156&view=auto
==============================================================================
--- incubator/libcloud/trunk/test/fixtures/ecp/network_list.json (added)
+++ incubator/libcloud/trunk/test/fixtures/ecp/network_list.json Mon Apr 26 17:58:12 2010
@@ -0,0 +1 @@
+{"errno": 0, "message": "Success", "networks": [{"uuid": "1", "vlan_id": null, "name": "Default"}]}
\ No newline at end of file

Added: incubator/libcloud/trunk/test/fixtures/ecp/ptemplate_list.json
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/test/fixtures/ecp/ptemplate_list.json?rev=938156&view=auto
==============================================================================
--- incubator/libcloud/trunk/test/fixtures/ecp/ptemplate_list.json (added)
+++ incubator/libcloud/trunk/test/fixtures/ecp/ptemplate_list.json Mon Apr 26 17:58:12 2010
@@ -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

Added: incubator/libcloud/trunk/test/fixtures/ecp/vm_1_action_delete.json
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/test/fixtures/ecp/vm_1_action_delete.json?rev=938156&view=auto
==============================================================================
--- incubator/libcloud/trunk/test/fixtures/ecp/vm_1_action_delete.json (added)
+++ incubator/libcloud/trunk/test/fixtures/ecp/vm_1_action_delete.json Mon Apr 26 17:58:12 2010
@@ -0,0 +1 @@
+{"errno": 0, "message": "Success"}
\ No newline at end of file

Added: incubator/libcloud/trunk/test/fixtures/ecp/vm_1_action_start.json
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/test/fixtures/ecp/vm_1_action_start.json?rev=938156&view=auto
==============================================================================
--- incubator/libcloud/trunk/test/fixtures/ecp/vm_1_action_start.json (added)
+++ incubator/libcloud/trunk/test/fixtures/ecp/vm_1_action_start.json Mon Apr 26 17:58:12 2010
@@ -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

Added: incubator/libcloud/trunk/test/fixtures/ecp/vm_1_action_stop.json
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/test/fixtures/ecp/vm_1_action_stop.json?rev=938156&view=auto
==============================================================================
--- incubator/libcloud/trunk/test/fixtures/ecp/vm_1_action_stop.json (added)
+++ incubator/libcloud/trunk/test/fixtures/ecp/vm_1_action_stop.json Mon Apr 26 17:58:12 2010
@@ -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

Added: incubator/libcloud/trunk/test/fixtures/ecp/vm_1_get.json
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/test/fixtures/ecp/vm_1_get.json?rev=938156&view=auto
==============================================================================
--- incubator/libcloud/trunk/test/fixtures/ecp/vm_1_get.json (added)
+++ incubator/libcloud/trunk/test/fixtures/ecp/vm_1_get.json Mon Apr 26 17:58:12 2010
@@ -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

Added: incubator/libcloud/trunk/test/fixtures/ecp/vm_list.json
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/test/fixtures/ecp/vm_list.json?rev=938156&view=auto
==============================================================================
--- incubator/libcloud/trunk/test/fixtures/ecp/vm_list.json (added)
+++ incubator/libcloud/trunk/test/fixtures/ecp/vm_list.json Mon Apr 26 17:58:12 2010
@@ -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

Added: incubator/libcloud/trunk/test/fixtures/ecp/vm_put.json
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/test/fixtures/ecp/vm_put.json?rev=938156&view=auto
==============================================================================
--- incubator/libcloud/trunk/test/fixtures/ecp/vm_put.json (added)
+++ incubator/libcloud/trunk/test/fixtures/ecp/vm_put.json Mon Apr 26 17:58:12 2010
@@ -0,0 +1 @@
+{"errno": 0, "message": "Success", "txid": "fc38963c-a9fa-11de-8c4b-001baaa56c51", "machine_id": "1234"}
\ No newline at end of file

Modified: incubator/libcloud/trunk/test/secrets.py-dist
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/test/secrets.py-dist?rev=938156&r1=938155&r2=938156&view=diff
==============================================================================
--- incubator/libcloud/trunk/test/secrets.py-dist (original)
+++ incubator/libcloud/trunk/test/secrets.py-dist Mon Apr 26 17:58:12 2010
@@ -44,3 +44,6 @@ SOFTLAYER_APIKEY=''
 
 VOXEL_KEY=''
 VOXEL_SECRET=''
+
+ECP_USER_NAME=''
+ECP_PASSWORD=''
\ No newline at end of file

Added: incubator/libcloud/trunk/test/test_ecp.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/test/test_ecp.py?rev=938156&view=auto
==============================================================================
--- incubator/libcloud/trunk/test/test_ecp.py (added)
+++ incubator/libcloud/trunk/test/test_ecp.py Mon Apr 26 17:58:12 2010
@@ -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())



Re: [libcloud] Re: svn commit: r938156 - in /incubator/libcloud/trunk: libcloud/ libcloud/drivers/ test/ test/fixtures/ecp/

Posted by Paul Querna <pa...@querna.org>.
On Tue, Apr 27, 2010 at 12:15 PM, Viktor Stanchev <vi...@enomaly.com> wrote:
> Paul,
> I made all the changes to the ECP driver that were suggested. They are in
> the attached patch. I tested it live and everything appears to work
> including InvalidCredsException, common errors and all features. I moved
> error checking to the universal error checking function rather than having
> it for each request.
> I have a few questions though.
> - When creating a VM, should the driver wait for a VM to be finished
> creating before returning?
> If so, then I'd have to modify the driver slightly. If not, it seems that
> there is no way to check if the creation is still in progress or if it
> failed eventually. Maybe you should look into adding a feature to check the
> status of transactions?

As long as the Node.id returned from create_node is correct, so a
future call to list_nodes will return a node with the same ID, we
consider this 'good enough'.  If you need to block in create_node
until some object is created on the backend, you can see how the
Softlayer driver does this because we can't get the node ID right
away. It isn't ideal, but we are trying to enforce that while
create_node might return a Node object in a state of pre-booting, the
ID attribute is accurate.

> - The ECP API has no way to "restart" a VM, instead, I'm turning it off and
> then back on. This takes a bit longer than normal, but is inevitable. I
> guess I can make the reboot happen in a separate thread, but it doesn't seem
> that useful and the error checking won't be reliable. Should I try that?

I would avoid spinning up a thread, and instead just block.  We are
trying to provide an abstraction layer, and sometimes that might be a
little slower, but it would be best to be able to still catch errors
in a consistent manner.


> - The ECP software is used by multiple service providers. We only create the
> software. Therefore there are multiple URLs that it would connect to. How do
> you plan to address this issue? I think adding a wrapper class that sets a
> different URL as the "host" would be the best solution, but it's also
> possible to allow the user of libcloud to select an arbitrary URL.

The node constructor should take an URL.  We are dealing with this
right now in the Eucalyptus and OpenNebula drivers too.  I think we
will end up with a custom subclass of some kind, CustomURLConnection
or some-such in the long run, but for now I'd suggest just copying
what the Eucalyptus driver does in trunk.

> - The node states are not used very well right now, but I don't know if
> there's anything I can do about it. The node states in ECP don't match the
> node states in libcloud.

What states does ECP expose for a node?

Thanks,

Paul

Re: [libcloud] Re: svn commit: r938156 - in /incubator/libcloud/trunk: libcloud/ libcloud/drivers/ test/ test/fixtures/ecp/

Posted by Viktor Stanchev <vi...@enomaly.com>.
Paul,

The node.id is consistent, so I'll keep creation the way it is. The libcloud
user would have to check periodically if the VM is done provisioning. The
only thing that concerns me is that they'd have no way of knowing that a
provision failed. It's not  common for that to happen though, so it should
be fine.

I made new nodes returned by create_node() be in the PENDING state when the
provision command is sent. I guess the returned node would only be used to
keep track of which nodes the libcloud user is waiting to be provisioned.

The reboot works by blocking so I'm leaving it that way.

Destroy doesn't actually check if the node was successfully deleted. Should
I do that? It seems unnecessary and slows down the call, but it would be
good for error checking. Actually, rebooting doesn't check if the node is
back to "running" either.

The problem with rebooting and destroying is that if rebooting fails to shut
down the node, it will be stuck waiting for it to turn off. Same with
destroying a node. I can set a timeout. Would 30 seconds sound reasonable? 1
minute? This can be cause problems if the connection is very slow.

I don't know where to find the Eucapliptus driver. I don't see eucaliptus.py
in trunk. Can you send me a link? I'm not sure I understand how you want me
to implement custom URLs.

Your states are:
    RUNNING = 0
    REBOOTING = 1
    TERMINATED = 2
    PENDING = 3
    UNKNOWN = 4

Our states are:
running
off
paused
unknown

The only state that matches directly is "running".

The only time an ECP node is "rebooting" is in the middle of a node.reboot()
call. So basically the only time a node will be left in the "rebooting"
state is when it fails to start after being shut down (shouldn't happen).

I don't think TERMINATED matches our off state. I'm guessing TERMINATED is
the state a node goes into while waiting to be deleted. In ECP this is such
a short period of time that we don't have a state for it. In the driver it
is set when deleting a node.

I mentioned abode how I'm using PENDING.

I'm not sure if I should let the driver return nodes in the "unknown" state.
That statue usually indicates that the node is in the middle of
transitioning from one state to another or the machine it's running on is
not responding. The driver currently doesn't do that, but I can add it if
you think it is necessary.

It would be nice if there was support for turning nodes on and off because I
can imagine we're not the only ones who have this feature.

I've submitted the patch to jira.
https://issues.apache.org/jira/secure/ManageAttachments.jspa?id=12463218

- Viktor

On Tue, Apr 27, 2010 at 8:04 PM, Paul Querna <pa...@querna.org> wrote:

> On Tue, Apr 27, 2010 at 12:15 PM, Viktor Stanchev <vi...@enomaly.com>
> wrote:
> > Paul,
> > I made all the changes to the ECP driver that were suggested. They are in
> > the attached patch. I tested it live and everything appears to work
>
> Seems like the patch didn't make it to the list, maybe attach it to jira
> again?
>
> Thanks,
>
> Paul
>

Re: [libcloud] Re: svn commit: r938156 - in /incubator/libcloud/trunk: libcloud/ libcloud/drivers/ test/ test/fixtures/ecp/

Posted by Paul Querna <pa...@querna.org>.
On Tue, Apr 27, 2010 at 12:15 PM, Viktor Stanchev <vi...@enomaly.com> wrote:
> Paul,
> I made all the changes to the ECP driver that were suggested. They are in
> the attached patch. I tested it live and everything appears to work

Seems like the patch didn't make it to the list, maybe attach it to jira again?

Thanks,

Paul

Re: [libcloud] Re: svn commit: r938156 - in /incubator/libcloud/trunk: libcloud/ libcloud/drivers/ test/ test/fixtures/ecp/

Posted by Viktor Stanchev <vi...@enomaly.com>.
Paul,

I made all the changes to the ECP driver that were suggested. They are in
the attached patch. I tested it live and everything appears to work
including InvalidCredsException, common errors and all features. I moved
error checking to the universal error checking function rather than having
it for each request.

I have a few questions though.

- When creating a VM, should the driver wait for a VM to be finished
creating before returning?
If so, then I'd have to modify the driver slightly. If not, it seems that
there is no way to check if the creation is still in progress or if it
failed eventually. Maybe you should look into adding a feature to check the
status of transactions?

- The ECP API has no way to "restart" a VM, instead, I'm turning it off and
then back on. This takes a bit longer than normal, but is inevitable. I
guess I can make the reboot happen in a separate thread, but it doesn't seem
that useful and the error checking won't be reliable. Should I try that?

- The ECP software is used by multiple service providers. We only create the
software. Therefore there are multiple URLs that it would connect to. How do
you plan to address this issue? I think adding a wrapper class that sets a
different URL as the "host" would be the best solution, but it's also
possible to allow the user of libcloud to select an arbitrary URL.

- The node states are not used very well right now, but I don't know if
there's anything I can do about it. The node states in ECP don't match the
node states in libcloud.

Viktor

On Tue, Apr 27, 2010 at 12:04 PM, Viktor Stanchev <vi...@enomaly.com>wrote:

> Paul,
>
> Thanks for reviewing my code. Replies to specific comments below:
>
> On Mon, Apr 26, 2010 at 4:10 PM, Paul Querna <pa...@querna.org> wrote:
>
>> On Mon, Apr 26, 2010 at 10:58 AM,  <or...@apache.org> wrote:
>> > Author: oremj
>> > Date: Mon Apr 26 17:58:12 2010
>> > New Revision: 938156
>> >
>> > URL: http://svn.apache.org/viewvc?rev=938156&view=rev
>> > Log:
>> > Add Enomaly ECP Driver.
>> >
>> > Author:    Viktor Stanchev <vi...@enomaly.com>
>> > Signed-off-by: Jeremy Orem <je...@gmail.com>
>> ...snip...
>> > +    def request(self, *args, **kwargs):
>> > +        return super(ECPConnection, self).request(*args, **kwargs)
>>
>> Any reason this needs to be in there?
>>
>>
> It seems like it doesn't do anything. I guess I was copying and pasting and
> then deleting things that were unnecessary and that was left over. I'll
> remove it.
>
>
>> ....
>> > +    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
>>
>> I thought that httplib would support doing this, but it appears it
>> doesn't support the multipart form data encoding:
>> http://bugs.python.org/issue3244
>>
>> I personally kinda wish the boundary was randomly generated, and
>> checked to make sure its content was not inside the content.
>>
>> I guess i would at least be more happy with something like this:
>> boundary = os.urandom(16).encode('hex')
>>
>> (Related, BOUNDARY isn't really a good style, it should be lower case,
>> and I am concerned that this code was copied from
>> <http://code.activestate.com/recipes/146306/> without any attribution)
>>
>>
> You seem to be correct that the code is from there. Sorry about that. I
> didn't copy it intentionally. It was given to me without any context by
> someone when I was trying to figure out why the request wasn't working
> without it. I don't mind adding a link/reference in the code to indicate its
> source. I'll look into making the boundary random, but the benefit is very
> little.
>
>
>> .... snip ....
>> > +class ECPNodeDriver(NodeDriver):
>> > +    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
>>
>> Shouldn't these just use the built in key / secret variables, and let
>> the default NodeDriver __init__ do this?
>>
>>
> That would make sense. I'm not sure why I did this. Maybe
> because ECPConnection doesn't have a method called "connect", but I guess I
> can just add to it "def connect(self): pass"
>
>
>> ... snip ...
>> > +    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,
>> > +        )
>>
>> This seems less than ideal, setting public_ip and private_ip to the
>> same lists.  It likely needs code similiar to the slicehost driver,
>> which splits the two based on the published private subnets.  We can
>> likely move that function from the slicehost driver to a util area in
>> the base library.
>>
>>
> We don't distinguish between public and private IPs either, so that would
> be nice. Meanwhile, can I copy the function?
>
>
>> It also does not throw InvalidCredsException anywhere, which is bad
>> because then we can't tell if there is a error in the username or
>> password -- so parse_error should be extended to detect those errors
>> and throw the right exception.
>>
>>
> I'll add the error handling.
>
>
>> One last thing, uuid is imported, but unused, so pyflakes complains.
>>
>>
> I'll remove it. I didn't notice that.
>
>
>> Thanks for getting the driver in,
>>
>> Paul
>>
>
> No problem. I'll work on fixing everything mentioned. I'll assume it's ok
> to copy the code from slicehost unless you tell me otherwise.
>
> By the way, I would appreciate it if you could provide some testing code
> that I can run to make sure the driver works on a live server. I would
> imagine I'm not the first person to need to do that.
>
> - Viktor
>

Re: [libcloud] Re: svn commit: r938156 - in /incubator/libcloud/trunk: libcloud/ libcloud/drivers/ test/ test/fixtures/ecp/

Posted by Viktor Stanchev <vi...@enomaly.com>.
Paul,

Thanks for reviewing my code. Replies to specific comments below:

On Mon, Apr 26, 2010 at 4:10 PM, Paul Querna <pa...@querna.org> wrote:

> On Mon, Apr 26, 2010 at 10:58 AM,  <or...@apache.org> wrote:
> > Author: oremj
> > Date: Mon Apr 26 17:58:12 2010
> > New Revision: 938156
> >
> > URL: http://svn.apache.org/viewvc?rev=938156&view=rev
> > Log:
> > Add Enomaly ECP Driver.
> >
> > Author:    Viktor Stanchev <vi...@enomaly.com>
> > Signed-off-by: Jeremy Orem <je...@gmail.com>
> ...snip...
> > +    def request(self, *args, **kwargs):
> > +        return super(ECPConnection, self).request(*args, **kwargs)
>
> Any reason this needs to be in there?
>
>
It seems like it doesn't do anything. I guess I was copying and pasting and
then deleting things that were unnecessary and that was left over. I'll
remove it.


> ....
> > +    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
>
> I thought that httplib would support doing this, but it appears it
> doesn't support the multipart form data encoding:
> http://bugs.python.org/issue3244
>
> I personally kinda wish the boundary was randomly generated, and
> checked to make sure its content was not inside the content.
>
> I guess i would at least be more happy with something like this:
> boundary = os.urandom(16).encode('hex')
>
> (Related, BOUNDARY isn't really a good style, it should be lower case,
> and I am concerned that this code was copied from
> <http://code.activestate.com/recipes/146306/> without any attribution)
>
>
You seem to be correct that the code is from there. Sorry about that. I
didn't copy it intentionally. It was given to me without any context by
someone when I was trying to figure out why the request wasn't working
without it. I don't mind adding a link/reference in the code to indicate its
source. I'll look into making the boundary random, but the benefit is very
little.


> .... snip ....
> > +class ECPNodeDriver(NodeDriver):
> > +    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
>
> Shouldn't these just use the built in key / secret variables, and let
> the default NodeDriver __init__ do this?
>
>
That would make sense. I'm not sure why I did this. Maybe
because ECPConnection doesn't have a method called "connect", but I guess I
can just add to it "def connect(self): pass"


> ... snip ...
> > +    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,
> > +        )
>
> This seems less than ideal, setting public_ip and private_ip to the
> same lists.  It likely needs code similiar to the slicehost driver,
> which splits the two based on the published private subnets.  We can
> likely move that function from the slicehost driver to a util area in
> the base library.
>
>
We don't distinguish between public and private IPs either, so that would be
nice. Meanwhile, can I copy the function?


> It also does not throw InvalidCredsException anywhere, which is bad
> because then we can't tell if there is a error in the username or
> password -- so parse_error should be extended to detect those errors
> and throw the right exception.
>
>
I'll add the error handling.


> One last thing, uuid is imported, but unused, so pyflakes complains.
>
>
I'll remove it. I didn't notice that.


> Thanks for getting the driver in,
>
> Paul
>

No problem. I'll work on fixing everything mentioned. I'll assume it's ok to
copy the code from slicehost unless you tell me otherwise.

By the way, I would appreciate it if you could provide some testing code
that I can run to make sure the driver works on a live server. I would
imagine I'm not the first person to need to do that.

- Viktor

[libcloud] Re: svn commit: r938156 - in /incubator/libcloud/trunk: libcloud/ libcloud/drivers/ test/ test/fixtures/ecp/

Posted by Paul Querna <pa...@querna.org>.
On Mon, Apr 26, 2010 at 10:58 AM,  <or...@apache.org> wrote:
> Author: oremj
> Date: Mon Apr 26 17:58:12 2010
> New Revision: 938156
>
> URL: http://svn.apache.org/viewvc?rev=938156&view=rev
> Log:
> Add Enomaly ECP Driver.
>
> Author:    Viktor Stanchev <vi...@enomaly.com>
> Signed-off-by: Jeremy Orem <je...@gmail.com>
...snip...
> +    def request(self, *args, **kwargs):
> +        return super(ECPConnection, self).request(*args, **kwargs)

Any reason this needs to be in there?

....
> +    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

I thought that httplib would support doing this, but it appears it
doesn't support the multipart form data encoding:
http://bugs.python.org/issue3244

I personally kinda wish the boundary was randomly generated, and
checked to make sure its content was not inside the content.

I guess i would at least be more happy with something like this:
boundary = os.urandom(16).encode('hex')

(Related, BOUNDARY isn't really a good style, it should be lower case,
and I am concerned that this code was copied from
<http://code.activestate.com/recipes/146306/> without any attribution)

.... snip ....
> +class ECPNodeDriver(NodeDriver):
> +    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

Shouldn't these just use the built in key / secret variables, and let
the default NodeDriver __init__ do this?

... snip ...
> +    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,
> +        )

This seems less than ideal, setting public_ip and private_ip to the
same lists.  It likely needs code similiar to the slicehost driver,
which splits the two based on the published private subnets.  We can
likely move that function from the slicehost driver to a util area in
the base library.

It also does not throw InvalidCredsException anywhere, which is bad
because then we can't tell if there is a error in the username or
password -- so parse_error should be extended to detect those errors
and throw the right exception.

One last thing, uuid is imported, but unused, so pyflakes complains.

Thanks for getting the driver in,

Paul