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/03/08 00:44:12 UTC

svn commit: r1079029 [3/13] - in /incubator/libcloud/trunk: ./ demos/ dist/ libcloud/ libcloud/common/ libcloud/compute/ libcloud/compute/drivers/ libcloud/drivers/ libcloud/storage/ libcloud/storage/drivers/ test/ test/compute/ test/compute/fixtures/ ...

Added: incubator/libcloud/trunk/libcloud/compute/drivers/ec2.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/libcloud/compute/drivers/ec2.py?rev=1079029&view=auto
==============================================================================
--- incubator/libcloud/trunk/libcloud/compute/drivers/ec2.py (added)
+++ incubator/libcloud/trunk/libcloud/compute/drivers/ec2.py Mon Mar  7 23:44:06 2011
@@ -0,0 +1,937 @@
+# 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.
+
+"""
+Amazon EC2 driver
+"""
+import base64
+import hmac
+import os
+import time
+import urllib
+
+from hashlib import sha256
+from xml.etree import ElementTree as ET
+
+from libcloud.common.base import Response, ConnectionUserAndKey
+from libcloud.common.types import InvalidCredsError, MalformedResponseError, LibcloudError
+from libcloud.compute.providers import Provider
+from libcloud.compute.types import NodeState
+from libcloud.compute.base import Node, NodeDriver, NodeLocation, NodeSize
+from libcloud.compute.base import NodeImage
+
+EC2_US_EAST_HOST = 'ec2.us-east-1.amazonaws.com'
+EC2_US_WEST_HOST = 'ec2.us-west-1.amazonaws.com'
+EC2_EU_WEST_HOST = 'ec2.eu-west-1.amazonaws.com'
+EC2_AP_SOUTHEAST_HOST = 'ec2.ap-southeast-1.amazonaws.com'
+EC2_AP_NORTHEAST_HOST = 'ec2.ap-northeast-1.amazonaws.com'
+
+API_VERSION = '2010-08-31'
+
+NAMESPACE = "http://ec2.amazonaws.com/doc/%s/" % (API_VERSION)
+
+"""
+Sizes must be hardcoded, because Amazon doesn't provide an API to fetch them.
+From http://aws.amazon.com/ec2/instance-types/
+"""
+EC2_INSTANCE_TYPES = {
+    't1.micro': {
+        'id': 't1.micro',
+        'name': 'Micro Instance',
+        'ram': 613,
+        'disk': 15,
+        'bandwidth': None
+    },
+    'm1.small': {
+        'id': 'm1.small',
+        'name': 'Small Instance',
+        'ram': 1740,
+        'disk': 160,
+        'bandwidth': None
+    },
+    'm1.large': {
+        'id': 'm1.large',
+        'name': 'Large Instance',
+        'ram': 7680,
+        'disk': 850,
+        'bandwidth': None
+    },
+    'm1.xlarge': {
+        'id': 'm1.xlarge',
+        'name': 'Extra Large Instance',
+        'ram': 15360,
+        'disk': 1690,
+        'bandwidth': None
+    },
+    'c1.medium': {
+        'id': 'c1.medium',
+        'name': 'High-CPU Medium Instance',
+        'ram': 1740,
+        'disk': 350,
+        'bandwidth': None
+    },
+    'c1.xlarge': {
+        'id': 'c1.xlarge',
+        'name': 'High-CPU Extra Large Instance',
+        'ram': 7680,
+        'disk': 1690,
+        'bandwidth': None
+    },
+    'm2.xlarge': {
+        'id': 'm2.xlarge',
+        'name': 'High-Memory Extra Large Instance',
+        'ram': 17510,
+        'disk': 420,
+        'bandwidth': None
+    },
+    'm2.2xlarge': {
+        'id': 'm2.2xlarge',
+        'name': 'High-Memory Double Extra Large Instance',
+        'ram': 35021,
+        'disk': 850,
+        'bandwidth': None
+    },
+    'm2.4xlarge': {
+        'id': 'm2.4xlarge',
+        'name': 'High-Memory Quadruple Extra Large Instance',
+        'ram': 70042,
+        'disk': 1690,
+        'bandwidth': None
+    },
+    'cg1.4xlarge': {
+        'id': 'cg1.4xlarge',
+        'name': 'Cluster GPU Quadruple Extra Large Instance',
+        'ram': 22528,
+        'disk': 1690,
+        'bandwidth': None
+    },
+    'cc1.4xlarge': {
+        'id': 'cc1.4xlarge',
+        'name': 'Cluster Compute Quadruple Extra Large Instance',
+        'ram': 23552,
+        'disk': 1690,
+        'bandwidth': None
+    },
+}
+
+CLUSTER_INSTANCES_IDS = [ 'cg1.4xlarge', 'cc1.4xlarge' ]
+
+EC2_US_EAST_INSTANCE_TYPES = dict(EC2_INSTANCE_TYPES)
+EC2_US_WEST_INSTANCE_TYPES = dict(EC2_INSTANCE_TYPES)
+EC2_EU_WEST_INSTANCE_TYPES = dict(EC2_INSTANCE_TYPES)
+EC2_AP_SOUTHEAST_INSTANCE_TYPES = dict(EC2_INSTANCE_TYPES)
+EC2_AP_NORTHEAST_INSTANCE_TYPES = dict(EC2_INSTANCE_TYPES)
+
+#
+# On demand prices must also be hardcoded, because Amazon doesn't provide an
+# API to fetch them. From http://aws.amazon.com/ec2/pricing/
+#
+EC2_US_EAST_INSTANCE_TYPES['t1.micro']['price'] = '.02'
+EC2_US_EAST_INSTANCE_TYPES['m1.small']['price'] = '.085'
+EC2_US_EAST_INSTANCE_TYPES['m1.large']['price'] = '.34'
+EC2_US_EAST_INSTANCE_TYPES['m1.xlarge']['price'] = '.68'
+EC2_US_EAST_INSTANCE_TYPES['c1.medium']['price'] = '.17'
+EC2_US_EAST_INSTANCE_TYPES['c1.xlarge']['price'] = '.68'
+EC2_US_EAST_INSTANCE_TYPES['m2.xlarge']['price'] = '.50'
+EC2_US_EAST_INSTANCE_TYPES['m2.2xlarge']['price'] = '1.0'
+EC2_US_EAST_INSTANCE_TYPES['m2.4xlarge']['price'] = '2.0'
+EC2_US_EAST_INSTANCE_TYPES['cg1.4xlarge']['price'] = '2.1'
+EC2_US_EAST_INSTANCE_TYPES['cc1.4xlarge']['price'] = '1.6'
+
+EC2_US_WEST_INSTANCE_TYPES['t1.micro']['price'] = '.025'
+EC2_US_WEST_INSTANCE_TYPES['m1.small']['price'] = '.095'
+EC2_US_WEST_INSTANCE_TYPES['m1.large']['price'] = '.38'
+EC2_US_WEST_INSTANCE_TYPES['m1.xlarge']['price'] = '.76'
+EC2_US_WEST_INSTANCE_TYPES['c1.medium']['price'] = '.19'
+EC2_US_WEST_INSTANCE_TYPES['c1.xlarge']['price'] = '.76'
+EC2_US_WEST_INSTANCE_TYPES['m2.xlarge']['price'] = '.57'
+EC2_US_WEST_INSTANCE_TYPES['m2.2xlarge']['price'] = '1.14'
+EC2_US_WEST_INSTANCE_TYPES['m2.4xlarge']['price'] = '2.28'
+
+EC2_EU_WEST_INSTANCE_TYPES['t1.micro']['price'] = '.025'
+EC2_EU_WEST_INSTANCE_TYPES['m1.small']['price'] = '.095'
+EC2_EU_WEST_INSTANCE_TYPES['m1.large']['price'] = '.38'
+EC2_EU_WEST_INSTANCE_TYPES['m1.xlarge']['price'] = '.76'
+EC2_EU_WEST_INSTANCE_TYPES['c1.medium']['price'] = '.19'
+EC2_EU_WEST_INSTANCE_TYPES['c1.xlarge']['price'] = '.76'
+EC2_EU_WEST_INSTANCE_TYPES['m2.xlarge']['price'] = '.57'
+EC2_EU_WEST_INSTANCE_TYPES['m2.2xlarge']['price'] = '1.14'
+EC2_EU_WEST_INSTANCE_TYPES['m2.4xlarge']['price'] = '2.28'
+
+# prices are the same
+EC2_AP_SOUTHEAST_INSTANCE_TYPES = dict(EC2_EU_WEST_INSTANCE_TYPES)
+
+EC2_AP_NORTHEAST_INSTANCE_TYPES['t1.micro']['price'] = '.027'
+EC2_AP_NORTHEAST_INSTANCE_TYPES['m1.small']['price'] = '.10'
+EC2_AP_NORTHEAST_INSTANCE_TYPES['m1.large']['price'] = '.40'
+EC2_AP_NORTHEAST_INSTANCE_TYPES['m1.xlarge']['price'] = '.80'
+EC2_AP_NORTHEAST_INSTANCE_TYPES['c1.medium']['price'] = '.20'
+EC2_AP_NORTHEAST_INSTANCE_TYPES['c1.xlarge']['price'] = '.80'
+EC2_AP_NORTHEAST_INSTANCE_TYPES['m2.xlarge']['price'] = '.60'
+EC2_AP_NORTHEAST_INSTANCE_TYPES['m2.2xlarge']['price'] = '1.20'
+EC2_AP_NORTHEAST_INSTANCE_TYPES['m2.4xlarge']['price'] = '2.39'
+
+class EC2NodeLocation(NodeLocation):
+    def __init__(self, id, name, country, driver, availability_zone):
+        super(EC2NodeLocation, self).__init__(id, name, country, driver)
+        self.availability_zone = availability_zone
+
+    def __repr__(self):
+        return (('<EC2NodeLocation: id=%s, name=%s, country=%s, '
+                 'availability_zone=%s driver=%s>')
+                % (self.id, self.name, self.country,
+                   self.availability_zone.name, self.driver.name))
+
+class EC2Response(Response):
+    """
+    EC2 specific response parsing and error handling.
+    """
+    def parse_body(self):
+        if not self.body:
+            return None
+        try:
+          body = ET.XML(self.body)
+        except:
+          raise MalformedResponseError("Failed to parse XML", body=self.body, driver=EC2NodeDriver)
+        return body
+
+    def parse_error(self):
+        err_list = []
+        # Okay, so for Eucalyptus, you can get a 403, with no body,
+        # if you are using the wrong user/password.
+        msg = "Failure: 403 Forbidden"
+        if self.status == 403 and self.body[:len(msg)] == msg:
+            raise InvalidCredsError(msg)
+
+        try:
+            body = ET.XML(self.body)
+        except:
+            raise MalformedResponseError("Failed to parse XML", body=self.body, driver=EC2NodeDriver)
+
+        for err in body.findall('Errors/Error'):
+            code, message = err.getchildren()
+            err_list.append("%s: %s" % (code.text, message.text))
+            if code.text == "InvalidClientTokenId":
+                raise InvalidCredsError(err_list[-1])
+            if code.text == "SignatureDoesNotMatch":
+                raise InvalidCredsError(err_list[-1])
+            if code.text == "AuthFailure":
+                raise InvalidCredsError(err_list[-1])
+            if code.text == "OptInRequired":
+                raise InvalidCredsError(err_list[-1])
+            if code.text == "IdempotentParameterMismatch":
+                raise IdempotentParamError(err_list[-1])
+        return "\n".join(err_list)
+
+class EC2Connection(ConnectionUserAndKey):
+    """
+    Repersents a single connection to the EC2 Endpoint
+    """
+
+    host = EC2_US_EAST_HOST
+    responseCls = EC2Response
+
+    def add_default_params(self, params):
+        params['SignatureVersion'] = '2'
+        params['SignatureMethod'] = 'HmacSHA256'
+        params['AWSAccessKeyId'] = self.user_id
+        params['Version'] = API_VERSION
+        params['Timestamp'] = time.strftime('%Y-%m-%dT%H:%M:%SZ',
+                                            time.gmtime())
+        params['Signature'] = self._get_aws_auth_param(params, self.key, self.action)
+        return params
+
+    def _get_aws_auth_param(self, params, secret_key, path='/'):
+        """
+        Creates the signature required for AWS, per
+        http://bit.ly/aR7GaQ [docs.amazonwebservices.com]:
+
+        StringToSign = HTTPVerb + "\n" +
+                       ValueOfHostHeaderInLowercase + "\n" +
+                       HTTPRequestURI + "\n" +
+                       CanonicalizedQueryString <from the preceding step>
+        """
+        keys = params.keys()
+        keys.sort()
+        pairs = []
+        for key in keys:
+            pairs.append(urllib.quote(key, safe='') + '=' +
+                         urllib.quote(params[key], safe='-_~'))
+
+        qs = '&'.join(pairs)
+        string_to_sign = '\n'.join(('GET', self.host, path, qs))
+
+        b64_hmac = base64.b64encode(
+            hmac.new(secret_key, string_to_sign, digestmod=sha256).digest()
+        )
+        return b64_hmac
+
+class ExEC2AvailabilityZone(object):
+    """
+    Extension class which stores information about an EC2 availability zone.
+
+    Note: This class is EC2 specific.
+    """
+    def __init__(self, name, zone_state, region_name):
+        self.name = name
+        self.zone_state = zone_state
+        self.region_name = region_name
+
+    def __repr__(self):
+        return (('<ExEC2AvailabilityZone: name=%s, zone_state=%s, '
+                 'region_name=%s>')
+                % (self.name, self.zone_state, self.region_name))
+
+class EC2NodeDriver(NodeDriver):
+    """
+    Amazon EC2 node driver
+    """
+
+    connectionCls = EC2Connection
+    type = Provider.EC2
+    name = 'Amazon EC2 (us-east-1)'
+    friendly_name = 'Amazon US N. Virginia'
+    country = 'US'
+    region_name = 'us-east-1'
+    path = '/'
+
+    _instance_types = EC2_US_EAST_INSTANCE_TYPES
+
+    NODE_STATE_MAP = {
+        'pending': NodeState.PENDING,
+        'running': NodeState.RUNNING,
+        'shutting-down': NodeState.TERMINATED,
+        'terminated': NodeState.TERMINATED
+    }
+
+    def _findtext(self, element, xpath):
+        return element.findtext(self._fixxpath(xpath))
+
+    def _fixxpath(self, xpath):
+        # ElementTree wants namespaces in its xpaths, so here we add them.
+        return "/".join(["{%s}%s" % (NAMESPACE, e) for e in xpath.split("/")])
+
+    def _findattr(self, element, xpath):
+        return element.findtext(self._fixxpath(xpath))
+
+    def _findall(self, element, xpath):
+        return element.findall(self._fixxpath(xpath))
+
+    def _pathlist(self, key, arr):
+        """
+        Converts a key and an array of values into AWS query param format.
+        """
+        params = {}
+        i = 0
+        for value in arr:
+            i += 1
+            params["%s.%s" % (key, i)] = value
+        return params
+
+    def _get_boolean(self, element):
+        tag = "{%s}%s" % (NAMESPACE, 'return')
+        return element.findtext(tag) == 'true'
+
+    def _get_terminate_boolean(self, element):
+        status = element.findtext(".//{%s}%s" % (NAMESPACE, 'name'))
+        return any([ term_status == status
+                     for term_status
+                     in ('shutting-down', 'terminated') ])
+
+    def _to_nodes(self, object, xpath, groups=None):
+        return [ self._to_node(el, groups=groups)
+                 for el in object.findall(self._fixxpath(xpath)) ]
+
+    def _to_node(self, element, groups=None):
+        try:
+            state = self.NODE_STATE_MAP[
+                self._findattr(element, "instanceState/name")
+            ]
+        except KeyError:
+            state = NodeState.UNKNOWN
+
+        n = Node(
+            id=self._findtext(element, 'instanceId'),
+            name=self._findtext(element, 'instanceId'),
+            state=state,
+            public_ip=[self._findtext(element, 'ipAddress')],
+            private_ip=[self._findtext(element, 'privateIpAddress')],
+            driver=self.connection.driver,
+            extra={
+                'dns_name': self._findattr(element, "dnsName"),
+                'instanceId': self._findattr(element, "instanceId"),
+                'imageId': self._findattr(element, "imageId"),
+                'private_dns': self._findattr(element, "privateDnsName"),
+                'status': self._findattr(element, "instanceState/name"),
+                'keyname': self._findattr(element, "keyName"),
+                'launchindex': self._findattr(element, "amiLaunchIndex"),
+                'productcode':
+                    [p.text for p in self._findall(
+                        element, "productCodesSet/item/productCode"
+                     )],
+                'instancetype': self._findattr(element, "instanceType"),
+                'launchdatetime': self._findattr(element, "launchTime"),
+                'availability': self._findattr(element,
+                                               "placement/availabilityZone"),
+                'kernelid': self._findattr(element, "kernelId"),
+                'ramdiskid': self._findattr(element, "ramdiskId"),
+                'clienttoken' : self._findattr(element, "clientToken"),
+                'groups': groups
+            }
+        )
+        return n
+
+    def _to_images(self, object):
+        return [ self._to_image(el)
+                 for el in object.findall(
+                    self._fixxpath('imagesSet/item')
+                 ) ]
+
+    def _to_image(self, element):
+        n = NodeImage(id=self._findtext(element, 'imageId'),
+                      name=self._findtext(element, 'imageLocation'),
+                      driver=self.connection.driver)
+        return n
+
+    def list_nodes(self):
+        params = {'Action': 'DescribeInstances' }
+        elem=self.connection.request(self.path, params=params).object
+        nodes=[]
+        for rs in self._findall(elem, 'reservationSet/item'):
+            groups=[g.findtext('')
+                        for g in self._findall(rs, 'groupSet/item/groupId')]
+            nodes += self._to_nodes(rs, 'instancesSet/item', groups)
+
+        nodes_elastic_ips_mappings = self.ex_describe_addresses(nodes)
+        for node in nodes:
+            node.public_ip.extend(nodes_elastic_ips_mappings[node.id])
+        return nodes
+
+    def list_sizes(self, location=None):
+        # Cluster instances are currently only available in the US - N. Virginia Region
+        include_cluser_instances = self.region_name == 'us-east-1'
+        sizes = self._get_sizes(include_cluser_instances =
+                                include_cluser_instances)
+
+        return sizes
+
+    def _get_sizes(self, include_cluser_instances=False):
+        sizes = [ NodeSize(driver=self.connection.driver, **i)
+                         for i in self._instance_types.values() ]
+
+        if not include_cluser_instances:
+            sizes = [ size for size in sizes if \
+                      size.id not in CLUSTER_INSTANCES_IDS]
+        return sizes
+
+    def list_images(self, location=None):
+        params = {'Action': 'DescribeImages'}
+        images = self._to_images(
+            self.connection.request(self.path, params=params).object
+        )
+        return images
+
+    def list_locations(self):
+        locations = []
+        for index, availability_zone in enumerate(self.ex_list_availability_zones()):
+            locations.append(EC2NodeLocation(index,
+                                             self.friendly_name,
+                                             self.country,
+                                             self,
+                                             availability_zone))
+        return locations
+
+    def ex_create_keypair(self, name):
+        """Creates a new keypair
+
+        @note: This is a non-standard extension API, and
+               only works for EC2.
+
+        @type name: C{str}
+        @param name: The name of the keypair to Create. This must be
+                     unique, otherwise an InvalidKeyPair.Duplicate
+                     exception is raised.
+        """
+        params = {
+            'Action': 'CreateKeyPair',
+            'KeyName': name,
+        }
+        response = self.connection.request(self.path, params=params).object
+        key_material = self._findtext(response, 'keyMaterial')
+        key_fingerprint = self._findtext(response, 'keyFingerprint')
+        return {
+            'keyMaterial': key_material,
+            'keyFingerprint': key_fingerprint,
+        }
+
+    def ex_import_keypair(self, name, keyfile):
+        """imports a new public key
+
+        @note: This is a non-standard extension API, and only works for EC2.
+
+        @type name: C{str}
+        @param name: The name of the public key to import. This must be unique,
+                     otherwise an InvalidKeyPair.Duplicate exception is raised.
+
+        @type keyfile: C{str}
+        @param keyfile: The filename with path of the public key to import.
+
+        """
+
+        base64key = base64.b64encode(open(os.path.expanduser(keyfile)).read())
+
+        params = {'Action': 'ImportKeyPair',
+                  'KeyName': name,
+                  'PublicKeyMaterial': base64key
+        }
+
+        response = self.connection.request(self.path, params=params).object
+        key_name = self._findtext(response, 'keyName')
+        key_fingerprint = self._findtext(response, 'keyFingerprint')
+        return {
+                'keyName': key_name,
+                'keyFingerprint': key_fingerprint,
+        }
+
+    def ex_describe_keypairs(self, name):
+        """Describes a keypiar by name
+
+        @note: This is a non-standard extension API, and only works for EC2.
+
+        @type name: C{str}
+        @param name: The name of the keypair to describe.
+
+        """
+
+        params = {'Action': 'DescribeKeyPairs',
+                  'KeyName.1': name
+        }
+
+        response = self.connection.request(self.path, params=params).object
+        key_name = self._findattr(response, 'keySet/item/keyName')
+        return {
+                'keyName': key_name
+        }
+
+    def ex_create_security_group(self, name, description):
+        """Creates a new Security Group
+
+        @note: This is a non-standard extension API, and only works for EC2.
+
+        @type name: C{str}
+        @param name: The name of the security group to Create. This must be unique.
+
+        @type description: C{str}
+        @param description: Human readable description of a Security Group.
+        """
+        params = {'Action': 'CreateSecurityGroup',
+                  'GroupName': name,
+                  'GroupDescription': description}
+        return self.connection.request(self.path, params=params).object
+
+    def ex_authorize_security_group_permissive(self, name):
+        """Edit a Security Group to allow all traffic.
+
+        @note: This is a non-standard extension API, and only works for EC2.
+
+        @type name: C{str}
+        @param name: The name of the security group to edit
+        """
+
+        results = []
+        params = {'Action': 'AuthorizeSecurityGroupIngress',
+                  'GroupName': name,
+                  'IpProtocol': 'tcp',
+                  'FromPort': '0',
+                  'ToPort': '65535',
+                  'CidrIp': '0.0.0.0/0'}
+        try:
+            results.append(
+                self.connection.request(self.path, params=params.copy()).object
+            )
+        except Exception, e:
+            if e.args[0].find("InvalidPermission.Duplicate") == -1:
+                raise e
+        params['IpProtocol'] = 'udp'
+
+        try:
+            results.append(
+                self.connection.request(self.path, params=params.copy()).object
+            )
+        except Exception, e:
+            if e.args[0].find("InvalidPermission.Duplicate") == -1:
+                raise e
+
+        params.update({'IpProtocol': 'icmp', 'FromPort': '-1', 'ToPort': '-1'})
+
+        try:
+            results.append(
+                self.connection.request(self.path, params=params.copy()).object
+            )
+        except Exception, e:
+            if e.args[0].find("InvalidPermission.Duplicate") == -1:
+                raise e
+        return results
+
+    def ex_list_availability_zones(self, only_available=True):
+        """
+        Return a list of L{ExEC2AvailabilityZone} objects for the
+        current region.
+
+        Note: This is an extension method and is only available for EC2
+        driver.
+
+        @keyword  only_available: If true, return only availability zones
+                                  with state 'available'
+        @type     only_available: C{string}
+        """
+        params = {'Action': 'DescribeAvailabilityZones'}
+
+        if only_available:
+            params.update({'Filter.0.Name': 'state'})
+            params.update({'Filter.0.Value.0': 'available'})
+
+        params.update({'Filter.1.Name': 'region-name'})
+        params.update({'Filter.1.Value.0': self.region_name})
+
+        result = self.connection.request(self.path,
+                                         params=params.copy()).object
+
+        availability_zones = []
+        for element in self._findall(result, 'availabilityZoneInfo/item'):
+            name = self._findtext(element, 'zoneName')
+            zone_state = self._findtext(element, 'zoneState')
+            region_name = self._findtext(element, 'regionName')
+
+            availability_zone = ExEC2AvailabilityZone(
+                name=name,
+                zone_state=zone_state,
+                region_name=region_name
+            )
+            availability_zones.append(availability_zone)
+
+        return availability_zones
+
+    def ex_describe_tags(self, node):
+        """
+        Return a dictionary of tags for this instance.
+
+        @type node: C{Node}
+        @param node: Node instance
+
+        @return dict Node tags
+        """
+        params = { 'Action': 'DescribeTags',
+                   'Filter.0.Name': 'resource-id',
+                   'Filter.0.Value.0': node.id,
+                   'Filter.1.Name': 'resource-type',
+                   'Filter.1.Value.0': 'instance',
+                   }
+
+        result = self.connection.request(self.path,
+                                         params=params.copy()).object
+
+        tags = {}
+        for element in self._findall(result, 'tagSet/item'):
+            key = self._findtext(element, 'key')
+            value = self._findtext(element, 'value')
+
+            tags[key] = value
+        return tags
+
+    def ex_create_tags(self, node, tags):
+        """
+        Create tags for an instance.
+
+        @type node: C{Node}
+        @param node: Node instance
+        @param tags: A dictionary or other mapping of strings to strings,
+                     associating tag names with tag values.
+        """
+        if not tags:
+            return
+
+        params = { 'Action': 'CreateTags',
+                   'ResourceId.0': node.id }
+        for i, key in enumerate(tags):
+            params['Tag.%d.Key' % i] = key
+            params['Tag.%d.Value' % i] = tags[key]
+
+        self.connection.request(self.path,
+                                params=params.copy()).object
+
+    def ex_delete_tags(self, node, tags):
+        """
+        Delete tags from an instance.
+
+        @type node: C{Node}
+        @param node: Node instance
+        @param tags: A dictionary or other mapping of strings to strings,
+                     specifying the tag names and tag values to be deleted.
+        """
+        if not tags:
+            return
+
+        params = { 'Action': 'DeleteTags',
+                   'ResourceId.0': node.id }
+        for i, key in enumerate(tags):
+            params['Tag.%d.Key' % i] = key
+            params['Tag.%d.Value' % i] = tags[key]
+
+        self.connection.request(self.path,
+                                params=params.copy()).object
+
+    def ex_describe_addresses(self, nodes):
+        """
+        Return Elastic IP addresses for all the nodes in the provided list.
+
+        @type nodes: C{list}
+        @param nodes: List of C{Node} instances
+
+        @return dict Dictionary where a key is a node ID and the value is a
+                     list with the Elastic IP addresses associated with this node.
+        """
+        if not nodes:
+            return {}
+
+        params = { 'Action': 'DescribeAddresses' }
+
+        if len(nodes) == 1:
+           params.update({
+                  'Filter.0.Name': 'instance-id',
+                  'Filter.0.Value.0': nodes[0].id
+           })
+
+        result = self.connection.request(self.path,
+                                         params=params.copy()).object
+
+        node_instance_ids = [ node.id for node in nodes ]
+        nodes_elastic_ip_mappings = {}
+
+        for node_id in node_instance_ids:
+            nodes_elastic_ip_mappings.setdefault(node_id, [])
+        for element in self._findall(result, 'addressesSet/item'):
+            instance_id = self._findtext(element, 'instanceId')
+            ip_address = self._findtext(element, 'publicIp')
+
+            if instance_id not in node_instance_ids:
+                continue
+
+            nodes_elastic_ip_mappings[instance_id].append(ip_address)
+        return nodes_elastic_ip_mappings
+
+    def ex_describe_addresses_for_node(self, node):
+        """
+        Return a list of Elastic IP addresses associated with this node.
+
+        @type node: C{Node}
+        @param node: Node instance
+
+        @return list Elastic IP addresses attached to this node.
+        """
+        node_elastic_ips = self.ex_describe_addresses([node])
+        return node_elastic_ips[node.id]
+
+    def create_node(self, **kwargs):
+        """Create a new EC2 node
+
+        See L{NodeDriver.create_node} for more keyword args.
+        Reference: http://bit.ly/8ZyPSy [docs.amazonwebservices.com]
+
+        @keyword    ex_mincount: Minimum number of instances to launch
+        @type       ex_mincount: C{int}
+
+        @keyword    ex_maxcount: Maximum number of instances to launch
+        @type       ex_maxcount: C{int}
+
+        @keyword    ex_securitygroup: Name of security group
+        @type       ex_securitygroup: C{str}
+
+        @keyword    ex_keyname: The name of the key pair
+        @type       ex_keyname: C{str}
+
+        @keyword    ex_userdata: User data
+        @type       ex_userdata: C{str}
+
+        @keyword    ex_clienttoken: Unique identifier to ensure idempotency
+        @type       ex_clienttoken: C{str}
+        """
+        image = kwargs["image"]
+        size = kwargs["size"]
+        params = {
+            'Action': 'RunInstances',
+            'ImageId': image.id,
+            'MinCount': kwargs.get('ex_mincount','1'),
+            'MaxCount': kwargs.get('ex_maxcount','1'),
+            'InstanceType': size.id
+        }
+
+        if 'ex_securitygroup' in kwargs:
+            if not isinstance(kwargs['ex_securitygroup'], list):
+                kwargs['ex_securitygroup'] = [kwargs['ex_securitygroup']]
+            for sig in range(len(kwargs['ex_securitygroup'])):
+                params['SecurityGroup.%d' % (sig+1,)]  = kwargs['ex_securitygroup'][sig]
+
+        if 'location' in kwargs:
+            availability_zone = getattr(kwargs['location'], 'availability_zone',
+                                        None)
+            if availability_zone:
+                if availability_zone.region_name != self.region_name:
+                    raise AttributeError('Invalid availability zone: %s'
+                                         % (availability_zone.name))
+                params['Placement.AvailabilityZone'] = availability_zone.name
+
+        if 'ex_keyname' in kwargs:
+            params['KeyName'] = kwargs['ex_keyname']
+
+        if 'ex_userdata' in kwargs:
+            params['UserData'] = base64.b64encode(kwargs['ex_userdata'])
+
+        if 'ex_clienttoken' in kwargs:
+            params['ClientToken'] = kwargs['ex_clienttoken']
+
+        object = self.connection.request(self.path, params=params).object
+        nodes = self._to_nodes(object, 'instancesSet/item')
+
+        if len(nodes) == 1:
+            return nodes[0]
+        else:
+            return nodes
+
+    def reboot_node(self, node):
+        """
+        Reboot the node by passing in the node object
+        """
+        params = {'Action': 'RebootInstances'}
+        params.update(self._pathlist('InstanceId', [node.id]))
+        res = self.connection.request(self.path, params=params).object
+        return self._get_boolean(res)
+
+    def destroy_node(self, node):
+        """
+        Destroy node by passing in the node object
+        """
+        params = {'Action': 'TerminateInstances'}
+        params.update(self._pathlist('InstanceId', [node.id]))
+        res = self.connection.request(self.path, params=params).object
+        return self._get_terminate_boolean(res)
+
+class IdempotentParamError(LibcloudError):
+    """
+    Request used the same client token as a previous, but non-identical request.
+    """
+    def __str__(self):
+        return repr(self.value)
+
+class EC2EUConnection(EC2Connection):
+    """
+    Connection class for EC2 in the Western Europe Region
+    """
+    host = EC2_EU_WEST_HOST
+
+class EC2EUNodeDriver(EC2NodeDriver):
+    """
+    Driver class for EC2 in the Western Europe Region
+    """
+
+    name = 'Amazon EC2 (eu-west-1)'
+    friendly_name = 'Amazon Europe Ireland'
+    country = 'IE'
+    region_name = 'eu-west-1'
+    connectionCls = EC2EUConnection
+    _instance_types = EC2_EU_WEST_INSTANCE_TYPES
+
+class EC2USWestConnection(EC2Connection):
+    """
+    Connection class for EC2 in the Western US Region
+    """
+
+    host = EC2_US_WEST_HOST
+
+class EC2USWestNodeDriver(EC2NodeDriver):
+    """
+    Driver class for EC2 in the Western US Region
+    """
+
+    name = 'Amazon EC2 (us-west-1)'
+    friendly_name = 'Amazon US N. California'
+    country = 'US'
+    region_name = 'us-west-1'
+    connectionCls = EC2USWestConnection
+    _instance_types = EC2_US_WEST_INSTANCE_TYPES
+
+class EC2APSEConnection(EC2Connection):
+    """
+    Connection class for EC2 in the Southeast Asia Pacific Region
+    """
+
+    host = EC2_AP_SOUTHEAST_HOST
+
+class EC2APNEConnection(EC2Connection):
+    """
+    Connection class for EC2 in the Northeast Asia Pacific Region
+    """
+
+    host = EC2_AP_NORTHEAST_HOST
+
+class EC2APSENodeDriver(EC2NodeDriver):
+    """
+    Driver class for EC2 in the Southeast Asia Pacific Region
+    """
+
+    name = 'Amazon EC2 (ap-southeast-1)'
+    friendly_name = 'Amazon Asia-Pacific Singapore'
+    country = 'SG'
+    region_name = 'ap-southeast-1'
+    connectionCls = EC2APSEConnection
+    _instance_types = EC2_AP_SOUTHEAST_INSTANCE_TYPES
+
+class EC2APNENodeDriver(EC2NodeDriver):
+    """
+    Driver class for EC2 in the Northeast Asia Pacific Region
+    """
+
+    name = 'Amazon EC2 (ap-northeast-1)'
+    friendly_name = 'Amazon Asia-Pacific Tokyo'
+    country = 'JP'
+    region_name = 'ap-northeast-1'
+    connectionCls = EC2APNEConnection
+    _instance_types = EC2_AP_NORTHEAST_INSTANCE_TYPES
+
+class EucConnection(EC2Connection):
+    """
+    Connection class for Eucalyptus
+    """
+
+    host = None
+
+class EucNodeDriver(EC2NodeDriver):
+    """
+    Driver class for Eucalyptus
+    """
+
+    name = 'Eucalyptus'
+    connectionCls = EucConnection
+    _instance_types = EC2_US_WEST_INSTANCE_TYPES
+
+    def __init__(self, key, secret=None, secure=True, host=None, path=None, port=None):
+        super(EucNodeDriver, self).__init__(key, secret, secure, host, port)
+        if path is None:
+            path = "/services/Eucalyptus"
+        self.path = path
+
+    def list_locations(self):
+        raise NotImplementedError, \
+            'list_locations not implemented for this driver'

Added: incubator/libcloud/trunk/libcloud/compute/drivers/ecp.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/libcloud/compute/drivers/ecp.py?rev=1079029&view=auto
==============================================================================
--- incubator/libcloud/trunk/libcloud/compute/drivers/ecp.py (added)
+++ incubator/libcloud/trunk/libcloud/compute/drivers/ecp.py Mon Mar  7 23:44:06 2011
@@ -0,0 +1,360 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Enomaly ECP driver
+"""
+import time
+import base64
+import httplib
+import socket
+import os
+
+# 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
+
+from libcloud.common.base import Response, ConnectionUserAndKey
+from libcloud.compute.base import NodeDriver, NodeSize, NodeLocation
+from libcloud.compute.base import NodeImage, Node
+from libcloud.compute.types import Provider, NodeState, InvalidCredsError
+from libcloud.compute.base import is_private_subnet
+
+#Defaults
+API_HOST = ''
+API_PORT = (80,443)
+
+class ECPResponse(Response):
+
+    def success(self):
+        if self.status == httplib.OK or self.status == httplib.CREATED:
+            try:
+                j_body = json.loads(self.body)
+            except ValueError:
+                self.error = "JSON response cannot be decoded."
+                return False
+            if j_body['errno'] == 0:
+                return True
+            else:
+                self.error = "ECP error: %s" % j_body['message']
+                return False
+        elif self.status == httplib.UNAUTHORIZED:
+            raise InvalidCredsError()
+        else:
+            self.error = "HTTP Error Code: %s" % self.status
+        return False
+
+    def parse_error(self):
+        return self.error
+
+    #Interpret the json responses - no error checking required
+    def parse_body(self):
+        return json.loads(self.body)
+
+    def getheaders(self):
+        return self.headers
+
+class ECPConnection(ConnectionUserAndKey):
+    """
+    Connection class for the Enomaly ECP driver
+    """
+
+    responseCls = ECPResponse
+    host = API_HOST
+    port = API_PORT
+
+    def add_default_headers(self, headers):
+        #Authentication
+        username = self.user_id
+        password = self.key
+        base64string =  base64.encodestring(
+                '%s:%s' % (username, password))[:-1]
+        authheader =  "Basic %s" % base64string
+        headers['Authorization']= authheader
+
+        return headers
+
+    def _encode_multipart_formdata(self, fields):
+        """
+        Based on Wade Leftwich's function:
+        http://code.activestate.com/recipes/146306/
+        """
+        #use a random boundary that does not appear in the fields
+        boundary = ''
+        while boundary in ''.join(fields):
+            boundary = os.urandom(16).encode('hex')
+        L = []
+        for i in fields:
+            L.append('--' + boundary)
+            L.append('Content-Disposition: form-data; name="%s"' % i)
+            L.append('')
+            L.append(fields[i])
+        L.append('--' + boundary + '--')
+        L.append('')
+        body = '\r\n'.join(L)
+        content_type = 'multipart/form-data; boundary=%s' % boundary
+        header = {'Content-Type':content_type}
+        return header, body
+
+
+class ECPNodeDriver(NodeDriver):
+    """
+    Enomaly ECP node driver
+    """
+
+    name = "Enomaly Elastic Computing Platform"
+    type = Provider.ECP
+    connectionCls = ECPConnection
+
+    def list_nodes(self):
+        """
+        Returns a list of all running Nodes
+        """
+
+        #Make the call
+        res = self.connection.request('/rest/hosting/vm/list').parse_body()
+
+        #Put together a list of node objects
+        nodes=[]
+        for vm in res['vms']:
+            node = self._to_node(vm)
+            if 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']
+
+        public_ips = []
+        private_ips = []
+        for ip in iplist:
+            try:
+                socket.inet_aton(ip)
+            except socket.error:
+                # not a valid ip
+                continue
+            if is_private_subnet(ip):
+                private_ips.append(ip)
+            else:
+                public_ips.append(ip)
+
+        #Create the node object
+        n = Node(
+          id=vm['uuid'],
+          name=vm['name'],
+          state=NodeState.RUNNING,
+          public_ip=public_ips,
+          private_ip=private_ips,
+          driver=self,
+        )
+
+        return n
+
+    def reboot_node(self, node):
+        """
+        Shuts down a VM and then starts it again.
+        """
+
+        #Turn the VM off
+        #Black magic to make the POST requests work
+        d = self.connection._encode_multipart_formdata({'action':'stop'})
+        self.connection.request(
+                   '/rest/hosting/vm/%s' % node.id,
+                   method='POST',
+                   headers=d[0],
+                   data=d[1]
+        ).parse_body()
+
+        node.state = NodeState.REBOOTING
+        #Wait for it to turn off and then continue (to turn it on again)
+        while node.state == NodeState.REBOOTING:
+            #Check if it's off.
+            response = self.connection.request(
+                     '/rest/hosting/vm/%s' % node.id
+                     ).parse_body()
+            if response['vm']['state'] == 'off':
+                node.state = NodeState.TERMINATED
+            else:
+                time.sleep(5)
+
+
+        #Turn the VM back on.
+        #Black magic to make the POST requests work
+        d = self.connection._encode_multipart_formdata({'action':'start'})
+        self.connection.request(
+            '/rest/hosting/vm/%s' % node.id,
+            method='POST',
+            headers=d[0],
+            data=d[1]
+        ).parse_body()
+
+        node.state = NodeState.RUNNING
+        return True
+
+    def destroy_node(self, node):
+        """
+        Shuts down and deletes a VM.
+        """
+
+        #Shut down first
+        #Black magic to make the POST requests work
+        d = self.connection._encode_multipart_formdata({'action':'stop'})
+        self.connection.request(
+            '/rest/hosting/vm/%s' % node.id,
+            method = 'POST',
+            headers=d[0],
+            data=d[1]
+        ).parse_body()
+
+        #Ensure there was no applicationl level error
+        node.state = NodeState.PENDING
+        #Wait for the VM to turn off before continuing
+        while node.state == NodeState.PENDING:
+            #Check if it's off.
+            response = self.connection.request(
+                       '/rest/hosting/vm/%s' % node.id
+                       ).parse_body()
+            if response['vm']['state'] == 'off':
+                node.state = NodeState.TERMINATED
+            else:
+                time.sleep(5)
+
+        #Delete the VM
+        #Black magic to make the POST requests work
+        d = self.connection._encode_multipart_formdata({'action':'delete'})
+        self.connection.request(
+            '/rest/hosting/vm/%s' % (node.id),
+            method='POST',
+            headers=d[0],
+            data=d[1]
+        ).parse_body()
+
+        return True
+
+    def list_images(self, location=None):
+        """
+        Returns a list of all package templates aka appiances aka images
+        """
+
+        #Make the call
+        response = self.connection.request(
+            '/rest/hosting/ptemplate/list').parse_body()
+
+        #Turn the response into an array of NodeImage objects
+        images = []
+        for ptemplate in response['packages']:
+            images.append(NodeImage(
+                id = ptemplate['uuid'],
+                name= '%s: %s' % (ptemplate['name'], ptemplate['description']),
+                driver = self,
+                ))
+
+        return images
+
+
+    def list_sizes(self, location=None):
+        """
+        Returns a list of all hardware templates
+        """
+
+        #Make the call
+        response = self.connection.request(
+            '/rest/hosting/htemplate/list').parse_body()
+
+        #Turn the response into an array of NodeSize objects
+        sizes = []
+        for htemplate in response['templates']:
+            sizes.append(NodeSize(
+                id = htemplate['uuid'],
+                name = htemplate['name'],
+                ram = htemplate['memory'],
+                disk = 0, #Disk is independent of hardware template
+                bandwidth = 0, #There is no way to keep track of bandwidth
+                price = 0, #The billing system is external
+                driver = self,
+                ))
+
+        return sizes
+
+    def list_locations(self):
+        """
+        This feature does not exist in ECP. Returns hard coded dummy location.
+        """
+        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()
+
+        #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()
+
+        #Create a node object and return it.
+        n = Node(
+            id=response['machine_id'],
+            name=data['name'],
+            state=NodeState.PENDING,
+            public_ip=[],
+            private_ip=[],
+            driver=self,
+        )
+
+        return n

Added: incubator/libcloud/trunk/libcloud/compute/drivers/elastichosts.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/libcloud/compute/drivers/elastichosts.py?rev=1079029&view=auto
==============================================================================
--- incubator/libcloud/trunk/libcloud/compute/drivers/elastichosts.py (added)
+++ incubator/libcloud/trunk/libcloud/compute/drivers/elastichosts.py Mon Mar  7 23:44:06 2011
@@ -0,0 +1,568 @@
+# 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.
+"""
+ElasticHosts Driver
+"""
+import re
+import time
+import base64
+
+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
+
+# 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 for the base connection clase.
+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 = {
+    '38df0986-4d85-4b76-b502-3878ffc80161': {
+        'uuid': '38df0986-4d85-4b76-b502-3878ffc80161',
+        'description': 'CentOS Linux 5.5',
+        'size_gunzipped': '3GB',
+        'supports_deployment': True,
+    },
+    '980cf63c-f21e-4382-997b-6541d5809629': {
+        'uuid': '980cf63c-f21e-4382-997b-6541d5809629',
+        'description': 'Debian Linux 5.0',
+        'size_gunzipped': '1GB',
+        'supports_deployment': True,
+    },
+    'aee5589a-88c3-43ef-bb0a-9cab6e64192d': {
+        'uuid': 'aee5589a-88c3-43ef-bb0a-9cab6e64192d',
+        'description': 'Ubuntu Linux 10.04',
+        'size_gunzipped': '1GB',
+        'supports_deployment': True,
+    },
+    'b9d0eb72-d273-43f1-98e3-0d4b87d372c0': {
+        'uuid': 'b9d0eb72-d273-43f1-98e3-0d4b87d372c0',
+        'description': 'Windows Web Server 2008',
+        'size_gunzipped': '13GB',
+        'supports_deployment': False,
+    },
+    '30824e97-05a4-410c-946e-2ba5a92b07cb': {
+        'uuid': '30824e97-05a4-410c-946e-2ba5a92b07cb',
+        'description': 'Windows Web Server 2008 R2',
+        'size_gunzipped': '13GB',
+        'supports_deployment': False,
+    },
+    '9ecf810e-6ad1-40ef-b360-d606f0444671': {
+        'uuid': '9ecf810e-6ad1-40ef-b360-d606f0444671',
+        'description': 'Windows Web Server 2008 R2 + SQL Server',
+        'size_gunzipped': '13GB',
+        'supports_deployment': False,
+    },
+    '10a88d1c-6575-46e3-8d2c-7744065ea530': {
+        'uuid': '10a88d1c-6575-46e3-8d2c-7744065ea530',
+        'description': 'Windows Server 2008 Standard R2',
+        'size_gunzipped': '13GB',
+        'supports_deployment': False,
+    },
+    '2567f25c-8fb8-45c7-95fc-bfe3c3d84c47': {
+        'uuid': '2567f25c-8fb8-45c7-95fc-bfe3c3d84c47',
+        'description': 'Windows Server 2008 Standard R2 + SQL Server',
+        'size_gunzipped': '13GB',
+        'supports_deployment': False,
+    },
+}
+
+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
+    """
+
+    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
+    """
+
+    type = Provider.ELASTICHOSTS
+    name = 'ElasticHosts'
+    connectionCls = ElasticHostsBaseConnection
+    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 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
+                               (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 == 200 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):
+    """
+    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

Added: incubator/libcloud/trunk/libcloud/compute/drivers/gogrid.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/libcloud/compute/drivers/gogrid.py?rev=1079029&view=auto
==============================================================================
--- incubator/libcloud/trunk/libcloud/compute/drivers/gogrid.py (added)
+++ incubator/libcloud/trunk/libcloud/compute/drivers/gogrid.py Mon Mar  7 23:44:06 2011
@@ -0,0 +1,470 @@
+# 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.
+"""
+GoGrid driver
+"""
+import time
+import hashlib
+
+try:
+    import json
+except ImportError:
+    import simplejson as json
+
+from libcloud.common.base import ConnectionUserAndKey, Response
+from libcloud.common.types import InvalidCredsError, LibcloudError
+from libcloud.common.types import MalformedResponseError
+from libcloud.compute.providers import Provider
+from libcloud.compute.types import NodeState
+from libcloud.compute.base import Node, NodeDriver
+from libcloud.compute.base import NodeSize, NodeImage, NodeLocation
+
+HOST = 'api.gogrid.com'
+PORTS_BY_SECURITY = { True: 443, False: 80 }
+API_VERSION = '1.7'
+
+STATE = {
+    "Starting": NodeState.PENDING,
+    "On": NodeState.RUNNING,
+    "Off": NodeState.PENDING,
+    "Restarting": NodeState.REBOOTING,
+    "Saving": NodeState.PENDING,
+    "Restoring": NodeState.PENDING,
+}
+
+GOGRID_INSTANCE_TYPES = {'512MB': {'id': '512MB',
+                       'name': '512MB',
+                       'ram': 512,
+                       'disk': 30,
+                       'bandwidth': None,
+                       'price':0.095},
+        '1GB': {'id': '1GB',
+                       'name': '1GB',
+                       'ram': 1024,
+                       'disk': 60,
+                       'bandwidth': None,
+                       'price':0.19},
+        '2GB': {'id': '2GB',
+                       'name': '2GB',
+                       'ram': 2048,
+                       'disk': 120,
+                       'bandwidth': None,
+                       'price':0.38},
+        '4GB': {'id': '4GB',
+                       'name': '4GB',
+                       'ram': 4096,
+                       'disk': 240,
+                       'bandwidth': None,
+                       'price':0.76},
+        '8GB': {'id': '8GB',
+                       'name': '8GB',
+                       'ram': 8192,
+                       'disk': 480,
+                       'bandwidth': None,
+                       'price':1.52}}
+
+
+class GoGridResponse(Response):
+
+    def success(self):
+        if self.status == 403:
+            raise InvalidCredsError('Invalid credentials', GoGridNodeDriver)
+        if self.status == 401:
+            raise InvalidCredsError('API Key has insufficient rights', GoGridNodeDriver)
+        if not self.body:
+            return None
+        try:
+            return json.loads(self.body)['status'] == 'success'
+        except ValueError:
+            raise MalformedResponseError('Malformed reply', body=self.body, driver=GoGridNodeDriver)
+
+    def parse_body(self):
+        if not self.body:
+            return None
+        return json.loads(self.body)
+
+    def parse_error(self):
+        try:
+            return json.loads(self.body)["list"][0]['message']
+        except ValueError:
+            return None
+
+class GoGridConnection(ConnectionUserAndKey):
+    """
+    Connection class for the GoGrid driver
+    """
+
+    host = HOST
+    responseCls = GoGridResponse
+
+    def add_default_params(self, params):
+        params["api_key"] = self.user_id
+        params["v"] = API_VERSION
+        params["format"] = 'json'
+        params["sig"] = self.get_signature(self.user_id, self.key)
+
+        return params
+
+    def get_signature(self, key, secret):
+        """ create sig from md5 of key + secret + time """
+        m = hashlib.md5(key+secret+str(int(time.time())))
+        return m.hexdigest()
+
+class GoGridIpAddress(object):
+    """
+    IP Address
+    """
+
+    def __init__(self, id, ip, public, state, subnet):
+        self.id = id
+        self.ip = ip
+        self.public = public
+        self.state = state
+        self.subnet = subnet
+
+class GoGridNode(Node):
+    # Generating uuid based on public ip to get around missing id on
+    # create_node in gogrid api
+    #
+    # Used public ip since it is not mutable and specified at create time,
+    # so uuid of node should not change after add is completed
+    def get_uuid(self):
+        return hashlib.sha1(
+            "%s:%d" % (self.public_ip,self.driver.type)
+        ).hexdigest()
+
+class GoGridNodeDriver(NodeDriver):
+    """
+    GoGrid node driver
+    """
+
+    connectionCls = GoGridConnection
+    type = Provider.GOGRID
+    name = 'GoGrid'
+    features = {"create_node": ["generates_password"]}
+
+    _instance_types = GOGRID_INSTANCE_TYPES
+
+    def _get_state(self, element):
+        try:
+            return STATE[element['state']['name']]
+        except:
+            pass
+        return NodeState.UNKNOWN
+
+    def _get_ip(self, element):
+        return element.get('ip').get('ip')
+
+    def _get_id(self, element):
+        return element.get('id')
+
+    def _to_node(self, element, password=None):
+        state = self._get_state(element)
+        ip = self._get_ip(element)
+        id = self._get_id(element)
+        n = GoGridNode(id=id,
+                 name=element['name'],
+                 state=state,
+                 public_ip=[ip],
+                 private_ip=[],
+                 extra={'ram': element.get('ram').get('name'),
+                     'isSandbox': element['isSandbox'] == 'true'},
+                 driver=self.connection.driver)
+        if password:
+            n.extra['password'] = password
+
+        return n
+
+    def _to_image(self, element):
+        n = NodeImage(id=element['id'],
+                      name=element['friendlyName'],
+                      driver=self.connection.driver)
+        return n
+
+    def _to_images(self, object):
+        return [ self._to_image(el)
+                 for el in object['list'] ]
+
+    def _to_location(self, element):
+        location = NodeLocation(id=element['id'],
+                name=element['name'],
+                country="US",
+                driver=self.connection.driver)
+        return location
+
+    def _to_ip(self, element):
+        ip = GoGridIpAddress(id=element['id'],
+                ip=element['ip'],
+                public=element['public'],
+                subnet=element['subnet'],
+                state=element["state"]["name"])
+        ip.location = self._to_location(element['datacenter'])
+        return ip
+
+    def _to_ips(self, object):
+        return [ self._to_ip(el)
+                for el in object['list'] ]
+
+    def _to_locations(self, object):
+        return [self._to_location(el)
+                for el in object['list']]
+
+    def list_images(self, location=None):
+        params = {}
+        if location is not None:
+            params["datacenter"] = location.id
+        images = self._to_images(
+                self.connection.request('/api/grid/image/list', params).object)
+        return images
+
+    def list_nodes(self):
+        passwords_map = {}
+
+        res = self._server_list()
+        try:
+          for password in self._password_list()['list']:
+              try:
+                  passwords_map[password['server']['id']] = password['password']
+              except KeyError:
+                  pass
+        except InvalidCredsError:
+          # some gogrid API keys don't have permission to access the password list.
+          pass
+
+        return [ self._to_node(el, passwords_map.get(el.get('id')))
+                 for el
+                 in res['list'] ]
+
+    def reboot_node(self, node):
+        id = node.id
+        power = 'restart'
+        res = self._server_power(id, power)
+        if not res.success():
+            raise Exception(res.parse_error())
+        return True
+
+    def destroy_node(self, node):
+        id = node.id
+        res = self._server_delete(id)
+        if not res.success():
+            raise Exception(res.parse_error())
+        return True
+
+    def _server_list(self):
+        return self.connection.request('/api/grid/server/list').object
+
+    def _password_list(self):
+        return self.connection.request('/api/support/password/list').object
+
+    def _server_power(self, id, power):
+        # power in ['start', 'stop', 'restart']
+        params = {'id': id, 'power': power}
+        return self.connection.request("/api/grid/server/power", params,
+                                         method='POST')
+
+    def _server_delete(self, id):
+        params = {'id': id}
+        return self.connection.request("/api/grid/server/delete", params,
+                                        method='POST')
+
+    def _get_first_ip(self, location=None):
+        ips = self.ex_list_ips(public=True, assigned=False, location=location)
+        try:
+            return ips[0].ip 
+        except IndexError:
+            raise LibcloudError('No public unassigned IPs left',
+                    GoGridNodeDriver)
+
+    def list_sizes(self, location=None):
+        return [ NodeSize(driver=self.connection.driver, **i)
+                    for i in self._instance_types.values() ]
+
+    def list_locations(self):
+        locations = self._to_locations(
+            self.connection.request('/api/common/lookup/list',
+                params={'lookup': 'ip.datacenter'}).object)
+        return locations
+
+    def ex_create_node_nowait(self, **kwargs):
+        """Don't block until GoGrid allocates id for a node
+        but return right away with id == None.
+
+        The existance of this method is explained by the fact
+        that GoGrid assigns id to a node only few minutes after
+        creation."""
+        name = kwargs['name']
+        image = kwargs['image']
+        size = kwargs['size']
+        try:
+            ip = kwargs['ex_ip']
+        except KeyError:
+            ip = self._get_first_ip(kwargs.get('location'))
+
+        params = {'name': name,
+                  'image': image.id,
+                  'description': kwargs.get('ex_description', ''),
+                  'isSandbox': str(kwargs.get('ex_issandbox', False)).lower(),
+                  'server.ram': size.id,
+                  'ip': ip}
+
+        object = self.connection.request('/api/grid/server/add',
+                                         params=params, method='POST').object
+        node = self._to_node(object['list'][0])
+
+        return node
+
+    def create_node(self, **kwargs):
+        """Create a new GoGird node
+
+        See L{NodeDriver.create_node} for more keyword args.
+
+        @keyword    ex_description: Description of a Node
+        @type       ex_description: C{string}
+        @keyword    ex_issandbox: Should server be sendbox?
+        @type       ex_issandbox: C{bool}
+        @keyword    ex_ip: Public IP address to use for a Node. If not
+                    specified, first available IP address will be picked
+        @type       ex_ip: C{string}
+        """
+        node = self.ex_create_node_nowait(**kwargs)
+
+        timeout = 60 * 20
+        waittime = 0
+        interval = 2 * 60
+
+        while node.id is None and waittime < timeout:
+            nodes = self.list_nodes()
+
+            for i in nodes:
+                if i.public_ip[0] == node.public_ip[0] and i.id is not None:
+                    return i
+
+            waittime += interval
+            time.sleep(interval)
+
+        if id is None:
+            raise Exception("Wasn't able to wait for id allocation for the node %s" % str(node))
+
+        return node
+
+    def ex_save_image(self, node, name):
+        """Create an image for node.
+
+        Please refer to GoGrid documentation to get info
+        how prepare a node for image creation:
+
+        http://wiki.gogrid.com/wiki/index.php/MyGSI
+
+        @keyword    node: node to use as a base for image
+        @type       node: L{Node}
+        @keyword    name: name for new image
+        @type       name: C{string}
+        """
+        params = {'server': node.id,
+                  'friendlyName': name}
+        object = self.connection.request('/api/grid/image/save', params=params,
+                                         method='POST').object
+
+        return self._to_images(object)[0]
+
+    def ex_edit_node(self, **kwargs):
+        """Change attributes of a node.
+
+        @keyword    node: node to be edited
+        @type       node: L{Node}
+        @keyword    size: new size of a node
+        @type       size: L{NodeSize}
+        @keyword    ex_description: new description of a node
+        @type       ex_description: C{string}
+        """
+        node = kwargs['node']
+        size = kwargs['size']
+
+        params = {'id': node.id,
+                'server.ram': size.id}
+
+        if 'ex_description' in kwargs:
+            params['description'] = kwargs['ex_description']
+
+        object = self.connection.request('/api/grid/server/edit',
+                params=params).object
+
+        return self._to_node(object['list'][0])
+
+    def ex_edit_image(self, **kwargs):
+        """Edit metadata of a server image.
+
+        @keyword    image: image to be edited
+        @type       image: L{NodeImage}
+        @keyword    public: should be the image public?
+        @type       public: C{bool}
+        @keyword    ex_description: description of the image (optional)
+        @type       ex_description: C{string}
+        @keyword    name: name of the image
+        @type       name C{string}
+
+        """
+
+        image = kwargs['image']
+        public = kwargs['public']
+
+        params = {'id': image.id,
+                'isPublic': str(public).lower()}
+
+        if 'ex_description' in kwargs:
+            params['description'] = kwargs['ex_description']
+
+        if 'name' in kwargs:
+            params['friendlyName'] = kwargs['name']
+
+        object = self.connection.request('/api/grid/image/edit',
+                params=params).object
+
+        return self._to_image(object['list'][0])
+
+    def ex_list_ips(self, **kwargs):
+        """Return list of IP addresses assigned to
+        the account.
+
+        @keyword    public: set to True to list only
+                    public IPs or False to list only
+                    private IPs. Set to None or not specify
+                    at all not to filter by type
+        @type       public: C{bool}
+        @keyword    assigned: set to True to list only addresses
+                    assigned to servers, False to list unassigned
+                    addresses and set to None or don't set at all
+                    not no filter by state
+        @type       assigned: C{bool}
+        @keyword    location: filter IP addresses by location
+        @type       location: L{NodeLocation}
+        @return:    C{list} of L{GoGridIpAddress}es
+        """
+
+        params = {}
+
+        if "public" in kwargs and kwargs["public"] is not None:
+            params["ip.type"] = {True: "Public",
+                    False: "Private"}[kwargs["public"]]
+        if "assigned" in kwargs and kwargs["assigned"] is not None:
+            params["ip.state"] = {True: "Assigned",
+                    False: "Unassigned"}[kwargs["assigned"]]
+        if "location" in kwargs and kwargs['location'] is not None:
+            params['datacenter'] = kwargs['location'].id
+
+        ips = self._to_ips(
+                self.connection.request('/api/grid/ip/list',
+                    params=params).object)
+        return ips