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