You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@libcloud.apache.org by to...@apache.org on 2011/06/16 14:58:10 UTC

svn commit: r1136418 - in /libcloud/trunk: libcloud/compute/drivers/__init__.py libcloud/compute/drivers/elastichosts.py libcloud/compute/drivers/elasticstack.py test/compute/test_elastichosts.py

Author: tomaz
Date: Thu Jun 16 12:58:09 2011
New Revision: 1136418

URL: http://svn.apache.org/viewvc?rev=1136418&view=rev
Log:
Refactor functionality from ElasticHost driver into a separate base 
ElasticStack class.
There are multiple providers which are using ElasticStack platform and this
should allow us to easily add new providers based on the ElasticStack platform.

Added:
    libcloud/trunk/libcloud/compute/drivers/elasticstack.py
Modified:
    libcloud/trunk/libcloud/compute/drivers/__init__.py
    libcloud/trunk/libcloud/compute/drivers/elastichosts.py
    libcloud/trunk/test/compute/test_elastichosts.py

Modified: libcloud/trunk/libcloud/compute/drivers/__init__.py
URL: http://svn.apache.org/viewvc/libcloud/trunk/libcloud/compute/drivers/__init__.py?rev=1136418&r1=1136417&r2=1136418&view=diff
==============================================================================
--- libcloud/trunk/libcloud/compute/drivers/__init__.py (original)
+++ libcloud/trunk/libcloud/compute/drivers/__init__.py Thu Jun 16 12:58:09 2011
@@ -23,6 +23,7 @@ __all__ = [
     'dummy',
     'ec2',
     'ecp',
+    'elasticstack',
     'elastichosts',
     'cloudsigma',
     'gogrid',

Modified: libcloud/trunk/libcloud/compute/drivers/elastichosts.py
URL: http://svn.apache.org/viewvc/libcloud/trunk/libcloud/compute/drivers/elastichosts.py?rev=1136418&r1=1136417&r2=1136418&view=diff
==============================================================================
--- libcloud/trunk/libcloud/compute/drivers/elastichosts.py (original)
+++ libcloud/trunk/libcloud/compute/drivers/elastichosts.py Thu Jun 16 12:58:09 2011
@@ -12,25 +12,15 @@
 # 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.
+
 """
 ElasticHosts Driver
 """
-import re
-import time
-import base64
-import httplib
-
-try:
-    import json
-except:
-    import simplejson as json
-
-from libcloud.common.base import ConnectionUserAndKey, Response
-from libcloud.common.types import InvalidCredsError, MalformedResponseError
-from libcloud.compute.types import Provider, NodeState
-from libcloud.compute.base import NodeDriver, NodeSize, Node
-from libcloud.compute.base import NodeImage
-from libcloud.compute.deployment import ScriptDeployment, SSHKeyDeployment, MultiStepDeployment
+
+from libcloud.compute.types import Provider
+from libcloud.compute.drivers.elasticstack import ElasticStackBaseNodeDriver
+from libcloud.compute.drivers.elasticstack import ElasticStackBaseConnection
+
 
 # API end-points
 API_ENDPOINTS = {
@@ -51,67 +41,9 @@ API_ENDPOINTS = {
     },
 }
 
-# Default API end-point for the base connection clase.
+# Default API end-point for the base connection class.
 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,
-    },
-    'medium': {
-        'id': 'medium',
-        'name': 'Medium instance',
-        'cpu': 3000,
-        'memory': 4096,
-        'disk': 500,
-        '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 = {
     '38df0986-4d85-4b76-b502-3878ffc80161': {
@@ -164,381 +96,21 @@ STANDARD_DRIVES = {
     },
 }
 
-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 InvalidCredsError()
-
-        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:
-            raise MalformedResponseError("Failed to parse JSON",
-                                         body=self.body,
-                                         driver=ElasticHostsBaseNodeDriver)
-
-        return data
-
-    def parse_error(self):
-        error_header = self.headers.get('x-elastic-error', '')
-        return 'X-Elastic-Error: %s (%s)' % (error_header, self.body.strip())
-
-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 ElasticHostsBaseConnection(ConnectionUserAndKey):
-    """
-    Base connection class for the ElasticHosts driver
-    """
 
+class ElasticHostsBaseConnection(ElasticStackBaseConnection):
     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):
-    """
-    Base ElasticHosts node driver
-    """
 
+class ElasticHostsBaseNodeDriver(ElasticStackBaseNodeDriver):
     type = Provider.ELASTICHOSTS
     api_name = 'elastichosts'
     name = 'ElasticHosts'
     connectionCls = ElasticHostsBaseConnection
     features = {"create_node": ["generates_password"]}
+    _standard_drives = STANDARD_DRIVES
 
-    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=self._get_size_price(size_id=value['id']),
-                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
-                               (if not specified, e1000 is used)
-        @type       nic_model: C{string}
-
-        @keyword    vnc_password: If set, the same password is also used for
-                                  SSH access with user toor,
-                                  otherwise VNC access is disabled and
-                                  no SSH login is possible.
-        @type       vnc_password: C{string}
-        """
-        size = kwargs['size']
-        image = kwargs['image']
-        smp = kwargs.get('smp', 'auto')
-        nic_model = kwargs.get('nic_model', 'e1000')
-        vnc_password = ssh_password = kwargs.get('vnc_password', None)
-
-        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)})
-
-        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
-        # than 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',
-                          'smp': smp})
-        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'
-        ).object
-
-        if isinstance(response, list):
-            nodes = [self._to_node(node, ssh_password) for node in response]
-        else:
-            nodes = self._to_node(response, ssh_password)
-
-        return nodes
-
-    # 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 == httplib.OK and response.body != '')
-
-    def deploy_node(self, **kwargs):
-        """
-        Create a new node, and start deployment.
-
-        @keyword    enable_root: If true, root password will be set to
-                                 vnc_password (this will enable SSH access)
-                                 and default 'toor' account will be deleted.
-        @type       enable_root: C{bool}
-
-        For detailed description and keywords args, see
-        L{NodeDriver.deploy_node}.
-        """
-        image = kwargs['image']
-        vnc_password = kwargs.get('vnc_password', None)
-        enable_root = kwargs.get('enable_root', False)
-
-        if not vnc_password:
-            raise ValueError('You need to provide vnc_password argument '
-                             'if you want to use deployment')
-
-        if (image in STANDARD_DRIVES
-            and STANDARD_DRIVES[image]['supports_deployment']):
-            raise ValueError('Image %s does not support deployment'
-                             % (image.id))
-
-        if enable_root:
-            script = ("unset HISTFILE;"
-                      "echo root:%s | chpasswd;"
-                      "sed -i '/^toor.*$/d' /etc/passwd /etc/shadow;"
-                      "history -c") % vnc_password
-            root_enable_script = ScriptDeployment(script=script,
-                                                  delete=True)
-            deploy = kwargs.get('deploy', None)
-            if deploy:
-                if (isinstance(deploy, ScriptDeployment)
-                    or isinstance(deploy, SSHKeyDeployment)):
-                    deployment = MultiStepDeployment([deploy,
-                                                      root_enable_script])
-                elif isinstance(deploy, MultiStepDeployment):
-                    deployment = deploy
-                    deployment.add(root_enable_script)
-            else:
-                deployment = root_enable_script
-
-            kwargs['deploy'] = deployment
-
-        if not kwargs.get('ssh_username', None):
-            kwargs['ssh_username'] = 'toor'
-
-        return super(ElasticHostsBaseNodeDriver, self).deploy_node(**kwargs)
-
-    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, ssh_password=None):
-        try:
-            state = NODE_STATE_MAP[data['status']]
-        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']})
-
-        if ssh_password:
-            extra.update({'password': ssh_password})
-
-        node = Node(id=data['server'], name=data['name'], state=state,
-                    public_ip=public_ip, private_ip=None,
-                    driver=self.connection.driver,
-                    extra=extra)
 
-        return node
-
-class ElasticHostsUK1Connection(ElasticHostsBaseConnection):
+class ElasticHostsUK1Connection(ElasticStackBaseConnection):
     """
     Connection class for the ElasticHosts driver for
     the London Peer 1 end-point
@@ -546,32 +118,37 @@ class ElasticHostsUK1Connection(ElasticH
 
     host = API_ENDPOINTS['uk-1']['host']
 
+
 class ElasticHostsUK1NodeDriver(ElasticHostsBaseNodeDriver):
     """
     ElasticHosts node driver for the London Peer 1 end-point
     """
     connectionCls = ElasticHostsUK1Connection
 
-class ElasticHostsUK2Connection(ElasticHostsBaseConnection):
+
+class ElasticHostsUK2Connection(ElasticStackBaseConnection):
     """
     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):
+
+class ElasticHostsUS1Connection(ElasticStackBaseConnection):
     """
     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

Added: libcloud/trunk/libcloud/compute/drivers/elasticstack.py
URL: http://svn.apache.org/viewvc/libcloud/trunk/libcloud/compute/drivers/elasticstack.py?rev=1136418&view=auto
==============================================================================
--- libcloud/trunk/libcloud/compute/drivers/elasticstack.py (added)
+++ libcloud/trunk/libcloud/compute/drivers/elasticstack.py Thu Jun 16 12:58:09 2011
@@ -0,0 +1,465 @@
+# 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.
+
+"""
+Base driver for the providers based on the ElasticStack platform -
+http://www.elasticstack.com.
+"""
+
+import re
+import time
+import base64
+import httplib
+
+try:
+    import json
+except:
+    import simplejson as json
+
+from libcloud.common.base import ConnectionUserAndKey, Response
+from libcloud.common.types import InvalidCredsError, MalformedResponseError
+from libcloud.compute.types import NodeState
+from libcloud.compute.base import NodeDriver, NodeSize, Node
+from libcloud.compute.base import NodeImage
+from libcloud.compute.deployment import ScriptDeployment, SSHKeyDeployment
+from libcloud.compute.deployment import MultiStepDeployment
+
+
+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
+
+# ElasticStack doesn't specify special instance types, so I just specified
+# some plans based on the other provider offerings.
+#
+# 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,
+    },
+    'medium': {
+        'id': 'medium',
+        'name': 'Medium instance',
+        'cpu': 3000,
+        'memory': 4096,
+        'disk': 500,
+        '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,
+    },
+}
+
+
+class ElasticStackException(Exception):
+    def __str__(self):
+        return self.args[0]
+
+    def __repr__(self):
+        return "<ElasticStackException '%s'>" % (self.args[0])
+
+
+class ElasticStackResponse(Response):
+    def success(self):
+        if self.status == 401:
+            raise InvalidCredsError()
+
+        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:
+            raise MalformedResponseError('Failed to parse JSON',
+                                         body=self.body,
+                                         driver=ElasticStackBaseNodeDriver)
+
+        return data
+
+    def parse_error(self):
+        error_header = self.headers.get('x-elastic-error', '')
+        return 'X-Elastic-Error: %s (%s)' % (error_header, self.body.strip())
+
+
+class ElasticStackNodeSize(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 ElasticStackBaseConnection(ConnectionUserAndKey):
+    """
+    Base connection class for the ElasticStack driver
+    """
+
+    host = None
+    responseCls = ElasticStackResponse
+
+    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 ElasticStackBaseNodeDriver(NodeDriver):
+    connectionCls = ElasticStackBaseConnection
+    features = {"create_node": ["generates_password"]}
+
+    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 self._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 = ElasticStackNodeSize(
+                id=value['id'],
+                name=value['name'], cpu=value['cpu'], ram=value['memory'],
+                disk=value['disk'], bandwidth=value['bandwidth'],
+                price=self._get_size_price(size_id=value['id']),
+                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 ElasticStack 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
+                               (if not specified, e1000 is used)
+        @type       nic_model: C{string}
+
+        @keyword    vnc_password: If set, the same password is also used for
+                                  SSH access with user toor,
+                                  otherwise VNC access is disabled and
+                                  no SSH login is possible.
+        @type       vnc_password: C{string}
+        """
+        size = kwargs['size']
+        image = kwargs['image']
+        smp = kwargs.get('smp', 'auto')
+        nic_model = kwargs.get('nic_model', 'e1000')
+        vnc_password = ssh_password = kwargs.get('vnc_password', None)
+
+        if nic_model not in ('e1000', 'rtl8139', 'virtio'):
+            raise ElasticStackException('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)})
+
+        response = self.connection.request(action='/drives/create',
+                                           data=json.dumps(drive_data),
+                                           method='POST').object
+
+        if not response:
+            raise ElasticStackException('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 ElasticStackException('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
+        # than a few minutes)
+        response = self.connection.request(
+            action='/drives/%s/info' % (drive_uuid)
+        ).object
+
+        imaging_start = time.time()
+        while 'imaging' in response:
+            response = self.connection.request(
+                action='/drives/%s/info' % (drive_uuid)
+            ).object
+
+            elapsed_time = time.time() - imaging_start
+            if ('imaging' in response
+                and elapsed_time >= IMAGING_TIMEOUT):
+                raise ElasticStackException('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',
+                          'smp': smp})
+        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'
+        ).object
+
+        if isinstance(response, list):
+            nodes = [self._to_node(node, ssh_password) for node in response]
+        else:
+            nodes = self._to_node(response, ssh_password)
+
+        return nodes
+
+    # 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 ElasticStackException(
+                '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 == httplib.OK and response.body != '')
+
+    def deploy_node(self, **kwargs):
+        """
+        Create a new node, and start deployment.
+
+        @keyword    enable_root: If true, root password will be set to
+                                 vnc_password (this will enable SSH access)
+                                 and default 'toor' account will be deleted.
+        @type       enable_root: C{bool}
+
+        For detailed description and keywords args, see
+        L{NodeDriver.deploy_node}.
+        """
+        image = kwargs['image']
+        vnc_password = kwargs.get('vnc_password', None)
+        enable_root = kwargs.get('enable_root', False)
+
+        if not vnc_password:
+            raise ValueError('You need to provide vnc_password argument '
+                             'if you want to use deployment')
+
+        if (image in self._standard_drives
+            and not self._standard_drives[image]['supports_deployment']):
+            raise ValueError('Image %s does not support deployment'
+                             % (image.id))
+
+        if enable_root:
+            script = ("unset HISTFILE;"
+                      "echo root:%s | chpasswd;"
+                      "sed -i '/^toor.*$/d' /etc/passwd /etc/shadow;"
+                      "history -c") % vnc_password
+            root_enable_script = ScriptDeployment(script=script,
+                                                  delete=True)
+            deploy = kwargs.get('deploy', None)
+            if deploy:
+                if (isinstance(deploy, ScriptDeployment)
+                    or isinstance(deploy, SSHKeyDeployment)):
+                    deployment = MultiStepDeployment([deploy,
+                                                      root_enable_script])
+                elif isinstance(deploy, MultiStepDeployment):
+                    deployment = deploy
+                    deployment.add(root_enable_script)
+            else:
+                deployment = root_enable_script
+
+            kwargs['deploy'] = deployment
+
+        if not kwargs.get('ssh_username', None):
+            kwargs['ssh_username'] = 'toor'
+
+        return super(ElasticStackBaseNodeDriver, self).deploy_node(**kwargs)
+
+    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, ssh_password=None):
+        try:
+            state = NODE_STATE_MAP[data['status']]
+        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 'vnc:ip' in data and 'vnc:password' in data:
+            extra.update({'vnc_ip': data['vnc:ip'],
+                          'vnc_password': data['vnc:password']})
+
+        if ssh_password:
+            extra.update({'password': ssh_password})
+
+        node = Node(id=data['server'], name=data['name'], state=state,
+                    public_ip=public_ip, private_ip=None,
+                    driver=self.connection.driver,
+                    extra=extra)
+
+        return node

Modified: libcloud/trunk/test/compute/test_elastichosts.py
URL: http://svn.apache.org/viewvc/libcloud/trunk/test/compute/test_elastichosts.py?rev=1136418&r1=1136417&r2=1136418&view=diff
==============================================================================
--- libcloud/trunk/test/compute/test_elastichosts.py (original)
+++ libcloud/trunk/test/compute/test_elastichosts.py Thu Jun 16 12:58:09 2011
@@ -12,33 +12,43 @@
 # 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.compute.base import Node
+from libcloud.compute.types import Provider
+from libcloud.compute.drivers.elasticstack import (ElasticStackException,
+                              ElasticStackBaseConnection,
+                              ElasticStackBaseNodeDriver as ElasticStack)
 from libcloud.compute.drivers.elastichosts import \
-                              (ElasticHostsBaseNodeDriver as ElasticHosts,
-                               ElasticHostsException)
+                              (ElasticHostsBaseNodeDriver as ElasticHosts)
 from libcloud.common.types import InvalidCredsError, MalformedResponseError
 
 from test import MockHttp
 from test.file_fixtures import ComputeFileFixtures
 
-class ElasticHostsTestCase(unittest.TestCase):
+class ElasticStackTestCase(unittest.TestCase):
 
     def setUp(self):
-        ElasticHosts.connectionCls.conn_classes = (None,
-                                                            ElasticHostsHttp)
-        ElasticHostsHttp.type = None
-        self.driver = ElasticHosts('foo', 'bar')
+        # Re-use ElasticHosts fixtures for the base ElasticStack platform tests
+        ElasticStack.type = Provider.ELASTICHOSTS
+        ElasticStack.api_name = 'elastichosts'
+
+        ElasticStackBaseConnection.host = 'test.com'
+        ElasticStack.connectionCls.conn_classes = (None,
+                                                   ElasticStackMockHttp)
+        ElasticStack._standard_drives = ElasticHosts._standard_drives
+
+        self.mockHttp = ElasticStackMockHttp
+        self.mockHttp.type = None
+        self.driver = ElasticStack('foo', 'bar')
         self.node = Node(id=72258, name=None, state=None, public_ip=None,
                          private_ip=None, driver=self.driver)
 
     def test_invalid_creds(self):
-        ElasticHostsHttp.type = 'UNAUTHORIZED'
+        self.mockHttp.type = 'UNAUTHORIZED'
         try:
             self.driver.list_nodes()
         except InvalidCredsError, e:
@@ -47,7 +57,7 @@ class ElasticHostsTestCase(unittest.Test
             self.fail('test should have thrown')
 
     def test_malformed_response(self):
-        ElasticHostsHttp.type = 'MALFORMED'
+        self.mockHttp.type = 'MALFORMED'
         try:
             self.driver.list_nodes()
         except MalformedResponseError:
@@ -56,7 +66,7 @@ class ElasticHostsTestCase(unittest.Test
             self.fail('test should have thrown')
 
     def test_parse_error(self):
-        ElasticHostsHttp.type = 'PARSE_ERROR'
+        self.mockHttp.type = 'PARSE_ERROR'
         try:
             self.driver.list_nodes()
         except Exception, e:
@@ -68,11 +78,12 @@ class ElasticHostsTestCase(unittest.Test
         success = self.driver.ex_set_node_configuration(node=self.node,
                                                         name='name',
                                                         cpu='2')
+        self.assertTrue(success)
 
     def test_ex_set_node_configuration_invalid_keys(self):
         try:
             self.driver.ex_set_node_configuration(node=self.node, foo='bar')
-        except ElasticHostsException:
+        except ElasticStackException:
             pass
         else:
             self.fail('Invalid option specified, but an exception was not thrown')
@@ -125,7 +136,17 @@ class ElasticHostsTestCase(unittest.Test
         self.assertTrue(self.driver.create_node(name="api.ivan.net.nz",
                                                 image=image, size=size))
 
-class ElasticHostsHttp(MockHttp):
+
+class ElasticHostsTestCase(ElasticStackTestCase):
+
+    def setUp(self):
+        ElasticHosts.connectionCls.conn_classes = (None,
+                                                   ElasticStackMockHttp)
+        self.driver = ElasticHosts('foo', 'bar')
+        super(ElasticHostsTestCase, self).setUp()
+
+
+class ElasticStackMockHttp(MockHttp):
 
     fixtures = ComputeFileFixtures('elastichosts')