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 [2/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/base.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/libcloud/compute/base.py?rev=1079029&view=auto
==============================================================================
--- incubator/libcloud/trunk/libcloud/compute/base.py (added)
+++ incubator/libcloud/trunk/libcloud/compute/base.py Mon Mar  7 23:44:06 2011
@@ -0,0 +1,573 @@
+# 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.
+
+"""
+Provides base classes for working with drivers
+"""
+import httplib, urllib
+import time
+import hashlib
+import StringIO
+import ssl
+import os
+import socket
+import struct
+
+from libcloud.common.base import ConnectionKey, ConnectionUserAndKey
+from libcloud.compute.types import NodeState, DeploymentError
+from libcloud.compute.ssh import SSHClient
+from libcloud.httplib_ssl import LibcloudHTTPSConnection
+from httplib import HTTPConnection as LibcloudHTTPConnection
+
+class Node(object):
+    """
+    Provide a common interface for handling nodes of all types.
+
+    The Node object provides the interface in libcloud through which
+    we can manipulate nodes in different cloud providers in the same
+    way.  Node objects don't actually do much directly themselves,
+    instead the node driver handles the connection to the node.
+
+    You don't normally create a node object yourself; instead you use
+    a driver and then have that create the node for you.
+
+    >>> from libcloud.compute.drivers.dummy import DummyNodeDriver
+    >>> driver = DummyNodeDriver(0)
+    >>> node = driver.create_node()
+    >>> node.public_ip[0]
+    '127.0.0.3'
+    >>> node.name
+    'dummy-3'
+
+    You can also get nodes from the driver's list_node function.
+
+    >>> node = driver.list_nodes()[0]
+    >>> node.name
+    'dummy-1'
+
+    the node keeps a reference to its own driver which means that we
+    can work on nodes from different providers without having to know
+    which is which.
+
+    >>> driver = DummyNodeDriver(72)
+    >>> node2 = driver.create_node()
+    >>> node.driver.creds
+    0
+    >>> node2.driver.creds
+    72
+
+    Althrough Node objects can be subclassed, this isn't normally
+    done.  Instead, any driver specific information is stored in the
+    "extra" proproperty of the node.
+
+    >>> node.extra
+    {'foo': 'bar'}
+
+    """
+
+    def __init__(self, id, name, state, public_ip, private_ip,
+                 driver, extra=None):
+        self.id = str(id) if id else None
+        self.name = name
+        self.state = state
+        self.public_ip = public_ip
+        self.private_ip = private_ip
+        self.driver = driver
+        self.uuid = self.get_uuid()
+        if not extra:
+            self.extra = {}
+        else:
+            self.extra = extra
+
+    def get_uuid(self):
+        """Unique hash for this node
+
+        @return: C{string}
+
+        The hash is a function of an SHA1 hash of the node's ID and
+        its driver which means that it should be unique between all
+        nodes.  In some subclasses (e.g. GoGrid) there is no ID
+        available so the public IP address is used.  This means that,
+        unlike a properly done system UUID, the same UUID may mean a
+        different system install at a different time
+
+        >>> from libcloud.compute.drivers.dummy import DummyNodeDriver
+        >>> driver = DummyNodeDriver(0)
+        >>> node = driver.create_node()
+        >>> node.get_uuid()
+        'd3748461511d8b9b0e0bfa0d4d3383a619a2bb9f'
+
+        Note, for example, that this example will always produce the
+        same UUID!
+        """
+        return hashlib.sha1("%s:%d" % (self.id,self.driver.type)).hexdigest()
+
+    def reboot(self):
+        """Reboot this node
+
+        @return: C{bool}
+
+        This calls the node's driver and reboots the node
+
+        >>> from libcloud.compute.drivers.dummy import DummyNodeDriver
+        >>> driver = DummyNodeDriver(0)
+        >>> node = driver.create_node()
+        >>> from libcloud.compute.types import NodeState
+        >>> node.state == NodeState.RUNNING
+        True
+        >>> node.state == NodeState.REBOOTING
+        False
+        >>> node.reboot()
+        True
+        >>> node.state == NodeState.REBOOTING
+        True
+        """
+        return self.driver.reboot_node(self)
+
+    def destroy(self):
+        """Destroy this node
+
+        @return: C{bool}
+
+        This calls the node's driver and destroys the node
+
+        >>> from libcloud.compute.drivers.dummy import DummyNodeDriver
+        >>> driver = DummyNodeDriver(0)
+        >>> from libcloud.compute.types import NodeState
+        >>> node = driver.create_node()
+        >>> node.state == NodeState.RUNNING
+        True
+        >>> node.destroy()
+        True
+        >>> node.state == NodeState.RUNNING
+        False
+
+        """
+        return self.driver.destroy_node(self)
+
+    def __repr__(self):
+        return (('<Node: uuid=%s, name=%s, state=%s, public_ip=%s, '
+                 'provider=%s ...>')
+                % (self.uuid, self.name, self.state, self.public_ip,
+                   self.driver.name))
+
+
+class NodeSize(object):
+    """
+    A Base NodeSize class to derive from.
+
+    NodeSizes are objects which are typically returned a driver's
+    list_sizes function.  They contain a number of different
+    parameters which define how big an image is.
+
+    The exact parameters available depends on the provider.
+
+    N.B. Where a parameter is "unlimited" (for example bandwidth in
+    Amazon) this will be given as 0.
+
+    >>> from libcloud.compute.drivers.dummy import DummyNodeDriver
+    >>> driver = DummyNodeDriver(0)
+    >>> size = driver.list_sizes()[0]
+    >>> size.ram
+    128
+    >>> size.bandwidth
+    500
+    >>> size.price
+    4
+    """
+
+    def __init__(self, id, name, ram, disk, bandwidth, price, driver):
+        self.id = str(id)
+        self.name = name
+        self.ram = ram
+        self.disk = disk
+        self.bandwidth = bandwidth
+        self.price = price
+        self.driver = driver
+    def __repr__(self):
+        return (('<NodeSize: id=%s, name=%s, ram=%s disk=%s bandwidth=%s '
+                 'price=%s driver=%s ...>')
+                % (self.id, self.name, self.ram, self.disk, self.bandwidth,
+                   self.price, self.driver.name))
+
+
+class NodeImage(object):
+    """
+    An operating system image.
+
+    NodeImage objects are typically returned by the driver for the
+    cloud provider in response to the list_images function
+
+    >>> from libcloud.compute.drivers.dummy import DummyNodeDriver
+    >>> driver = DummyNodeDriver(0)
+    >>> image = driver.list_images()[0]
+    >>> image.name
+    'Ubuntu 9.10'
+
+    Apart from name and id, there is no further standard information;
+    other parameters are stored in a driver specific "extra" variable
+
+    When creating a node, a node image should be given as an argument
+    to the create_node function to decide which OS image to use.
+
+    >>> node = driver.create_node(image=image)
+
+    """
+
+    def __init__(self, id, name, driver, extra=None):
+        self.id = str(id)
+        self.name = name
+        self.driver = driver
+        if not extra:
+            self.extra = {}
+        else:
+            self.extra = extra
+    def __repr__(self):
+        return (('<NodeImage: id=%s, name=%s, driver=%s  ...>')
+                % (self.id, self.name, self.driver.name))
+
+class NodeLocation(object):
+    """
+    A physical location where nodes can be.
+
+    >>> from libcloud.compute.drivers.dummy import DummyNodeDriver
+    >>> driver = DummyNodeDriver(0)
+    >>> location = driver.list_locations()[0]
+    >>> location.country
+    'US'
+    """
+
+    def __init__(self, id, name, country, driver):
+        self.id = str(id)
+        self.name = name
+        self.country = country
+        self.driver = driver
+    def __repr__(self):
+        return (('<NodeLocation: id=%s, name=%s, country=%s, driver=%s>')
+                % (self.id, self.name, self.country, self.driver.name))
+
+class NodeAuthSSHKey(object):
+    """
+    An SSH key to be installed for authentication to a node.
+
+    This is the actual contents of the users ssh public key which will
+    normally be installed as root's public key on the node.
+
+    >>> pubkey = '...' # read from file
+    >>> from libcloud.compute.base import NodeAuthSSHKey
+    >>> k = NodeAuthSSHKey(pubkey)
+    >>> k
+    <NodeAuthSSHKey>
+
+    """
+    def __init__(self, pubkey):
+        self.pubkey = pubkey
+    def __repr__(self):
+        return '<NodeAuthSSHKey>'
+
+class NodeAuthPassword(object):
+    """
+    A password to be used for authentication to a node.
+    """
+    def __init__(self, password):
+        self.password = password
+    def __repr__(self):
+        return '<NodeAuthPassword>'
+
+class NodeDriver(object):
+    """
+    A base NodeDriver class to derive from
+
+    This class is always subclassed by a specific driver.  For
+    examples of base behavior of most functions (except deploy node)
+    see the dummy driver.
+
+    """
+
+    connectionCls = ConnectionKey
+    name = None
+    type = None
+    port = None
+    features = {"create_node": []}
+    """
+    List of available features for a driver.
+        - L{create_node}
+            - ssh_key: Supports L{NodeAuthSSHKey} as an authentication method
+              for nodes.
+            - password: Supports L{NodeAuthPassword} as an authentication
+              method for nodes.
+            - generates_password: Returns a password attribute on the Node
+              object returned from creation.
+    """
+
+    NODE_STATE_MAP = {}
+
+    def __init__(self, key, secret=None, secure=True, host=None, port=None):
+        """
+        @keyword    key:    API key or username to used
+        @type       key:    str
+
+        @keyword    secret: Secret password to be used
+        @type       secret: str
+
+        @keyword    secure: Weither to use HTTPS or HTTP. Note: Some providers
+                            only support HTTPS, and it is on by default.
+        @type       secure: bool
+
+        @keyword    host: Override hostname used for connections.
+        @type       host: str
+
+        @keyword    port: Override port used for connections.
+        @type       port: int
+        """
+        self.key = key
+        self.secret = secret
+        self.secure = secure
+        args = [self.key]
+
+        if self.secret != None:
+            args.append(self.secret)
+
+        args.append(secure)
+
+        if host != None:
+            args.append(host)
+
+        if port != None:
+            args.append(port)
+
+        self.connection = self.connectionCls(*args)
+
+        self.connection.driver = self
+        self.connection.connect()
+
+    def create_node(self, **kwargs):
+        """Create a new node instance.
+
+        @keyword    name:   String with a name for this new node (required)
+        @type       name:   str
+
+        @keyword    size:   The size of resources allocated to this node.
+                            (required)
+        @type       size:   L{NodeSize}
+
+        @keyword    image:  OS Image to boot on node. (required)
+        @type       image:  L{NodeImage}
+
+        @keyword    location: Which data center to create a node in. If empty,
+                              undefined behavoir will be selected. (optional)
+        @type       location: L{NodeLocation}
+
+        @keyword    auth:   Initial authentication information for the node
+                            (optional)
+        @type       auth:   L{NodeAuthSSHKey} or L{NodeAuthPassword}
+
+        @return: The newly created L{Node}.
+        """
+        raise NotImplementedError, \
+            'create_node not implemented for this driver'
+
+    def destroy_node(self, node):
+        """Destroy a node.
+
+        Depending upon the provider, this may destroy all data associated with
+        the node, including backups.
+
+        @return: C{bool} True if the destroy was successful, otherwise False
+        """
+        raise NotImplementedError, \
+            'destroy_node not implemented for this driver'
+
+    def reboot_node(self, node):
+        """
+        Reboot a node.
+        @return: C{bool} True if the reboot was successful, otherwise False
+        """
+        raise NotImplementedError, \
+            'reboot_node not implemented for this driver'
+
+    def list_nodes(self):
+        """
+        List all nodes
+        @return: C{list} of L{Node} objects
+        """
+        raise NotImplementedError, \
+            'list_nodes not implemented for this driver'
+
+    def list_images(self, location=None):
+        """
+        List images on a provider
+        @return: C{list} of L{NodeImage} objects
+        """
+        raise NotImplementedError, \
+            'list_images not implemented for this driver'
+
+    def list_sizes(self, location=None):
+        """
+        List sizes on a provider
+        @return: C{list} of L{NodeSize} objects
+        """
+        raise NotImplementedError, \
+            'list_sizes not implemented for this driver'
+
+    def list_locations(self):
+        """
+        List data centers for a provider
+        @return: C{list} of L{NodeLocation} objects
+        """
+        raise NotImplementedError, \
+            'list_locations not implemented for this driver'
+
+    def deploy_node(self, **kwargs):
+        """
+        Create a new node, and start deployment.
+
+        Depends on a Provider Driver supporting either using a specific password
+        or returning a generated password.
+
+        This function may raise a L{DeploymentException}, if a create_node
+        call was successful, but there is a later error (like SSH failing or
+        timing out).  This exception includes a Node object which you may want
+        to destroy if incomplete deployments are not desirable.
+
+        @keyword    deploy: Deployment to run once machine is online and availble to SSH.
+        @type       deploy: L{Deployment}
+
+        @keyword    ssh_username: Optional name of the account which is used when connecting to
+                                  SSH server (default is root)
+        @type       ssh_username: C{str}
+
+        @keyword    ssh_port: Optional SSH server port (default is 22)
+        @type       ssh_port: C{int}
+
+        See L{NodeDriver.create_node} for more keyword args.
+
+        >>> from libcloud.compute.drivers.dummy import DummyNodeDriver
+        >>> from libcloud.deployment import ScriptDeployment, MultiStepDeployment
+        >>> from libcloud.compute.base import NodeAuthSSHKey
+        >>> driver = DummyNodeDriver(0)
+        >>> key = NodeAuthSSHKey('...') # read from file
+        >>> script = ScriptDeployment("yum -y install emacs strace tcpdump")
+        >>> msd = MultiStepDeployment([key, script])
+        >>> def d():
+        ...     try:
+        ...         node = driver.deploy_node(deploy=msd)
+        ...     except NotImplementedError:
+        ...         print "not implemented for dummy driver"
+        >>> d()
+        not implemented for dummy driver
+
+        Deploy node is typically not overridden in subclasses.  The
+        existing implementation should be able to handle most such.
+        """
+        # TODO: support ssh keys
+        WAIT_PERIOD=3
+        password = None
+
+        if 'generates_password' not in self.features["create_node"]:
+            if 'password' not in self.features["create_node"]:
+                raise NotImplementedError, \
+                    'deploy_node not implemented for this driver'
+
+            if not kwargs.has_key('auth'):
+                kwargs['auth'] = NodeAuthPassword(os.urandom(16).encode('hex'))
+
+            password = kwargs['auth'].password
+        node = self.create_node(**kwargs)
+        try:
+          if 'generates_password' in self.features["create_node"]:
+              password = node.extra.get('password')
+          start = time.time()
+          end = start + (60 * 15)
+          while time.time() < end:
+              # need to wait until we get a public IP address.
+              # TODO: there must be a better way of doing this
+              time.sleep(WAIT_PERIOD)
+              nodes = self.list_nodes()
+              nodes = filter(lambda n: n.uuid == node.uuid, nodes)
+              if len(nodes) == 0:
+                  raise DeploymentError(node, "Booted node[%s] is missing form list_nodes." % node)
+              if len(nodes) > 1:
+                  raise DeploymentError(node, "Booted single node[%s], but multiple nodes have same UUID"% node)
+
+              node = nodes[0]
+
+              if node.public_ip is not None and node.public_ip != "" and node.state == NodeState.RUNNING:
+                  break
+
+          ssh_username = kwargs.get('ssh_username', 'root')
+          ssh_port = kwargs.get('ssh_port', 22)
+
+          client = SSHClient(hostname=node.public_ip[0],
+                             port=ssh_port, username=ssh_username,
+                             password=password)
+          laste = None
+          while time.time() < end:
+              laste = None
+              try:
+                  client.connect()
+                  break
+              except (IOError, socket.gaierror, socket.error), e:
+                  laste = e
+              time.sleep(WAIT_PERIOD)
+          if laste is not None:
+              raise e
+
+          tries = 3
+          while tries >= 0:
+            try:
+              n = kwargs["deploy"].run(node, client)
+              client.close()
+              break
+            except Exception, e:
+              tries -= 1
+              if tries == 0:
+                raise
+              client.connect()
+
+        except DeploymentError, e:
+          raise
+        except Exception, e:
+          raise DeploymentError(node, e)
+        return n
+
+def is_private_subnet(ip):
+    """
+    Utility function to check if an IP address is inside a private subnet.
+
+    @type ip: C{str}
+    @keyword ip: IP address to check
+
+    @return: C{bool} if the specified IP address is private.
+    """
+    priv_subnets = [ {'subnet': '10.0.0.0', 'mask': '255.0.0.0'},
+                     {'subnet': '172.16.0.0', 'mask': '255.240.0.0'},
+                     {'subnet': '192.168.0.0', 'mask': '255.255.0.0'} ]
+
+    ip = struct.unpack('I',socket.inet_aton(ip))[0]
+
+    for network in priv_subnets:
+        subnet = struct.unpack('I',socket.inet_aton(network['subnet']))[0]
+        mask = struct.unpack('I',socket.inet_aton(network['mask']))[0]
+
+        if (ip & mask) == (subnet & mask):
+            return True
+
+    return False
+
+
+if __name__ == "__main__":
+    import doctest
+    doctest.testmod()

Added: incubator/libcloud/trunk/libcloud/compute/deployment.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/libcloud/compute/deployment.py?rev=1079029&view=auto
==============================================================================
--- incubator/libcloud/trunk/libcloud/compute/deployment.py (added)
+++ incubator/libcloud/trunk/libcloud/compute/deployment.py Mon Mar  7 23:44:06 2011
@@ -0,0 +1,130 @@
+# 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.
+
+"""
+Provides generic deployment steps for machines post boot.
+"""
+import os
+
+class Deployment(object):
+    """
+    Base class for deployment tasks.
+    """
+
+    def run(self, node, client):
+        """
+        Runs this deployment task on C{node} using the C{client} provided.
+
+        @type node: L{Node}
+        @keyword node: Node to operate one
+
+        @type client: L{BaseSSHClient}
+        @keyword client: Connected SSH client to use.
+
+        @return: L{Node}
+        """
+        raise NotImplementedError, \
+            'run not implemented for this deployment'
+
+
+class SSHKeyDeployment(Deployment):
+    """
+    Installs a public SSH Key onto a host.
+    """
+
+    def __init__(self, key):
+        """
+        @type key: C{str}
+        @keyword key: Contents of the public key write
+        """
+        self.key = key
+
+    def run(self, node, client):
+        """
+        Installs SSH key into C{.ssh/authorized_keys}
+
+        See also L{Deployment.run}
+        """
+        client.put(".ssh/authorized_keys", contents=self.key)
+        return node
+
+class ScriptDeployment(Deployment):
+    """
+    Runs an arbitrary Shell Script task.
+    """
+
+    def __init__(self, script, name=None, delete=False):
+        """
+        @type script: C{str}
+        @keyword script: Contents of the script to run
+
+        @type name: C{str}
+        @keyword name: Name of the script to upload it as, if not specified, a random name will be choosen.
+
+        @type delete: C{bool}
+        @keyword delete: Whether to delete the script on completion.
+        """
+        self.script = script
+        self.stdout = None
+        self.stderr = None
+        self.exit_status = None
+        self.delete = delete
+        self.name = name
+        if self.name is None:
+            self.name = "/root/deployment_%s.sh" % (os.urandom(4).encode('hex'))
+
+    def run(self, node, client):
+        """
+        Uploads the shell script and then executes it.
+
+        See also L{Deployment.run}
+        """
+        client.put(path=self.name, chmod=755, contents=self.script)
+        self.stdout, self.stderr, self.exit_status = client.run(self.name)
+        if self.delete:
+            client.delete(self.name)
+        return node
+
+class MultiStepDeployment(Deployment):
+    """
+    Runs a chain of Deployment steps.
+    """
+    def __init__(self, add = None):
+        """
+        @type add: C{list}
+        @keyword add: Deployment steps to add.
+        """
+        self.steps = []
+        self.add(add)
+
+    def add(self, add):
+        """Add a deployment to this chain.
+
+        @type add: Single L{Deployment} or a C{list} of L{Deployment}
+        @keyword add: Adds this deployment to the others already in this object.
+        """
+        if add is not None:
+            add = add if isinstance(add, (list, tuple)) else [add]
+            self.steps.extend(add)
+
+    def run(self, node, client):
+        """
+        Run each deployment that has been added.
+
+        See also L{Deployment.run}
+        """
+        for s in self.steps:
+            node = s.run(node, client)
+        return node

Added: incubator/libcloud/trunk/libcloud/compute/drivers/__init__.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/libcloud/compute/drivers/__init__.py?rev=1079029&view=auto
==============================================================================
--- incubator/libcloud/trunk/libcloud/compute/drivers/__init__.py (added)
+++ incubator/libcloud/trunk/libcloud/compute/drivers/__init__.py Mon Mar  7 23:44:06 2011
@@ -0,0 +1,38 @@
+# 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.
+
+"""
+Drivers for working with different providers
+"""
+
+__all__ = [
+    'brightbox',
+    'dummy',
+    'ec2',
+    'ecp',
+    'elastichosts',
+    'cloudsigma',
+    'gogrid',
+    'ibm_sbc',
+    'linode',
+    'opennebula',
+    'rackspace',
+    'rimuhosting',
+    'slicehost',
+    'softlayer',
+    'vcloud',
+    'voxel',
+    'vpsnet',
+]

Added: incubator/libcloud/trunk/libcloud/compute/drivers/brightbox.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/libcloud/compute/drivers/brightbox.py?rev=1079029&view=auto
==============================================================================
--- incubator/libcloud/trunk/libcloud/compute/drivers/brightbox.py (added)
+++ incubator/libcloud/trunk/libcloud/compute/drivers/brightbox.py Mon Mar  7 23:44:06 2011
@@ -0,0 +1,221 @@
+# 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.
+"""
+Brightbox Driver
+"""
+import httplib
+import base64
+
+from libcloud.common.base import ConnectionUserAndKey, Response
+from libcloud.compute.types import Provider, NodeState, InvalidCredsError
+from libcloud.compute.base import NodeDriver
+from libcloud.compute.base import Node, NodeImage, NodeSize, NodeLocation
+
+try:
+    import json
+except ImportError:
+    import simplejson as json
+
+API_VERSION = '1.0'
+
+
+class BrightboxResponse(Response):
+    def success(self):
+        return self.status >= 200 and self.status < 400
+
+    def parse_body(self):
+        if self.headers['content-type'].split('; ')[0] == 'application/json' and len(self.body) > 0:
+            return json.loads(self.body)
+        else:
+            return self.body
+
+    def parse_error(self):
+        return json.loads(self.body)['error']
+
+
+class BrightboxConnection(ConnectionUserAndKey):
+    """
+    Connection class for the Brightbox driver
+    """
+
+    host = 'api.gb1.brightbox.com'
+    responseCls = BrightboxResponse
+
+    def _fetch_oauth_token(self):
+        body = json.dumps({'client_id': self.user_id, 'grant_type': 'none'})
+
+        authorization = 'Basic ' + base64.encodestring('%s:%s' % (self.user_id, self.key)).rstrip()
+
+        self.connect()
+
+        response = self.connection.request(method='POST', url='/token', body=body, headers={
+            'Host': self.host,
+            'User-Agent': self._user_agent(),
+            'Authorization': authorization,
+            'Content-Type': 'application/json',
+            'Content-Length': str(len(body))
+        })
+
+        response = self.connection.getresponse()
+
+        if response.status == 200:
+            return json.loads(response.read())['access_token']
+        else:
+            message = '%s (%s)' % (json.loads(response.read())['error'], response.status)
+
+            raise InvalidCredsError, message
+
+    def add_default_headers(self, headers):
+        try:
+            headers['Authorization'] = 'OAuth ' + self.token
+        except AttributeError:
+            self.token = self._fetch_oauth_token()
+
+            headers['Authorization'] = 'OAuth ' + self.token
+
+        return headers
+
+    def encode_data(self, data):
+        return json.dumps(data)
+
+
+class BrightboxNodeDriver(NodeDriver):
+    """
+    Brightbox node driver
+    """
+
+    connectionCls = BrightboxConnection
+
+    type = Provider.BRIGHTBOX
+    name = 'Brightbox'
+
+    NODE_STATE_MAP = { 'creating': NodeState.PENDING,
+                       'active': NodeState.RUNNING,
+                       'inactive': NodeState.UNKNOWN,
+                       'deleting': NodeState.UNKNOWN,
+                       'deleted': NodeState.TERMINATED,
+                       'failed': NodeState.UNKNOWN }
+
+    def _to_node(self, data):
+        return Node(
+            id = data['id'],
+            name = data['name'],
+            state = self.NODE_STATE_MAP[data['status']],
+            public_ip = map(lambda cloud_ip: cloud_ip['public_ip'], data['cloud_ips']),
+            private_ip = map(lambda interface: interface['ipv4_address'], data['interfaces']),
+            driver = self.connection.driver,
+            extra = {
+                'status': data['status'],
+                'interfaces': data['interfaces']
+            }
+        )
+
+    def _to_image(self, data):
+        return NodeImage(
+            id = data['id'],
+            name = data['name'],
+            driver = self,
+            extra = {
+                'description': data['description'],
+                'arch': data['arch']
+            }
+        )
+
+    def _to_size(self, data):
+        return NodeSize(
+            id = data['id'],
+            name = data['name'],
+            ram = data['ram'],
+            disk = data['disk_size'],
+            bandwidth = 0,
+            price = '',
+            driver = self
+        )
+
+    def _to_location(self, data):
+        return NodeLocation(
+            id = data['id'],
+            name = data['handle'],
+            country = 'GB',
+            driver = self
+        )
+
+    def _post(self, path, data={}):
+        headers = {'Content-Type': 'application/json'}
+
+        return self.connection.request(path, data=data, headers=headers, method='POST')
+
+    def create_node(self, **kwargs):
+        data = {
+            'name': kwargs['name'],
+            'server_type': kwargs['size'].id,
+            'image': kwargs['image'].id,
+            'user_data': ''
+        }
+
+        if kwargs.has_key('location'):
+            data['zone'] = kwargs['location'].id
+        else:
+            data['zone'] = ''
+
+        data = self._post('/%s/servers' % API_VERSION, data).object
+
+        return self._to_node(data)
+
+    def destroy_node(self, node):
+        response = self.connection.request('/%s/servers/%s' % (API_VERSION, node.id), method='DELETE')
+
+        return response.status == httplib.ACCEPTED
+
+    def list_nodes(self):
+        data = self.connection.request('/%s/servers' % API_VERSION).object
+
+        return map(self._to_node, data)
+
+    def list_images(self):
+        data = self.connection.request('/%s/images' % API_VERSION).object
+
+        return map(self._to_image, data)
+
+    def list_sizes(self):
+        data = self.connection.request('/%s/server_types' % API_VERSION).object
+
+        return map(self._to_size, data)
+
+    def list_locations(self):
+        data = self.connection.request('/%s/zones' % API_VERSION).object
+
+        return map(self._to_location, data)
+
+    def ex_list_cloud_ips(self):
+        return self.connection.request('/%s/cloud_ips' % API_VERSION).object
+
+    def ex_create_cloud_ip(self):
+        return self._post('/%s/cloud_ips' % API_VERSION).object
+
+    def ex_map_cloud_ip(self, cloud_ip_id, interface_id):
+        response = self._post('/%s/cloud_ips/%s/map' % (API_VERSION, cloud_ip_id), {'interface': interface_id})
+
+        return response.status == httplib.ACCEPTED
+
+    def ex_unmap_cloud_ip(self, cloud_ip_id):
+        response = self._post('/%s/cloud_ips/%s/unmap' % (API_VERSION, cloud_ip_id))
+
+        return response.status == httplib.ACCEPTED
+
+    def ex_destroy_cloud_ip(self, cloud_ip_id):
+        response = self.connection.request('/%s/cloud_ips/%s' % (API_VERSION, cloud_ip_id), method='DELETE')
+
+        return response.status == httplib.OK

Added: incubator/libcloud/trunk/libcloud/compute/drivers/cloudsigma.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/libcloud/compute/drivers/cloudsigma.py?rev=1079029&view=auto
==============================================================================
--- incubator/libcloud/trunk/libcloud/compute/drivers/cloudsigma.py (added)
+++ incubator/libcloud/trunk/libcloud/compute/drivers/cloudsigma.py Mon Mar  7 23:44:06 2011
@@ -0,0 +1,654 @@
+# -*- coding: utf-8 -*-
+# 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.
+"""
+CloudSigma Driver
+"""
+import re
+import time
+import base64
+
+from libcloud.common.base import ConnectionUserAndKey, Response
+from libcloud.common.types import InvalidCredsError
+from libcloud.compute.types import NodeState, Provider
+from libcloud.compute.base import NodeDriver, NodeSize, Node
+from libcloud.compute.base import NodeImage
+
+# API end-points
+API_ENDPOINTS = {
+    'zrh': {
+        'name': 'Zurich',
+        'country': 'Switzerland',
+        'host': 'api.cloudsigma.com'
+    },
+}
+
+# Default API end-point for the base connection clase.
+DEFAULT_ENDPOINT = 'zrh'
+
+# CloudSigma doesn't specify special instance types.
+# Basically for CPU any value between 0.5 GHz and 20.0 GHz should work, 500 MB to 32000 MB for ram
+# and 1 GB to 1024 GB for hard drive size.
+# Plans in this file are based on examples listed on http://www.cloudsigma.com/en/pricing/price-schedules
+INSTANCE_TYPES = {
+    'micro-regular': {
+        'id': 'micro-regular',
+        'name': 'Micro/Regular instance',
+        'cpu': 1100,
+        'memory': 640,
+        'disk': 50,
+        'price': '0.0548',
+        'bandwidth': None,
+    },
+    'micro-high-cpu': {
+        'id': 'micro-high-cpu',
+        'name': 'Micro/High CPU instance',
+        'cpu': 2200,
+        'memory': 640,
+        'disk': 80,
+        'price': '.381',
+        'bandwidth': None,
+    },
+    'standard-small': {
+        'id': 'standard-small',
+        'name': 'Standard/Small instance',
+        'cpu': 1100,
+        'memory': 1741,
+        'disk': 50,
+        'price': '0.0796',
+        'bandwidth': None,
+    },
+    'standard-large': {
+        'id': 'standard-large',
+        'name': 'Standard/Large instance',
+        'cpu': 4400,
+        'memory': 7680,
+        'disk': 250,
+        'price': '0.381',
+        'bandwidth': None,
+    },
+    'standard-extra-large': {
+        'id': 'standard-extra-large',
+        'name': 'Standard/Extra Large instance',
+        'cpu': 8800,
+        'memory': 15360,
+        'disk': 500,
+        'price': '0.762',
+        'bandwidth': None,
+    },
+    'high-memory-extra-large': {
+        'id': 'high-memory-extra-large',
+        'name': 'High Memory/Extra Large instance',
+        'cpu': 7150,
+        'memory': 17510,
+        'disk': 250,
+        'price': '0.642',
+        'bandwidth': None,
+    },
+    'high-memory-double-extra-large': {
+        'id': 'high-memory-double-extra-large',
+        'name': 'High Memory/Double Extra Large instance',
+        'cpu': 14300,
+        'memory': 32768,
+        'disk': 500,
+        'price': '1.383',
+        'bandwidth': None,
+    },
+    'high-cpu-medium': {
+        'id': 'high-cpu-medium',
+        'name': 'High CPU/Medium instance',
+        'cpu': 5500,
+        'memory': 1741,
+        'disk': 150,
+        'price': '0.211',
+        'bandwidth': None,
+    },
+    'high-cpu-extra-large': {
+        'id': 'high-cpu-extra-large',
+        'name': 'High CPU/Extra Large instance',
+        'cpu': 20000,
+        'memory': 7168,
+        'disk': 500,
+        'price': '0.780',
+        'bandwidth': None,
+    },
+}
+
+NODE_STATE_MAP = {
+    'active': NodeState.RUNNING,
+    'stopped': NodeState.TERMINATED,
+    'dead': NodeState.TERMINATED,
+    'dumped': NodeState.TERMINATED,
+}
+
+# Default timeout (in seconds) for the drive imaging process
+IMAGING_TIMEOUT = 20 * 60
+
+class CloudSigmaException(Exception):
+    def __str__(self):
+        return self.args[0]
+
+    def __repr__(self):
+        return "<CloudSigmaException '%s'>" % (self.args[0])
+
+class CloudSigmaInsufficientFundsException(Exception):
+    def __repr__(self):
+        return "<CloudSigmaInsufficientFundsException '%s'>" % (self.args[0])
+
+class CloudSigmaResponse(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
+
+        return str2dicts(self.body)
+
+    def parse_error(self):
+        return 'Error: %s' % (self.body.replace('errors:', '').strip())
+
+class CloudSigmaNodeSize(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 CloudSigmaBaseConnection(ConnectionUserAndKey):
+    host = API_ENDPOINTS[DEFAULT_ENDPOINT]['host']
+    responseCls = CloudSigmaResponse
+
+    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 CloudSigmaBaseNodeDriver(NodeDriver):
+    type = Provider.CLOUDSIGMA
+    name = 'CloudSigma'
+    connectionCls = CloudSigmaBaseConnection
+
+    def reboot_node(self, node):
+        """
+        Reboot a node.
+
+        Because Cloudsigma API does not provide native reboot call, it's emulated using stop and start.
+        """
+        node = self._get_node(node.id)
+        state = node.state
+
+        if state == NodeState.RUNNING:
+            stopped = self.ex_stop_node(node)
+        else:
+            stopped = True
+
+        if not stopped:
+            raise CloudSigmaException('Could not stop node with id %s' % (node.id))
+
+        success = self.ex_start_node(node)
+
+        return success
+
+    def destroy_node(self, node):
+        """
+        Destroy a node (all the drives associated with it are NOT destroyed).
+
+        If a node is still running, it's stopped before it's destroyed.
+        """
+        node = self._get_node(node.id)
+        state = node.state
+
+        # Node cannot be destroyed while running so it must be stopped first
+        if state == NodeState.RUNNING:
+            stopped = self.ex_stop_node(node)
+        else:
+            stopped = True
+
+        if not stopped:
+            raise CloudSigmaException('Could not stop node with id %s' % (node.id))
+
+        response = self.connection.request(action = '/servers/%s/destroy' % (node.id),
+                                           method = 'POST')
+        return response.status == 204
+
+    def list_images(self, location=None):
+        """
+        Return a list of available standard images (this call might take up to 15 seconds to return).
+        """
+        response = self.connection.request(action = '/drives/standard/info').object
+
+        images = []
+        for value in response:
+            if value.get('type'):
+                if value['type'] == 'disk':
+                    image = NodeImage(id = value['drive'], name = value['name'], driver = self.connection.driver,
+                                    extra = {'size': value['size']})
+                    images.append(image)
+
+        return images
+
+    def list_sizes(self, location = None):
+        """
+        Return a list of available node sizes.
+        """
+        sizes = []
+        for key, value in INSTANCE_TYPES.iteritems():
+            size = CloudSigmaNodeSize(id = value['id'], name = value['name'], cpu = value['cpu'], ram = value['memory'],
+                            disk = value['disk'], bandwidth = value['bandwidth'], price = value['price'],
+                            driver = self.connection.driver)
+            sizes.append(size)
+
+        return sizes
+
+    def list_nodes(self):
+        """
+        Return a list of nodes.
+        """
+        response = self.connection.request(action = '/servers/info').object
+
+        nodes = []
+        for data in response:
+            node = self._to_node(data)
+            if node:
+                nodes.append(node)
+        return nodes
+
+    def create_node(self, **kwargs):
+        """
+        Creates a CloudSigma instance
+
+        See L{NodeDriver.create_node} for more keyword args.
+
+        @keyword    name: String with a name for this new node (required)
+        @type       name: C{string}
+
+        @keyword    smp: Number of virtual processors or None to calculate based on the cpu speed
+        @type       smp: C{int}
+
+        @keyword    nic_model: e1000, rtl8139 or virtio (is not specified, e1000 is used)
+        @type       nic_model: C{string}
+
+        @keyword    vnc_password: If not set, VNC access is disabled.
+        @type       vnc_password: C{bool}
+        """
+        size = kwargs['size']
+        image = kwargs['image']
+        smp = kwargs.get('smp', 'auto')
+        nic_model = kwargs.get('nic_model', 'e1000')
+        vnc_password = kwargs.get('vnc_password', None)
+
+        if nic_model not in ['e1000', 'rtl8139', 'virtio']:
+            raise CloudSigmaException('Invalid NIC model specified')
+
+        drive_data = {}
+        drive_data.update({'name': kwargs['name'], 'size': '%sG' % (kwargs['size'].disk)})
+
+        response = self.connection.request(action = '/drives/%s/clone' % image.id, data = dict2str(drive_data),
+                                           method = 'POST').object
+
+        if not response:
+            raise CloudSigmaException('Drive creation failed')
+
+        drive_uuid = response[0]['drive']
+
+        response = self.connection.request(action = '/drives/%s/info' % (drive_uuid)).object
+        imaging_start = time.time()
+        while response[0].has_key('imaging'):
+            response = self.connection.request(action = '/drives/%s/info' % (drive_uuid)).object
+            elapsed_time = time.time() - imaging_start
+            if response[0].has_key('imaging') and elapsed_time >= IMAGING_TIMEOUT:
+                raise CloudSigmaException('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 = dict2str(node_data),
+                                           method = 'POST').object
+
+        if not isinstance(response, list):
+            response = [ response ]
+
+        node = self._to_node(response[0])
+        if node is None:
+            # Insufficient funds, destroy created drive
+            self.ex_drive_destroy(drive_uuid)
+            raise CloudSigmaInsufficientFundsException('Insufficient funds, node creation failed')
+
+        # Start the node after it has been created
+        started = self.ex_start_node(node)
+
+        if started:
+            node.state = NodeState.RUNNING
+
+        return node
+
+    def ex_destroy_node_and_drives(self, node):
+        """
+        Destroy a node and all the drives associated with it.
+        """
+        node = self._get_node_info(node)
+
+        drive_uuids = []
+        for key, value in node.iteritems():
+            if (key.startswith('ide:') or key.startswith('scsi') or key.startswith('block')) and \
+               not (key.endswith(':bytes') or key.endswith(':requests') or key.endswith('media')):
+                drive_uuids.append(value)
+
+        node_destroyed = self.destroy_node(self._to_node(node))
+
+        if not node_destroyed:
+            return False
+
+        for drive_uuid in drive_uuids:
+            self.ex_drive_destroy(drive_uuid)
+
+        return True
+
+    def ex_static_ip_list(self):
+        """
+        Return a list of available static IP addresses.
+        """
+        response = self.connection.request(action = '/resources/ip/list', method = 'GET')
+
+        if response.status != 200:
+            raise CloudSigmaException('Could not retrieve IP list')
+
+        ips = str2list(response.body)
+        return ips
+
+    def ex_drives_list(self):
+        """
+        Return a list of all the available drives.
+        """
+        response = self.connection.request(action = '/drives/info', method = 'GET')
+
+        result = str2dicts(response.body)
+        return result
+
+    def ex_static_ip_create(self):
+        """
+        Create a new static IP address.
+        """
+        response = self.connection.request(action = '/resources/ip/create', method = 'GET')
+
+        result = str2dicts(response.body)
+        return result
+
+    def ex_static_ip_destroy(self, ip_address):
+        """
+        Destroy a static IP address.
+        """
+        response = self.connection.request(action = '/resources/ip/%s/destroy' % (ip_address), method = 'GET')
+
+        return response.status == 204
+
+    def ex_drive_destroy(self, drive_uuid):
+        """
+        Destroy a drive with a specified uuid.
+        If the drive is currently mounted an exception is thrown.
+        """
+        response = self.connection.request(action = '/drives/%s/destroy' % (drive_uuid), method = 'POST')
+
+        return response.status == 204
+
+
+    def ex_set_node_configuration(self, node, **kwargs):
+        """
+        Update a node configuration.
+        Changing most of the parameters requires node to be stopped.
+        """
+        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 CloudSigmaException('Invalid configuration key specified: %s' % (',' .join(invalid_keys)))
+
+        response = self.connection.request(action = '/servers/%s/set' % (node.id), data = dict2str(kwargs),
+                                           method = 'POST')
+
+        return (response.status == 200 and response.body != '')
+
+    def ex_start_node(self, node):
+        """
+        Start a node.
+        """
+        response = self.connection.request(action = '/servers/%s/start' % (node.id),
+                                           method = 'POST')
+
+        return response.status == 200
+
+    def ex_stop_node(self, node):
+        """
+        Stop (shutdown) a node.
+        """
+        response = self.connection.request(action = '/servers/%s/stop' % (node.id),
+                                           method = 'POST')
+        return response.status == 204
+
+    def ex_shutdown_node(self, node):
+        """
+        Stop (shutdown) a node.
+        """
+        return self.ex_stop_node(node)
+
+    def ex_destroy_drive(self, drive_uuid):
+        """
+        Destroy a drive.
+        """
+        response = self.connection.request(action = '/drives/%s/destroy' % (drive_uuid),
+                                           method = 'POST')
+        return response.status == 204
+
+    def _to_node(self, data):
+        if data:
+            try:
+                state = NODE_STATE_MAP[data['status']]
+            except KeyError:
+                state = NodeState.UNKNOWN
+
+            if 'server' not in data:
+                # Response does not contain server UUID if the server
+                # creation failed because of insufficient funds.
+                return None
+
+            public_ip = []
+            if data.has_key('nic:0:dhcp'):
+                if isinstance(data['nic:0:dhcp'], list):
+                    public_ip = data['nic:0:dhcp']
+                else:
+                    public_ip = [data['nic:0:dhcp']]
+
+            extra = {}
+            extra_keys = [ ('cpu', 'int'), ('smp', 'auto'), ('mem', 'int'), ('status', 'str') ]
+            for key, value_type in extra_keys:
+                if data.has_key(key):
+                    value = data[key]
+
+                    if value_type == 'int':
+                        value = int(value)
+                    elif value_type == 'auto':
+                        try:
+                            value = int(value)
+                        except ValueError:
+                            pass
+
+                    extra.update({key: value})
+
+            if data.has_key('vnc:ip') and data.has_key('vnc:password'):
+                extra.update({'vnc_ip': data['vnc:ip'], 'vnc_password': data['vnc:password']})
+
+            node = Node(id = data['server'], name = data['name'], state =  state,
+                        public_ip = public_ip, private_ip = None, driver = self.connection.driver,
+                        extra = extra)
+
+            return node
+        return None
+
+    def _get_node(self, node_id):
+        nodes = self.list_nodes()
+        node = [node for node in nodes if node.id == node.id]
+
+        if not node:
+            raise CloudSigmaException('Node with id %s does not exist' % (node_id))
+
+        return node[0]
+
+    def _get_node_info(self, node):
+        response = self.connection.request(action = '/servers/%s/info' % (node.id))
+
+        result = str2dicts(response.body)
+        return result[0]
+
+class CloudSigmaZrhConnection(CloudSigmaBaseConnection):
+    """
+    Connection class for the CloudSigma driver for the Zurich end-point
+    """
+    host = API_ENDPOINTS[DEFAULT_ENDPOINT]['host']
+
+class CloudSigmaZrhNodeDriver(CloudSigmaBaseNodeDriver):
+    """
+    CloudSigma node driver for the Zurich end-point
+    """
+    connectionCls = CloudSigmaZrhConnection
+
+# Utility methods (should we place them in libcloud/utils.py ?)
+def str2dicts(data):
+    """
+    Create a list of dictionaries from a whitespace and newline delimited text.
+
+    For example, this:
+    cpu 1100
+    ram 640
+
+    cpu 2200
+    ram 1024
+
+    becomes:
+    [{'cpu': '1100', 'ram': '640'}, {'cpu': '2200', 'ram': '1024'}]
+    """
+    list_data = []
+    list_data.append({})
+    d = list_data[-1]
+
+    lines = data.split('\n')
+    for line in lines:
+        line = line.strip()
+
+        if not line:
+            d = {}
+            list_data.append(d)
+            d = list_data[-1]
+            continue
+
+        whitespace = line.find(' ')
+
+        if not whitespace:
+            continue
+
+        key = line[0:whitespace]
+        value = line[whitespace + 1:]
+        d.update({key: value})
+
+    list_data = [value for value in list_data if value != {}]
+    return list_data
+
+def str2list(data):
+    """
+    Create a list of values from a whitespace and newline delimited text (keys are ignored).
+
+    For example, this:
+    ip 1.2.3.4
+    ip 1.2.3.5
+    ip 1.2.3.6
+
+    becomes:
+    ['1.2.3.4', '1.2.3.5', '1.2.3.6']
+    """
+    list_data = []
+
+    for line in data.split('\n'):
+        line = line.strip()
+
+        if not line:
+            continue
+
+        try:
+            splitted = line.split(' ')
+            # key = splitted[0]
+            value = splitted[1]
+        except Exception:
+            continue
+
+        list_data.append(value)
+
+    return list_data
+
+def dict2str(data):
+    """
+    Create a string with a whitespace and newline delimited text from a dictionary.
+
+    For example, this:
+    {'cpu': '1100', 'ram': '640', 'smp': 'auto'}
+
+    becomes:
+    cpu 1100
+    ram 640
+    smp auto
+
+    cpu 2200
+    ram 1024
+    """
+    result = ''
+    for k in data:
+        if data[k] != None:
+            result += '%s %s\n' % (str(k), str(data[k]))
+        else:
+            result += '%s\n' % str(k)
+
+    return result

Added: incubator/libcloud/trunk/libcloud/compute/drivers/dreamhost.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/libcloud/compute/drivers/dreamhost.py?rev=1079029&view=auto
==============================================================================
--- incubator/libcloud/trunk/libcloud/compute/drivers/dreamhost.py (added)
+++ incubator/libcloud/trunk/libcloud/compute/drivers/dreamhost.py Mon Mar  7 23:44:06 2011
@@ -0,0 +1,243 @@
+# 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.
+"""
+DreamHost Driver
+"""
+
+try:
+    import json
+except:
+    import simplejson as json
+
+from libcloud.common.base import ConnectionKey, Response
+from libcloud.common.types import InvalidCredsError
+from libcloud.compute.base import Node, NodeDriver, NodeLocation, NodeSize
+from libcloud.compute.base import NodeImage
+from libcloud.compute.types import Provider, NodeState
+
+"""
+DreamHost Private Servers can be resized on the fly, but Libcloud doesn't
+currently support extensions to its interface, so we'll put some basic sizes
+in for node creation.
+"""
+DH_PS_SIZES = {
+    'minimum': {
+        'id' : 'minimum',
+        'name' : 'Minimum DH PS size',
+        'ram' : 300,
+        'price' : 15,
+        'disk' : None,
+        'bandwidth' : None
+    },
+    'maximum': {
+        'id' : 'maximum',
+        'name' : 'Maximum DH PS size',
+        'ram' : 4000,
+        'price' : 200,
+        'disk' : None,
+        'bandwidth' : None
+    },
+    'default': {
+        'id' : 'default',
+        'name' : 'Default DH PS size',
+        'ram' : 2300,
+        'price' : 115,
+        'disk' : None,
+        'bandwidth' : None
+    },
+    'low': {
+        'id' : 'low',
+        'name' : 'DH PS with 1GB RAM',
+        'ram' : 1000,
+        'price' : 50,
+        'disk' : None,
+        'bandwidth' : None
+    },
+    'high': {
+        'id' : 'high',
+        'name' : 'DH PS with 3GB RAM',
+        'ram' : 3000,
+        'price' : 150,
+        'disk' : None,
+        'bandwidth' : None
+    },
+}
+
+
+class DreamhostAPIException(Exception):
+
+    def __str__(self):
+        return self.args[0]
+
+    def __repr__(self):
+        return "<DreamhostException '%s'>" % (self.args[0])
+
+
+class DreamhostResponse(Response):
+    """
+    Response class for DreamHost PS
+    """
+
+    def parse_body(self):
+        resp = json.loads(self.body)
+        if resp['result'] != 'success':
+            raise Exception(self._api_parse_error(resp))
+        return resp['data']
+
+    def parse_error(self):
+        raise Exception
+
+    def _api_parse_error(self, response):
+        if 'data' in response:
+            if response['data'] == 'invalid_api_key':
+                raise InvalidCredsError("Oops!  You've entered an invalid API key")
+            else:
+                raise DreamhostAPIException(response['data'])
+        else:
+            raise DreamhostAPIException("Unknown problem: %s" % (self.body))
+
+class DreamhostConnection(ConnectionKey):
+    """
+    Connection class to connect to DreamHost's API servers
+    """
+
+    host = 'api.dreamhost.com'
+    responseCls = DreamhostResponse
+    format = 'json'
+
+    def add_default_params(self, params):
+        """
+        Add key and format parameters to the request.  Eventually should add
+        unique_id to prevent re-execution of a single request.
+        """
+        params['key'] = self.key
+        params['format'] = self.format
+        #params['unique_id'] = generate_unique_id()
+        return params
+
+
+class DreamhostNodeDriver(NodeDriver):
+    """
+    Node Driver for DreamHost PS
+    """
+    type = Provider.DREAMHOST
+    name = "Dreamhost"
+    connectionCls = DreamhostConnection
+    _sizes = DH_PS_SIZES
+
+    def create_node(self, **kwargs):
+        """Create a new Dreamhost node
+
+        See L{NodeDriver.create_node} for more keyword args.
+
+        @keyword    ex_movedata: Copy all your existing users to this new PS
+        @type       ex_movedata: C{str}
+        """
+        size = kwargs['size'].ram
+        params = {
+            'cmd' : 'dreamhost_ps-add_ps',
+            'movedata' : kwargs.get('movedata', 'no'),
+            'type' : kwargs['image'].name,
+            'size' : size
+        }
+        data = self.connection.request('/', params).object
+        return Node(
+            id = data['added_web'],
+            name = data['added_web'],
+            state = NodeState.PENDING,
+            public_ip = [],
+            private_ip = [],
+            driver = self.connection.driver,
+            extra = {
+                'type' : kwargs['image'].name
+            }
+        )
+
+    def destroy_node(self, node):
+        params = {
+            'cmd' : 'dreamhost_ps-remove_ps',
+            'ps' : node.id
+        }
+        try:
+            return self.connection.request('/', params).success()
+        except DreamhostAPIException:
+            return False
+
+    def reboot_node(self, node):
+        params = {
+            'cmd' : 'dreamhost_ps-reboot',
+            'ps' : node.id
+        }
+        try:
+            return self.connection.request('/', params).success()
+        except DreamhostAPIException:
+            return False
+
+    def list_nodes(self, **kwargs):
+        data = self.connection.request('/', {'cmd': 'dreamhost_ps-list_ps'}).object
+        return [self._to_node(n) for n in data]
+
+    def list_images(self, **kwargs):
+        data = self.connection.request('/', {'cmd': 'dreamhost_ps-list_images'}).object
+        images = []
+        for img in data:
+            images.append(NodeImage(
+                id = img['image'],
+                name = img['image'],
+                driver = self.connection.driver
+            ))
+        return images
+
+    def list_sizes(self, **kwargs):
+        return [ NodeSize(driver=self.connection.driver, **i)
+            for i in self._sizes.values() ]
+
+    def list_locations(self, **kwargs):
+        raise NotImplementedError('You cannot select a location for DreamHost Private Servers at this time.')
+
+    ############################################
+    # Private Methods (helpers and extensions) #
+    ############################################
+    def _resize_node(self, node, size):
+        if (size < 300 or size > 4000):
+            return False
+
+        params = {
+            'cmd' : 'dreamhost_ps-set_size',
+            'ps' : node.id,
+            'size' : size
+        }
+        try:
+            return self.connection.request('/', params).success()
+        except DreamhostAPIException:
+            return False
+
+    def _to_node(self, data):
+        """
+        Convert the data from a DreamhostResponse object into a Node
+        """
+        return Node(
+            id = data['ps'],
+            name = data['ps'],
+            state = NodeState.UNKNOWN,
+            public_ip = [data['ip']],
+            private_ip = [],
+            driver = self.connection.driver,
+            extra = {
+                'current_size' : data['memory_mb'],
+                'account_id' : data['account_id'],
+                'type' : data['type']
+            }
+        )

Added: incubator/libcloud/trunk/libcloud/compute/drivers/dummy.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/libcloud/compute/drivers/dummy.py?rev=1079029&view=auto
==============================================================================
--- incubator/libcloud/trunk/libcloud/compute/drivers/dummy.py (added)
+++ incubator/libcloud/trunk/libcloud/compute/drivers/dummy.py Mon Mar  7 23:44:06 2011
@@ -0,0 +1,297 @@
+# 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.
+"""
+Dummy Driver
+
+@note: This driver is out of date
+"""
+import uuid
+import socket
+import struct
+
+from libcloud.base import ConnectionKey, NodeDriver, NodeSize, NodeLocation
+from libcloud.compute.base import NodeImage, Node
+from libcloud.compute.types import Provider,NodeState
+
+class DummyConnection(ConnectionKey):
+    """
+    Dummy connection class
+    """
+
+    def connect(self, host=None, port=None):
+        pass
+
+class DummyNodeDriver(NodeDriver):
+    """
+    Dummy node driver
+
+    This is a fake driver which appears to always create or destroy
+    nodes successfully.
+
+    >>> from libcloud.compute.drivers.dummy import DummyNodeDriver
+    >>> driver = DummyNodeDriver(0)
+    >>> node=driver.create_node()
+    >>> node.public_ip[0]
+    '127.0.0.3'
+    >>> node.name
+    'dummy-3'
+
+    If the credentials you give convert to an integer then the next
+    node to be created will be one higher.
+
+    Each time you create a node you will get a different IP address.
+
+    >>> driver = DummyNodeDriver(22)
+    >>> node=driver.create_node()
+    >>> node.name
+    'dummy-23'
+
+    """
+
+    name = "Dummy Node Provider"
+    type = Provider.DUMMY
+
+    def __init__(self, creds):
+        self.creds = creds
+        try:
+          num = int(creds)
+        except ValueError:
+          num = None
+        if num:
+          self.nl = []
+          startip = _ip_to_int('127.0.0.1')
+          for i in xrange(num):
+            ip = _int_to_ip(startip + i)
+            self.nl.append(
+              Node(id=i,
+                   name='dummy-%d' % (i),
+                   state=NodeState.RUNNING,
+                   public_ip=[ip],
+                   private_ip=[],
+                   driver=self,
+                   extra={'foo': 'bar'})
+            )
+        else:
+          self.nl = [
+              Node(id=1,
+                   name='dummy-1',
+                   state=NodeState.RUNNING,
+                   public_ip=['127.0.0.1'],
+                   private_ip=[],
+                   driver=self,
+                   extra={'foo': 'bar'}),
+              Node(id=2,
+                   name='dummy-2',
+                   state=NodeState.RUNNING,
+                   public_ip=['127.0.0.1'],
+                   private_ip=[],
+                   driver=self,
+                   extra={'foo': 'bar'}),
+          ]
+        self.connection = DummyConnection(self.creds)
+
+    def get_uuid(self, unique_field=None):
+        return str(uuid.uuid4())
+
+    def list_nodes(self):
+        """
+        List the nodes known to a particular driver;
+        There are two default nodes created at the beginning
+
+        >>> from libcloud.compute.drivers.dummy import DummyNodeDriver
+        >>> driver = DummyNodeDriver(0)
+        >>> node_list=driver.list_nodes()
+        >>> sorted([node.name for node in node_list ])
+        ['dummy-1', 'dummy-2']
+
+        each item in the list returned is a node object from which you
+        can carry out any node actions you wish
+
+        >>> node_list[0].reboot()
+        True
+
+        As more nodes are added, list_nodes will return them
+
+        >>> node=driver.create_node()
+        >>> sorted([node.name for node in driver.list_nodes()])
+        ['dummy-1', 'dummy-2', 'dummy-3']
+        """
+        return self.nl
+
+    def reboot_node(self, node):
+        """
+        Sets the node state to rebooting; in this dummy driver always
+        returns True as if the reboot had been successful.
+
+        >>> from libcloud.compute.drivers.dummy import DummyNodeDriver
+        >>> driver = DummyNodeDriver(0)
+        >>> node=driver.create_node()
+        >>> from libcloud.compute.types import NodeState
+        >>> node.state == NodeState.RUNNING
+        True
+        >>> node.state == NodeState.REBOOTING
+        False
+        >>> driver.reboot_node(node)
+        True
+        >>> node.state == NodeState.REBOOTING
+        True
+
+        Please note, dummy nodes never recover from the reboot.
+        """
+
+        node.state = NodeState.REBOOTING
+        return True
+
+    def destroy_node(self, node):
+        """
+        Sets the node state to terminated and removes it from the node list
+
+        >>> from libcloud.compute.drivers.dummy import DummyNodeDriver
+        >>> driver = DummyNodeDriver(0)
+        >>> from libcloud.compute.types import NodeState
+        >>> node = [node for node in driver.list_nodes() if node.name == 'dummy-1'][0]
+        >>> node.state == NodeState.RUNNING
+        True
+        >>> driver.destroy_node(node)
+        True
+        >>> node.state == NodeState.RUNNING
+        False
+        >>> [node for node in driver.list_nodes() if node.name == 'dummy-1']
+        []
+        """
+
+        node.state = NodeState.TERMINATED
+        self.nl.remove(node)
+        return True
+
+    def list_images(self, location=None):
+        """
+        Returns a list of images as a cloud provider might have
+
+        >>> from libcloud.compute.drivers.dummy import DummyNodeDriver
+        >>> driver = DummyNodeDriver(0)
+        >>> sorted([image.name for image in driver.list_images()])
+        ['Slackware 4', 'Ubuntu 9.04', 'Ubuntu 9.10']
+        """
+        return [
+            NodeImage(id=1, name="Ubuntu 9.10", driver=self),
+            NodeImage(id=2, name="Ubuntu 9.04", driver=self),
+            NodeImage(id=3, name="Slackware 4", driver=self),
+        ]
+
+    def list_sizes(self, location=None):
+        """
+        Returns a list of node sizes as a cloud provider might have
+
+        >>> from libcloud.compute.drivers.dummy import DummyNodeDriver
+        >>> driver = DummyNodeDriver(0)
+        >>> sorted([size.ram for size in driver.list_sizes()])
+        [128, 512, 4096, 8192]
+        """
+
+        return [
+            NodeSize(id=1,
+                     name="Small",
+                     ram=128,
+                     disk=4,
+                     bandwidth=500,
+                     price=4,
+                     driver=self),
+            NodeSize(id=2,
+                     name="Medium",
+                     ram=512,
+                     disk=16,
+                     bandwidth=1500,
+                     price=8,
+                     driver=self),
+            NodeSize(id=3,
+                     name="Big",
+                     ram=4096,
+                     disk=32,
+                     bandwidth=2500,
+                     price=32,
+                     driver=self),
+            NodeSize(id=4,
+                     name="XXL Big",
+                     ram=4096*2,
+                     disk=32*4,
+                     bandwidth=2500*3,
+                     price=32*2,
+                     driver=self),
+        ]
+
+    def list_locations(self):
+        """
+        Returns a list of locations of nodes
+
+        >>> from libcloud.compute.drivers.dummy import DummyNodeDriver
+        >>> driver = DummyNodeDriver(0)
+        >>> sorted([loc.name + " in " + loc.country for loc in driver.list_locations()])
+        ['Island Datacenter in FJ', 'London Loft in GB', "Paul's Room in US"]
+        """
+        return [
+            NodeLocation(id=1,
+                         name="Paul's Room",
+                         country='US',
+                         driver=self),
+            NodeLocation(id=2,
+                         name="London Loft",
+                         country='GB',
+                         driver=self),
+            NodeLocation(id=3,
+                         name="Island Datacenter",
+                         country='FJ',
+                         driver=self),
+        ]
+
+    def create_node(self, **kwargs):
+        """
+        Creates a dummy node; the node id is equal to the number of
+        nodes in the node list
+
+        >>> from libcloud.compute.drivers.dummy import DummyNodeDriver
+        >>> driver = DummyNodeDriver(0)
+        >>> sorted([node.name for node in driver.list_nodes()])
+        ['dummy-1', 'dummy-2']
+        >>> nodeA = driver.create_node()
+        >>> sorted([node.name for node in driver.list_nodes()])
+        ['dummy-1', 'dummy-2', 'dummy-3']
+        >>> driver.create_node().name
+        'dummy-4'
+        >>> driver.destroy_node(nodeA)
+        True
+        >>> sorted([node.name for node in driver.list_nodes()])
+        ['dummy-1', 'dummy-2', 'dummy-4']
+        """
+        l = len(self.nl) + 1
+        n = Node(id=l,
+                 name='dummy-%d' % l,
+                 state=NodeState.RUNNING,
+                 public_ip=['127.0.0.%d' % l],
+                 private_ip=[],
+                 driver=self,
+                 extra={'foo': 'bar'})
+        self.nl.append(n)
+        return n
+
+def _ip_to_int(ip):
+    return socket.htonl(struct.unpack('I', socket.inet_aton(ip))[0])
+
+def _int_to_ip(ip):
+    return socket.inet_ntoa(struct.pack('I', socket.ntohl(ip)))
+
+if __name__ == "__main__":
+    import doctest
+    doctest.testmod()