From a92c34643c42bde384c6cc7012c3af5a3ee9995d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Toma=C5=BE=20Muraus?= <kami@k5-storitve.net>
Date: Sun, 1 Aug 2010 13:48:01 -0700
Subject: [PATCH 1/6] Don't append ? to the url, if there are no paramaters defined

---
 libcloud/base.py |    8 ++++++--
 1 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/libcloud/base.py b/libcloud/base.py
index 1a61b06..4a941be 100644
--- a/libcloud/base.py
+++ b/libcloud/base.py
@@ -422,8 +422,12 @@ class ConnectionKey(object):
         if data != '':
             data = self.encode_data(data)
         headers.update({'Content-Length': str(len(data))})
-        url = '?'.join((action, urllib.urlencode(params)))
-
+        
+        if params:
+            url = '?'.join((action, urllib.urlencode(params)))
+        else:
+            url = action
+        
         # Removed terrible hack...this a less-bad hack that doesn't execute a
         # request twice, but it's still a hack.
         self.connect()
-- 
1.7.0.4


From 770908504fb82ddff1f3e83994b035bc3f614e97 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Toma=C5=BE=20Muraus?= <kami@k5-storitve.net>
Date: Sun, 1 Aug 2010 22:23:28 -0700
Subject: [PATCH 2/6] Add ElasticHosts driver (http://www.elastichosts.com)

---
 libcloud/drivers/__init__.py     |    1 +
 libcloud/drivers/elastichosts.py |  415 ++++++++++++++++++++++++++++++++++++++
 libcloud/providers.py            |    2 +
 libcloud/types.py                |    1 +
 4 files changed, 419 insertions(+), 0 deletions(-)
 create mode 100644 libcloud/drivers/elastichosts.py

diff --git a/libcloud/drivers/__init__.py b/libcloud/drivers/__init__.py
index d4c195a..4e957c2 100644
--- a/libcloud/drivers/__init__.py
+++ b/libcloud/drivers/__init__.py
@@ -21,6 +21,7 @@ __all__ = [
     'dummy',
     'ec2',
     'ecp',
+    'elastichosts',
     'gogrid',
     'ibm_sbc',
     'linode',
diff --git a/libcloud/drivers/elastichosts.py b/libcloud/drivers/elastichosts.py
new file mode 100644
index 0000000..475323b
--- /dev/null
+++ b/libcloud/drivers/elastichosts.py
@@ -0,0 +1,415 @@
+# 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.
+# Copyright 2009 RedRata Ltd
+"""
+ElasticHosts Driver
+"""
+import re
+import time
+import base64
+
+from libcloud.types import Provider, NodeState, InvalidCredsException
+from libcloud.base import ConnectionUserAndKey, Response, NodeAuthPassword
+from libcloud.base import NodeDriver, NodeSize, Node, NodeLocation
+from libcloud.base import NodeImage
+
+# 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
+
+# API end-points
+API_ENDPOINTS = {
+    'uk-1': {
+        'name': 'London Peer 1',
+        'country': 'United Kingdom',
+        'host': 'api.lon-p.elastichosts.com'
+    },
+     'uk-2': {
+        'name': 'London BlueSquare',
+        'country': 'United Kingdom',
+        'host': 'api.lon-b.elastichosts.com'
+    },
+     'us-1': {
+        'name': 'San Antonio Peer 1',
+        'country': 'United States',
+        'host': 'api.sat-p.elastichosts.com'
+    },
+}
+
+# Default API end-point. This should be changed if the accounts are located elsewhere.
+DEFAULT_ENDPOINT = 'us-1'
+
+# ElasticHosts doesn't specify special instance types, so I just specified
+# some plans based on the pricing page (http://www.elastichosts.com/cloud-hosting/pricing)
+# and other provides.
+#
+# Basically for CPU any value between 500Mhz and 20000Mhz should work,
+# 256MB to 8192MB for ram and 1GB to 2TB for disk.
+INSTANCE_TYPES = {
+    'small': {
+        'id': 'small',
+        'name': 'Small instance',
+        'cpu': 2000,
+        'memory': 1700,
+        'disk': 160,
+        'bandwidth': None,
+    },
+    'large': {
+        'id': 'large',
+        'name': 'Large instance',
+        'cpu': 4000,
+        'memory': 7680,
+        'disk': 850,
+        'bandwidth': None,
+    },
+    'extra-large': {
+        'id': 'extra-large',
+        'name': 'Extra Large instance',
+        'cpu': 8000,
+        'memory': 8192,
+        'disk': 1690,
+        'bandwidth': None,
+    },
+    'high-cpu-medium': {
+        'id': 'high-cpu-medium',
+        'name': 'High-CPU Medium instance',
+        'cpu': 5000,
+        'memory': 1700,
+        'disk': 350,
+        'bandwidth': None,
+    },
+    'high-cpu-extra-large': {
+        'id': 'high-cpu-extra-large',
+        'name': 'High-CPU Extra Large instance',
+        'cpu': 20000,
+        'memory': 7168,
+        'disk': 1690,
+        'bandwidth': None,
+    },
+}
+
+# Retrieved from http://www.elastichosts.com/cloud-hosting/api
+STANDARD_DRIVES = {
+    'cf82519b-01a0-4247-aff5-a2dadf4401ad': {
+        'uuid': 'cf82519b-01a0-4247-aff5-a2dadf4401ad',
+        'description': 'Debian Linux 4.0: Base system without X',
+        'size_gunzipped': '1GB',
+    },
+    'e6111e4c-67af-4438-b1bc-189747d5a8e5': {
+        'uuid': 'e6111e4c-67af-4438-b1bc-189747d5a8e5',
+        'description': 'Debian Linux 5.0: Base system without X',
+        'size_gunzipped': '1GB',
+    },
+    'bf1d943e-2a55-46bb-a8c7-6099e44a3dde': {
+        'uuid': 'bf1d943e-2a55-46bb-a8c7-6099e44a3dde',
+        'description': 'Ubuntu Linux 8.10: Base system with X',
+        'size_gunzipped': '3GB',
+    },
+    '757586d5-f1e9-4d9c-b215-5a391c9a24bf': {
+        'uuid': '757586d5-f1e9-4d9c-b215-5a391c9a24bf',
+        'description': 'Ubuntu Linux 9.04: Base system with X',
+        'size_gunzipped': '3GB',
+    },
+    'b9d0eb72-d273-43f1-98e3-0d4b87d372c0': {
+        'uuid': 'b9d0eb72-d273-43f1-98e3-0d4b87d372c0',
+        'description': 'Windows Web Server 2008',
+        'size_gunzipped': '13GB',
+    },
+    '30824e97-05a4-410c-946e-2ba5a92b07cb': {
+        'uuid': '30824e97-05a4-410c-946e-2ba5a92b07cb',
+        'description': 'Windows Web Server 2008 R2',
+        'size_gunzipped': '13GB',
+    },
+    '9ecf810e-6ad1-40ef-b360-d606f0444671': {
+        'uuid': '9ecf810e-6ad1-40ef-b360-d606f0444671',
+        'description': 'Windows Web Server 2008 R2 + SQL Server',
+        'size_gunzipped': '13GB',
+    },
+    '10a88d1c-6575-46e3-8d2c-7744065ea530': {
+        'uuid': '10a88d1c-6575-46e3-8d2c-7744065ea530',
+        'description': 'Windows Server 2008 Standard R2',
+        'size_gunzipped': '13GB',
+    },
+    '2567f25c-8fb8-45c7-95fc-bfe3c3d84c47': {
+        'uuid': '2567f25c-8fb8-45c7-95fc-bfe3c3d84c47',
+        'description': 'Windows Server 2008 Standard R2 + SQL Server',
+        'size_gunzipped': '13GB',
+    },
+}
+
+NODE_STATE_MAP = {
+    'active': NodeState.RUNNING,
+    'dead': NodeState.TERMINATED,
+    'dumped': NodeState.TERMINATED,
+}
+
+# Default timeout (in seconds) for the drive imaging process
+IMAGING_TIMEOUT = 10 * 60
+
+class ElasticHostsException(Exception):
+    """
+    Exception class for ElasticHosts driver
+    """
+
+    def __str__(self):
+        return self.args[0]
+
+    def __repr__(self):
+        return "<ElasticHostsException '%s'>" % (self.args[0])
+
+class ElasticHostsResponse(Response):
+    def success(self):
+        if self.status == 401:
+            raise InvalidCredsException()
+
+        return self.status >= 200 and self.status <= 299
+    
+    def parse_body(self):
+        if not self.body:
+            return self.body
+        
+        try:
+            data = json.loads(self.body)
+        except ValueError:
+            raise ElasticHostsException('Could not parse body: %s')
+
+        return data
+    
+    def parse_error(self):
+        error_header = self.headers.get('x-elastic-error', '')
+        message = self.body
+
+        return 'X-Elastic-Error: %s (%s)' % (error_header, self.body.strip())
+
+class ElasticHostsConnection(ConnectionUserAndKey):
+    """
+    Connection class for the ElasticHosts driver
+    """
+
+    host = API_ENDPOINTS[DEFAULT_ENDPOINT]['host']
+    responseCls = ElasticHostsResponse
+
+    def add_default_headers(self, headers):
+        headers['Accept'] = 'application/json'
+        headers['Content-Type'] = 'application/json'
+
+        headers['Authorization'] = 'Basic %s' % (base64.b64encode('%s:%s' % (self.user_id, self.key)))
+
+        return headers
+    
+class ElasticHostsNodeSize(NodeSize):
+    def __init__(self, id, name, cpu, ram, disk, bandwidth, price, driver):
+        self.id = id
+        self.name = name
+        self.cpu = cpu
+        self.ram = ram
+        self.disk = disk
+        self.bandwidth = bandwidth
+        self.price = price
+        self.driver = driver
+
+    def __repr__(self):
+        return (('<NodeSize: id=%s, name=%s, cpu=%s, ram=%s disk=%s bandwidth=%s '
+                 'price=%s driver=%s ...>')
+                % (self.id, self.name, self.cpu, self.ram, self.disk, self.bandwidth,
+                   self.price, self.driver.name))
+    
+class ElasticHostsNodeDriver(NodeDriver):
+    """
+    ElasticHosts node driver
+    """
+
+    type = Provider.ELASTICHOSTS
+    name = 'ElasticHosts'
+    connectionCls = ElasticHostsConnection
+
+    def reboot_node(self, node):
+        # Reboots the node
+        response = self.connection.request(action = '/servers/%s/reset' % (node.id),
+                                           method = 'POST')
+    
+        return response.status == 204
+    
+    def destroy_node(self, node):
+        # Kills the server immediately
+        response = self.connection.request(action = '/servers/%s/destroy' % (node.id),
+                                           method = 'POST')
+    
+        return response.status == 204
+    
+    def list_images(self, location=None):
+        # Returns a list of available pre-installed system drive images
+        images = []
+        for key, value in STANDARD_DRIVES.iteritems():
+            image = NodeImage(id = value['uuid'], name = value['description'], driver = self.connection.driver,
+                               extra = {'size_gunzipped': value['size_gunzipped']})
+            images.append(image)
+        
+        return images
+    
+    def list_sizes(self, location=None):
+        sizes = []
+        for key, value in INSTANCE_TYPES.iteritems():
+            size = ElasticHostsNodeSize(id = value['id'], name = value['name'], cpu = value['cpu'], ram = value['memory'],
+                            disk = value['disk'], bandwidth = value['bandwidth'], price = '',
+                            driver = self.connection.driver)
+            sizes.append(size)
+        
+        return sizes
+    
+    def list_nodes(self):
+        # Returns a list of active (running) nodes
+        response = self.connection.request(action = '/servers/info').object
+        
+        nodes = []
+        for data in response:
+            node = self._to_node(data)
+            nodes.append(node)
+        
+        return nodes
+    
+    def create_node(self, **kwargs):
+        """Creates a ElasticHosts instance
+
+        See L{NodeDriver.create_node} for more keyword args.
+
+        @keyword    name: String with a name for this new node (required)
+        @type       name: C{string}
+        
+        @keyword    smp: Number of virtual processors or None to calculate based on the cpu speed
+        @type       smp: C{int}
+        
+        @keyword    nic_model: e1000, rtl8139 or virtio (is not specified, e1000 is used)
+        @type       nic_model: C{string}
+
+        @keyword    vnc_password: If not set, VNC access is disabled.
+        @type       vnc_password: C{bool}
+        """
+        size = kwargs['size']
+        image = kwargs['image']
+        smp = kwargs.get('smp', 'auto')
+        nic_model = kwargs.get('nic_model', 'e1000')
+        vnc_password = kwargs.get('vnc_password', None)
+        
+        if nic_model not in ['e1000', 'rtl8139', 'virtio']:
+            raise ElasticHostsException('Invalid NIC model specified')
+        
+        # First we create a drive with the specified size
+        drive_data = {}
+        drive_data.update({'name': kwargs['name'], 'size': '%sG' % (kwargs['size'].disk)})
+        
+        response = self.connection.request(action = '/drives/create', data = json.dumps(drive_data),
+                                           method = 'POST').object
+                                           
+        if not response:
+            raise ElasticHostsException('Drive creation failed')
+        
+        drive_uuid = response['drive']
+        
+        # Then we image the selected pre-installed system drive onto it
+        response = self.connection.request(action = '/drives/%s/image/%s/gunzip' % (drive_uuid, image.id),
+                                           method = 'POST')
+        
+        
+        if response.status != 204:
+            raise ElasticHostsException('Drive imaging failed')
+        
+        # We wait until the drive is imaged and then boot up the node (in most cases, the imaging process
+        # shouldn't take longer then a few minutes)
+        response = self.connection.request(action = '/drives/%s/info' % (drive_uuid)).object
+        imaging_start = time.time()
+        while response.has_key('imaging'):
+            response = self.connection.request(action = '/drives/%s/info' % (drive_uuid)).object
+            
+            elapsed_time = time.time() - imaging_start
+            if response.has_key('imaging') and elapsed_time >= IMAGING_TIMEOUT:
+                raise ElasticHostsException('Drive imaging timed out')
+            
+            time.sleep(1)
+        
+        node_data = {}
+        node_data.update({'name': kwargs['name'], 'cpu': size.cpu, 'mem': size.ram, 'ide:0:0': drive_uuid,
+                          'boot': 'ide:0:0'})
+        
+        node_data.update({'nic:0:model': nic_model, 'nic:0:dhcp': 'auto'})
+        
+        if vnc_password:
+            node_data.update({'vnc:ip': 'auto', 'vnc:password': vnc_password})
+
+        response = self.connection.request(action = '/servers/create', data = json.dumps(node_data),
+                                           method = 'POST')
+        
+        return (response.status == 200 and response.body != '')
+    
+    # Extension methods
+    def ex_set_node_configuration(self, node, **kwargs):
+        # Changes the configuration of the running server
+        valid_keys = ('^name$', '^parent$', '^cpu$', '^smp$', '^mem$', '^boot$', '^nic:0:model$', '^nic:0:dhcp',
+                      '^nic:1:model$', '^nic:1:vlan$', '^nic:1:mac$', '^vnc:ip$', '^vnc:password$', '^vnc:tls',
+                      '^ide:[0-1]:[0-1](:media)?$', '^scsi:0:[0-7](:media)?$', '^block:[0-7](:media)?$')
+        
+        invalid_keys = []
+        for key in kwargs.keys():
+            matches = False
+            for regex in valid_keys:
+                if re.match(regex, key):
+                    matches = True
+                    break
+            
+            if not matches:
+                invalid_keys.append(key)
+                                 
+        if invalid_keys:
+            raise ElasticHostsException('Invalid configuration key specified: %s' % (',' .join(invalid_keys)))
+
+        response = self.connection.request(action = '/servers/%s/set' % (node.id), data = json.dumps(kwargs),
+                                           method = 'POST')
+
+        return (response.status == 200 and response.body != '')
+    
+    def ex_shutdown_node(self, node):
+        # Sends the ACPI power-down event
+        response = self.connection.request(action = '/servers/%s/shutdown' % (node.id),
+                                           method = 'POST')
+    
+        return response.status == 204
+    
+    def ex_destroy_drive(self, drive_uuid):
+        # Deletes a drive
+        response = self.connection.request(action = '/drives/%s/destroy' % (drive_uuid),
+                                           method = 'POST')
+    
+        return response.status == 204
+    
+    # Helper methods
+    def _to_node(self, data):
+        try:
+            state = NODE_STATE_MAP[data['status']]
+        except KeyError:
+            state = NodeState.UNKNOWN
+            
+        extra = {'cpu': data['cpu'], 'smp': data['smp'], 'mem': data['mem'], 'started': data['started']}
+        
+        if data.has_key('vnc:ip') and data.has_key('vnc:password'):
+            extra.update({'vnc_ip': data['vnc:ip'], 'vnc_password': data['vnc:password']})
+            
+        node = Node(id = data['server'], name = data['name'], state =  state,
+                    public_ip = [data['nic:0:dhcp']], private_ip = None, driver = self.connection.driver,
+                    extra = extra)
+        
+        return node
diff --git a/libcloud/providers.py b/libcloud/providers.py
index 34ad80f..23f382c 100644
--- a/libcloud/providers.py
+++ b/libcloud/providers.py
@@ -29,6 +29,8 @@ DRIVERS = {
         ('libcloud.drivers.ec2', 'EC2USWestNodeDriver'),
     Provider.ECP:
         ('libcloud.drivers.ecp', 'ECPNodeDriver'),
+    Provider.ELASTICHOSTS:
+        ('libcloud.drivers.elastichosts', 'ElasticHostsNodeDriver'),
     Provider.GOGRID:
         ('libcloud.drivers.gogrid', 'GoGridNodeDriver'),
     Provider.RACKSPACE:
diff --git a/libcloud/types.py b/libcloud/types.py
index ef35c1a..41054ef 100644
--- a/libcloud/types.py
+++ b/libcloud/types.py
@@ -56,6 +56,7 @@ class Provider(object):
     IBM = 15
     OPENNEBULA = 16
     DREAMHOST = 17
+    ELASTICHOSTS = 18
 
 class NodeState(object):
     """
-- 
1.7.0.4


From 624e18a423c3ab2825de43813988f7d1f79901ed Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Toma=C5=BE=20Muraus?= <kami@k5-storitve.net>
Date: Sun, 1 Aug 2010 23:10:59 -0700
Subject: [PATCH 3/6] Better handling od multiple ip addresses

---
 libcloud/drivers/elastichosts.py |    7 ++++++-
 1 files changed, 6 insertions(+), 1 deletions(-)

diff --git a/libcloud/drivers/elastichosts.py b/libcloud/drivers/elastichosts.py
index 475323b..0c204d7 100644
--- a/libcloud/drivers/elastichosts.py
+++ b/libcloud/drivers/elastichosts.py
@@ -403,13 +403,18 @@ class ElasticHostsNodeDriver(NodeDriver):
         except KeyError:
             state = NodeState.UNKNOWN
             
+        if isinstance(data['nic:0:dhcp'], list):
+            public_ip = data['nic:0:dhcp']
+        else:
+            public_ip = [data['nic:0:dhcp']]
+            
         extra = {'cpu': data['cpu'], 'smp': data['smp'], 'mem': data['mem'], 'started': data['started']}
         
         if data.has_key('vnc:ip') and data.has_key('vnc:password'):
             extra.update({'vnc_ip': data['vnc:ip'], 'vnc_password': data['vnc:password']})
             
         node = Node(id = data['server'], name = data['name'], state =  state,
-                    public_ip = [data['nic:0:dhcp']], private_ip = None, driver = self.connection.driver,
+                    public_ip = public_ip, private_ip = None, driver = self.connection.driver,
                     extra = extra)
         
         return node
-- 
1.7.0.4


From 5dc423b666a8266ca23bed753dec86c40c892a9b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Toma=C5=BE=20Muraus?= <kami@k5-storitve.net>
Date: Sun, 1 Aug 2010 23:38:54 -0700
Subject: [PATCH 4/6] Return node instance upon node creation

---
 libcloud/drivers/elastichosts.py |   11 +++++++++--
 1 files changed, 9 insertions(+), 2 deletions(-)

diff --git a/libcloud/drivers/elastichosts.py b/libcloud/drivers/elastichosts.py
index 0c204d7..ebf68cb 100644
--- a/libcloud/drivers/elastichosts.py
+++ b/libcloud/drivers/elastichosts.py
@@ -354,8 +354,15 @@ class ElasticHostsNodeDriver(NodeDriver):
         response = self.connection.request(action = '/servers/create', data = json.dumps(node_data),
                                            method = 'POST')
         
-        return (response.status == 200 and response.body != '')
-    
+        nodes = response.object
+        
+        if len(nodes) == 1:
+            nodes = self._to_node(nodes[0])
+        else:
+            nodes = [self._to_node(node) for node in nodes]
+        
+        return nodes
+
     # Extension methods
     def ex_set_node_configuration(self, node, **kwargs):
         # Changes the configuration of the running server
-- 
1.7.0.4


From 82d195ea2e9b78b2a5f5ef9ee032ada3bba92a05 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Toma=C5=BE=20Muraus?= <kami@k5-storitve.net>
Date: Sun, 1 Aug 2010 23:53:11 -0700
Subject: [PATCH 5/6] Add tests for elastichosts

---
 test/fixtures/elastichosts/drives_create.json  |   12 +++
 test/fixtures/elastichosts/drives_info.json    |   12 +++
 test/fixtures/elastichosts/servers_create.json |   27 ++++++
 test/fixtures/elastichosts/servers_info.json   |   27 ++++++
 test/test_elastichosts.py                      |  104 ++++++++++++++++++++++++
 5 files changed, 182 insertions(+), 0 deletions(-)
 create mode 100644 test/fixtures/elastichosts/drives_create.json
 create mode 100644 test/fixtures/elastichosts/drives_info.json
 create mode 100644 test/fixtures/elastichosts/servers_create.json
 create mode 100644 test/fixtures/elastichosts/servers_info.json
 create mode 100644 test/test_elastichosts.py

diff --git a/test/fixtures/elastichosts/drives_create.json b/test/fixtures/elastichosts/drives_create.json
new file mode 100644
index 0000000..659ea41
--- /dev/null
+++ b/test/fixtures/elastichosts/drives_create.json
@@ -0,0 +1,12 @@
+{
+  "drive": "0012e24a-6eae-4279-9912-3432f698cec8", 
+  "encryption:cipher": "aes-xts-plain", 
+  "name": "test drive", 
+  "read:bytes": "4096", 
+  "read:requests": "1", 
+  "size": 10737418240, 
+  "status": "active", 
+  "user": "2164ce57-591c-43ee-ade5-e2fe0ee13c3e", 
+  "write:bytes": "4096", 
+  "write:requests": "1"
+}
\ No newline at end of file
diff --git a/test/fixtures/elastichosts/drives_info.json b/test/fixtures/elastichosts/drives_info.json
new file mode 100644
index 0000000..659ea41
--- /dev/null
+++ b/test/fixtures/elastichosts/drives_info.json
@@ -0,0 +1,12 @@
+{
+  "drive": "0012e24a-6eae-4279-9912-3432f698cec8", 
+  "encryption:cipher": "aes-xts-plain", 
+  "name": "test drive", 
+  "read:bytes": "4096", 
+  "read:requests": "1", 
+  "size": 10737418240, 
+  "status": "active", 
+  "user": "2164ce57-591c-43ee-ade5-e2fe0ee13c3e", 
+  "write:bytes": "4096", 
+  "write:requests": "1"
+}
\ No newline at end of file
diff --git a/test/fixtures/elastichosts/servers_create.json b/test/fixtures/elastichosts/servers_create.json
new file mode 100644
index 0000000..72b6b48
--- /dev/null
+++ b/test/fixtures/elastichosts/servers_create.json
@@ -0,0 +1,27 @@
+[
+  {
+    "boot": "ide:0:0", 
+    "cpu": 2000, 
+    "ide:0:0": "b6049e7a-aa1b-47f9-b21d-cdf2354e28d3", 
+    "ide:0:0:read:bytes": "299696128", 
+    "ide:0:0:read:requests": "73168", 
+    "ide:0:0:write:bytes": "321044480", 
+    "ide:0:0:write:requests": "78380", 
+    "mem": 1024, 
+    "name": "test api node", 
+    "nic:0:block": "tcp/21 tcp/22 tcp/23 tcp/25", 
+    "nic:0:dhcp": ["1.2.3.4", "1.2.3.5"], 
+    "nic:0:model": "virtio", 
+    "rx": 679560, 
+    "rx:packets": 644, 
+    "server": "b605ca90-c3e6-4cee-85f8-a8ebdf8f9903", 
+    "smp": 1, 
+    "started": 1280723696, 
+    "status": "active", 
+    "tx": 21271, 
+    "tx:packets": "251", 
+    "user": "2164ce57-591a-43ee-ade5-e2fe0ee13c3f", 
+    "vnc:ip": "216.151.208.174", 
+    "vnc:password": "testvncpass"
+  }
+]
\ No newline at end of file
diff --git a/test/fixtures/elastichosts/servers_info.json b/test/fixtures/elastichosts/servers_info.json
new file mode 100644
index 0000000..72b6b48
--- /dev/null
+++ b/test/fixtures/elastichosts/servers_info.json
@@ -0,0 +1,27 @@
+[
+  {
+    "boot": "ide:0:0", 
+    "cpu": 2000, 
+    "ide:0:0": "b6049e7a-aa1b-47f9-b21d-cdf2354e28d3", 
+    "ide:0:0:read:bytes": "299696128", 
+    "ide:0:0:read:requests": "73168", 
+    "ide:0:0:write:bytes": "321044480", 
+    "ide:0:0:write:requests": "78380", 
+    "mem": 1024, 
+    "name": "test api node", 
+    "nic:0:block": "tcp/21 tcp/22 tcp/23 tcp/25", 
+    "nic:0:dhcp": ["1.2.3.4", "1.2.3.5"], 
+    "nic:0:model": "virtio", 
+    "rx": 679560, 
+    "rx:packets": 644, 
+    "server": "b605ca90-c3e6-4cee-85f8-a8ebdf8f9903", 
+    "smp": 1, 
+    "started": 1280723696, 
+    "status": "active", 
+    "tx": 21271, 
+    "tx:packets": "251", 
+    "user": "2164ce57-591a-43ee-ade5-e2fe0ee13c3f", 
+    "vnc:ip": "216.151.208.174", 
+    "vnc:password": "testvncpass"
+  }
+]
\ No newline at end of file
diff --git a/test/test_elastichosts.py b/test/test_elastichosts.py
new file mode 100644
index 0000000..de3e676
--- /dev/null
+++ b/test/test_elastichosts.py
@@ -0,0 +1,104 @@
+# 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.
+# Copyright 2009 RedRata Ltd
+
+import sys
+import unittest
+import httplib
+
+from libcloud.drivers.elastichosts import ElasticHostsNodeDriver
+from test import MockHttp, TestCaseMixin
+from test.file_fixtures import FileFixtures
+
+class ElasticHostsTestCase(unittest.TestCase, TestCaseMixin):
+    def setUp(self):
+        ElasticHostsNodeDriver.connectionCls.conn_classes = (None,
+                                                            ElasticHostsHttp)
+        self.driver = ElasticHostsNodeDriver('foo', 'bar')
+
+    def test_list_nodes(self):
+        nodes = self.driver.list_nodes()
+        self.assertTrue(isinstance(nodes, list))
+        self.assertEqual(len(nodes), 1)
+        
+        node = nodes[0]
+        self.assertEqual(node.public_ip[0], "1.2.3.4")
+        self.assertEqual(node.public_ip[1], "1.2.3.5")
+        self.assertEqual(node.extra['smp'], 1)
+
+    def test_list_sizes(self):
+        images = self.driver.list_sizes()
+        self.assertEqual(len(images), 5)
+        image = images[0]
+        self.assertEqual(image.id, 'small')
+        self.assertEqual(image.name, 'Small instance')
+        self.assertEqual(image.cpu, 2000)
+        self.assertEqual(image.ram, 1700)
+        self.assertEqual(image.disk, 160)
+
+    def test_list_images(self):
+        sizes = self.driver.list_images()
+        self.assertEqual(len(sizes), 9)
+        size = sizes[0]
+        self.assertEqual(size.id, '757586d5-f1e9-4d9c-b215-5a391c9a24bf')
+        self.assertEqual(size.name, 'Ubuntu Linux 9.04: Base system with X')
+        
+    def test_list_locations_response(self):
+        pass
+
+    def test_reboot_node(self):
+        node = self.driver.list_nodes()[0]
+        self.assertTrue(self.driver.reboot_node(node))
+
+    def test_destroy_node(self):
+        node = self.driver.list_nodes()[0]
+        self.assertTrue(self.driver.destroy_node(node))
+
+    def test_create_node(self):
+        size = self.driver.list_sizes()[0]
+        image = self.driver.list_images()[0]
+        self.assertTrue(self.driver.create_node(name="api.ivan.net.nz", image=image, size=size))
+
+class ElasticHostsHttp(MockHttp):
+
+    fixtures = FileFixtures('elastichosts')
+    
+    def _servers_b605ca90_c3e6_4cee_85f8_a8ebdf8f9903_reset(self, method, url, body, headers):
+         return (httplib.NO_CONTENT, body, {}, httplib.responses[httplib.NO_CONTENT])
+    
+    def _servers_b605ca90_c3e6_4cee_85f8_a8ebdf8f9903_destroy(self, method, url, body, headers):
+         return (httplib.NO_CONTENT, body, {}, httplib.responses[httplib.NO_CONTENT])
+    
+    def _drives_create(self, method, url, body, headers):
+        body = self.fixtures.load('drives_create.json')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+    
+    def _drives_0012e24a_6eae_4279_9912_3432f698cec8_image_757586d5_f1e9_4d9c_b215_5a391c9a24bf_gunzip(self, method, url, body, headers):
+        return (httplib.NO_CONTENT, body, {}, httplib.responses[httplib.NO_CONTENT])
+
+    def _drives_0012e24a_6eae_4279_9912_3432f698cec8_info(self, method, url, body, headers):
+        body = self.fixtures.load('drives_info.json')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+    
+    def _servers_create(self, method, url, body, headers):
+        body = self.fixtures.load('servers_create.json')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _servers_info(self, method, url, body, headers):
+        body = self.fixtures.load('servers_info.json')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+if __name__ == '__main__':
+    sys.exit(unittest.main())
-- 
1.7.0.4


From 758d549abea0bf3387dc987ecedb2cd6cd51398c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Toma=C5=BE=20Muraus?= <kami@k5-storitve.net>
Date: Mon, 2 Aug 2010 21:58:08 -0700
Subject: [PATCH 6/6] Refactored the ElasticHosts driver so that now there are 3 separate classes -
 one for each API endpoint (uses the same style as the EC2 driver).

---
 libcloud/drivers/elastichosts.py               |   94 +++++++++++++++++-------
 libcloud/providers.py                          |    8 ++-
 libcloud/types.py                              |    3 +
 test/fixtures/elastichosts/servers_create.json |   52 ++++++-------
 test/test_elastichosts.py                      |    6 +-
 5 files changed, 105 insertions(+), 58 deletions(-)

diff --git a/libcloud/drivers/elastichosts.py b/libcloud/drivers/elastichosts.py
index ebf68cb..0ea335e 100644
--- a/libcloud/drivers/elastichosts.py
+++ b/libcloud/drivers/elastichosts.py
@@ -51,7 +51,7 @@ API_ENDPOINTS = {
     },
 }
 
-# Default API end-point. This should be changed if the accounts are located elsewhere.
+# Default API end-point for the base connection clase.
 DEFAULT_ENDPOINT = 'us-1'
 
 # ElasticHosts doesn't specify special instance types, so I just specified
@@ -195,22 +195,6 @@ class ElasticHostsResponse(Response):
         message = self.body
 
         return 'X-Elastic-Error: %s (%s)' % (error_header, self.body.strip())
-
-class ElasticHostsConnection(ConnectionUserAndKey):
-    """
-    Connection class for the ElasticHosts driver
-    """
-
-    host = API_ENDPOINTS[DEFAULT_ENDPOINT]['host']
-    responseCls = ElasticHostsResponse
-
-    def add_default_headers(self, headers):
-        headers['Accept'] = 'application/json'
-        headers['Content-Type'] = 'application/json'
-
-        headers['Authorization'] = 'Basic %s' % (base64.b64encode('%s:%s' % (self.user_id, self.key)))
-
-        return headers
     
 class ElasticHostsNodeSize(NodeSize):
     def __init__(self, id, name, cpu, ram, disk, bandwidth, price, driver):
@@ -228,15 +212,31 @@ class ElasticHostsNodeSize(NodeSize):
                  'price=%s driver=%s ...>')
                 % (self.id, self.name, self.cpu, self.ram, self.disk, self.bandwidth,
                    self.price, self.driver.name))
+
+class ElasticHostsBaseConnection(ConnectionUserAndKey):
+    """
+    Base connection class for the ElasticHosts driver
+    """
     
-class ElasticHostsNodeDriver(NodeDriver):
+    host = API_ENDPOINTS[DEFAULT_ENDPOINT]['host']
+    responseCls = ElasticHostsResponse
+
+    def add_default_headers(self, headers):
+        headers['Accept'] = 'application/json'
+        headers['Content-Type'] = 'application/json'
+
+        headers['Authorization'] = 'Basic %s' % (base64.b64encode('%s:%s' % (self.user_id, self.key)))
+
+        return headers
+
+class ElasticHostsBaseNodeDriver(NodeDriver):
     """
-    ElasticHosts node driver
+    Base ElasticHosts node driver
     """
 
     type = Provider.ELASTICHOSTS
     name = 'ElasticHosts'
-    connectionCls = ElasticHostsConnection
+    connectionCls = ElasticHostsBaseConnection
 
     def reboot_node(self, node):
         # Reboots the node
@@ -309,6 +309,8 @@ class ElasticHostsNodeDriver(NodeDriver):
         if nic_model not in ['e1000', 'rtl8139', 'virtio']:
             raise ElasticHostsException('Invalid NIC model specified')
         
+        # check that drive size is not smaller then pre installed image size
+        
         # First we create a drive with the specified size
         drive_data = {}
         drive_data.update({'name': kwargs['name'], 'size': '%sG' % (kwargs['size'].disk)})
@@ -352,14 +354,12 @@ class ElasticHostsNodeDriver(NodeDriver):
             node_data.update({'vnc:ip': 'auto', 'vnc:password': vnc_password})
 
         response = self.connection.request(action = '/servers/create', data = json.dumps(node_data),
-                                           method = 'POST')
-        
-        nodes = response.object
+                                           method = 'POST').object
         
-        if len(nodes) == 1:
-            nodes = self._to_node(nodes[0])
+        if isinstance(response, list):
+            nodes = [self._to_node(node) for node in response]
         else:
-            nodes = [self._to_node(node) for node in nodes]
+            nodes = self._to_node(response)
         
         return nodes
 
@@ -425,3 +425,45 @@ class ElasticHostsNodeDriver(NodeDriver):
                     extra = extra)
         
         return node
+      
+class ElasticHostsUK1Connection(ElasticHostsBaseConnection):
+    """
+    Connection class for the ElasticHosts driver for the London Peer 1 end-point
+    """
+
+    host = API_ENDPOINTS['uk-1']['host']
+      
+class ElasticHostsUK1NodeDriver(ElasticHostsBaseNodeDriver):
+    """
+    ElasticHosts node driver for the London Peer 1 end-point
+    """
+    
+    connectionCls = ElasticHostsUK1Connection
+    
+class ElasticHostsUK2Connection(ElasticHostsBaseConnection):
+    """
+    Connection class for the ElasticHosts driver for the London Bluesquare end-point
+    """
+
+    host = API_ENDPOINTS['uk-2']['host']
+    
+class ElasticHostsUK2NodeDriver(ElasticHostsBaseNodeDriver):
+    """
+    ElasticHosts node driver for the London Bluesquare end-point
+    """
+
+    connectionCls = ElasticHostsUK2Connection
+    
+class ElasticHostsUS1Connection(ElasticHostsBaseConnection):
+    """
+    Connection class for the ElasticHosts driver for the San Antonio Peer 1 end-point
+    """
+
+    host = API_ENDPOINTS['us-1']['host']
+    
+class ElasticHostsUS1NodeDriver(ElasticHostsBaseNodeDriver):
+    """
+    ElasticHosts node driver for the San Antonio Peer 1 end-point
+    """
+
+    connectionCls = ElasticHostsUS1Connection
diff --git a/libcloud/providers.py b/libcloud/providers.py
index 23f382c..485c3d2 100644
--- a/libcloud/providers.py
+++ b/libcloud/providers.py
@@ -29,8 +29,12 @@ DRIVERS = {
         ('libcloud.drivers.ec2', 'EC2USWestNodeDriver'),
     Provider.ECP:
         ('libcloud.drivers.ecp', 'ECPNodeDriver'),
-    Provider.ELASTICHOSTS:
-        ('libcloud.drivers.elastichosts', 'ElasticHostsNodeDriver'),
+    Provider.ELASTICHOSTS_UK1:
+        ('libcloud.drivers.elastichosts', 'ElasticHostsUK1NodeDriver'),
+    Provider.ELASTICHOSTS_UK2:
+        ('libcloud.drivers.elastichosts', 'ElasticHostsUK2NodeDriver'),
+    Provider.ELASTICHOSTS_US1:
+        ('libcloud.drivers.elastichosts', 'ElasticHostsUS1NodeDriver'),
     Provider.GOGRID:
         ('libcloud.drivers.gogrid', 'GoGridNodeDriver'),
     Provider.RACKSPACE:
diff --git a/libcloud/types.py b/libcloud/types.py
index 41054ef..4927d8a 100644
--- a/libcloud/types.py
+++ b/libcloud/types.py
@@ -57,6 +57,9 @@ class Provider(object):
     OPENNEBULA = 16
     DREAMHOST = 17
     ELASTICHOSTS = 18
+    ELASTICHOSTS_UK1 = 19
+    ELASTICHOSTS_UK2 = 20
+    ELASTICHOSTS_US1 = 21
 
 class NodeState(object):
     """
diff --git a/test/fixtures/elastichosts/servers_create.json b/test/fixtures/elastichosts/servers_create.json
index 72b6b48..3a17f96 100644
--- a/test/fixtures/elastichosts/servers_create.json
+++ b/test/fixtures/elastichosts/servers_create.json
@@ -1,27 +1,25 @@
-[
-  {
-    "boot": "ide:0:0", 
-    "cpu": 2000, 
-    "ide:0:0": "b6049e7a-aa1b-47f9-b21d-cdf2354e28d3", 
-    "ide:0:0:read:bytes": "299696128", 
-    "ide:0:0:read:requests": "73168", 
-    "ide:0:0:write:bytes": "321044480", 
-    "ide:0:0:write:requests": "78380", 
-    "mem": 1024, 
-    "name": "test api node", 
-    "nic:0:block": "tcp/21 tcp/22 tcp/23 tcp/25", 
-    "nic:0:dhcp": ["1.2.3.4", "1.2.3.5"], 
-    "nic:0:model": "virtio", 
-    "rx": 679560, 
-    "rx:packets": 644, 
-    "server": "b605ca90-c3e6-4cee-85f8-a8ebdf8f9903", 
-    "smp": 1, 
-    "started": 1280723696, 
-    "status": "active", 
-    "tx": 21271, 
-    "tx:packets": "251", 
-    "user": "2164ce57-591a-43ee-ade5-e2fe0ee13c3f", 
-    "vnc:ip": "216.151.208.174", 
-    "vnc:password": "testvncpass"
-  }
-]
\ No newline at end of file
+{
+  "boot": "ide:0:0", 
+  "cpu": 2000, 
+  "ide:0:0": "b6049e7a-aa1b-47f9-b21d-cdf2354e28d3", 
+  "ide:0:0:read:bytes": "299696128", 
+  "ide:0:0:read:requests": "73168", 
+  "ide:0:0:write:bytes": "321044480", 
+  "ide:0:0:write:requests": "78380", 
+  "mem": 1024, 
+  "name": "test api node", 
+  "nic:0:block": "tcp/21 tcp/22 tcp/23 tcp/25", 
+  "nic:0:dhcp": ["1.2.3.4", "1.2.3.5"], 
+  "nic:0:model": "virtio", 
+  "rx": 679560, 
+  "rx:packets": 644, 
+  "server": "b605ca90-c3e6-4cee-85f8-a8ebdf8f9903", 
+  "smp": 1, 
+  "started": 1280723696, 
+  "status": "active", 
+  "tx": 21271, 
+  "tx:packets": "251", 
+  "user": "2164ce57-591a-43ee-ade5-e2fe0ee13c3f", 
+  "vnc:ip": "216.151.208.174", 
+  "vnc:password": "testvncpass"
+}
\ No newline at end of file
diff --git a/test/test_elastichosts.py b/test/test_elastichosts.py
index de3e676..523deba 100644
--- a/test/test_elastichosts.py
+++ b/test/test_elastichosts.py
@@ -18,15 +18,15 @@ import sys
 import unittest
 import httplib
 
-from libcloud.drivers.elastichosts import ElasticHostsNodeDriver
+from libcloud.drivers.elastichosts import ElasticHostsBaseNodeDriver
 from test import MockHttp, TestCaseMixin
 from test.file_fixtures import FileFixtures
 
 class ElasticHostsTestCase(unittest.TestCase, TestCaseMixin):
     def setUp(self):
-        ElasticHostsNodeDriver.connectionCls.conn_classes = (None,
+        ElasticHostsBaseNodeDriver.connectionCls.conn_classes = (None,
                                                             ElasticHostsHttp)
-        self.driver = ElasticHostsNodeDriver('foo', 'bar')
+        self.driver = ElasticHostsBaseNodeDriver('foo', 'bar')
 
     def test_list_nodes(self):
         nodes = self.driver.list_nodes()
-- 
1.7.0.4

