libcloud-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From to...@apache.org
Subject svn commit: r1079029 [4/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/ ...
Date Mon, 07 Mar 2011 23:44:12 GMT
Added: incubator/libcloud/trunk/libcloud/compute/drivers/ibm_sbc.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/libcloud/compute/drivers/ibm_sbc.py?rev=1079029&view=auto
==============================================================================
--- incubator/libcloud/trunk/libcloud/compute/drivers/ibm_sbc.py (added)
+++ incubator/libcloud/trunk/libcloud/compute/drivers/ibm_sbc.py Mon Mar  7 23:44:06 2011
@@ -0,0 +1,191 @@
+# 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.
+"""
+Driver for the IBM Developer Cloud.
+"""
+import base64, urllib
+
+from libcloud.common.base import Response, ConnectionUserAndKey
+from libcloud.common.types import InvalidCredsError
+from libcloud.compute.types import NodeState, Provider
+from libcloud.compute.base import NodeDriver, Node, NodeImage, NodeSize, NodeLocation, NodeAuthSSHKey
+
+from xml.etree import ElementTree as ET
+
+HOST = 'www-147.ibm.com'
+REST_BASE = '/computecloud/enterprise/api/rest/20100331'
+
+class IBMResponse(Response):
+    def success(self):
+        return int(self.status) == 200
+
+    def parse_body(self):
+        if not self.body:
+            return None
+        return ET.XML(self.body)
+
+    def parse_error(self):
+        if int(self.status) == 401:
+            if not self.body:
+                raise InvalidCredsError(str(self.status) + ': ' + self.error)
+            else:
+                raise InvalidCredsError(self.body)
+        return self.body
+
+class IBMConnection(ConnectionUserAndKey):
+    """
+    Connection class for the IBM Developer Cloud driver
+    """
+
+    host = HOST
+    responseCls = IBMResponse
+
+    def add_default_headers(self, headers):
+        headers['Accept'] = 'text/xml'
+        headers['Authorization'] = ('Basic %s' % (base64.b64encode('%s:%s' % (self.user_id, self.key))))
+        if not 'Content-Type' in headers:
+            headers['Content-Type'] = 'text/xml'
+        return headers
+
+    def encode_data(self, data):
+        return urllib.urlencode(data)
+
+class IBMNodeDriver(NodeDriver):
+    """
+    IBM Developer Cloud node driver.
+    """
+    connectionCls = IBMConnection
+    type = Provider.IBM
+    name = "IBM Developer Cloud"
+
+    NODE_STATE_MAP = { 0: NodeState.PENDING,    # New
+                       1: NodeState.PENDING,    # Provisioning
+                       2: NodeState.TERMINATED, # Failed
+                       3: NodeState.TERMINATED, # Removed
+                       4: NodeState.TERMINATED, # Rejected
+                       5: NodeState.RUNNING,    # Active
+                       6: NodeState.UNKNOWN,    # Unknown
+                       7: NodeState.PENDING,    # Deprovisioning
+                       8: NodeState.REBOOTING,  # Restarting
+                       9: NodeState.PENDING,    # Starting
+                       10: NodeState.PENDING,   # Stopping
+                       11: NodeState.TERMINATED,# Stopped
+                       12: NodeState.PENDING,   # Deprovision Pending
+                       13: NodeState.PENDING,   # Restart Pending
+                       14: NodeState.PENDING,   # Attaching
+                       15: NodeState.PENDING }  # Detaching
+
+    def create_node(self, **kwargs):
+        """
+        Creates a node in the IBM Developer Cloud.
+
+        See L{NodeDriver.create_node} for more keyword args.
+
+        @keyword    ex_configurationData: Image-specific configuration parameters.
+                                       Configuration parameters are defined in
+                                       the parameters.xml file.  The URL to
+                                       this file is defined in the NodeImage
+                                       at extra[parametersURL].
+        @type       ex_configurationData: C{dict}
+        """
+
+        # Compose headers for message body
+        data = {}
+        data.update({'name': kwargs['name']})
+        data.update({'imageID': kwargs['image'].id})
+        data.update({'instanceType': kwargs['size'].id})
+        if 'location' in kwargs:
+            data.update({'location': kwargs['location'].id})
+        else:
+            data.update({'location': '1'})
+        if 'auth' in kwargs and isinstance(kwargs['auth'], NodeAuthSSHKey):
+            data.update({'publicKey': kwargs['auth'].pubkey})
+        if 'ex_configurationData' in kwargs:
+            configurationData = kwargs['ex_configurationData']
+            for key in configurationData.keys():
+                data.update({key: configurationData.get(key)})
+
+        # Send request!
+        resp = self.connection.request(action = REST_BASE + '/instances',
+                                       headers = {'Content-Type': 'application/x-www-form-urlencoded'},
+                                       method = 'POST',
+                                       data = data).object
+        return self._to_nodes(resp)[0]
+
+    def destroy_node(self, node):
+        url = REST_BASE + '/instances/%s' % (node.id)
+        status = int(self.connection.request(action = url, method='DELETE').status)
+        return status == 200
+
+    def reboot_node(self, node):
+        url = REST_BASE + '/instances/%s' % (node.id)
+        headers = {'Content-Type': 'application/x-www-form-urlencoded'}
+        data = {'state': 'restart'}
+
+        resp = self.connection.request(action = url,
+                                       method = 'PUT',
+                                       headers = headers,
+                                       data = data)
+        return int(resp.status) == 200
+
+    def list_nodes(self):
+        return self._to_nodes(self.connection.request(REST_BASE + '/instances').object)
+
+    def list_images(self, location = None):
+        return self._to_images(self.connection.request(REST_BASE + '/offerings/image').object)
+
+    def list_sizes(self, location = None):        
+        return [ NodeSize('BRZ32.1/2048/60*175', 'Bronze 32 bit', None, None, None, None, self.connection.driver),
+                 NodeSize('BRZ64.2/4096/60*500*350', 'Bronze 64 bit', None, None, None, None, self.connection.driver),
+                 NodeSize('COP32.1/2048/60', 'Copper 32 bit', None, None, None, None, self.connection.driver),
+                 NodeSize('COP64.2/4096/60', 'Copper 64 bit', None, None, None, None, self.connection.driver),
+                 NodeSize('SLV32.2/4096/60*350', 'Silver 32 bit', None, None, None, None, self.connection.driver),
+                 NodeSize('SLV64.4/8192/60*500*500', 'Silver 64 bit', None, None, None, None, self.connection.driver),
+                 NodeSize('GLD32.4/4096/60*350', 'Gold 32 bit', None, None, None, None, self.connection.driver),
+                 NodeSize('GLD64.8/16384/60*500*500', 'Gold 64 bit', None, None, None, None, self.connection.driver),
+                 NodeSize('PLT64.16/16384/60*500*500*500*500', 'Platinum 64 bit', None, None, None, None, self.connection.driver) ]
+
+    def list_locations(self):
+        return self._to_locations(self.connection.request(REST_BASE + '/locations').object)
+
+    def _to_nodes(self, object):
+        return [ self._to_node(instance) for instance in object.findall('Instance') ]
+
+    def _to_node(self, instance):
+        return Node(id = instance.findtext('ID'),
+                    name = instance.findtext('Name'),
+                    state = self.NODE_STATE_MAP[int(instance.findtext('Status'))],
+                    public_ip = instance.findtext('IP'),
+                    private_ip = None,
+                    driver = self.connection.driver)
+
+    def _to_images(self, object):
+        return [ self._to_image(image) for image in object.findall('Image') ]
+
+    def _to_image(self, image):
+        return NodeImage(id = image.findtext('ID'),
+                         name = image.findtext('Name'),
+                         driver = self.connection.driver,
+                         extra = {'parametersURL': image.findtext('Manifest')})
+
+    def _to_locations(self, object):
+        return [ self._to_location(location) for location in object.findall('Location') ]
+
+    def _to_location(self, location):
+        # NOTE: country currently hardcoded
+        return NodeLocation(id = location.findtext('ID'),
+                            name = location.findtext('Name'),
+                            country = 'US',
+                            driver = self.connection.driver)

Added: incubator/libcloud/trunk/libcloud/compute/drivers/linode.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/libcloud/compute/drivers/linode.py?rev=1079029&view=auto
==============================================================================
--- incubator/libcloud/trunk/libcloud/compute/drivers/linode.py (added)
+++ incubator/libcloud/trunk/libcloud/compute/drivers/linode.py Mon Mar  7 23:44:06 2011
@@ -0,0 +1,618 @@
+# 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.
+
+"""libcloud driver for the Linode(R) API
+
+This driver implements all libcloud functionality for the Linode API.  Since the
+API is a bit more fine-grained, create_node abstracts a significant amount of
+work (and may take a while to run).
+
+Linode home page                    http://www.linode.com/
+Linode API documentation            http://www.linode.com/api/
+Alternate bindings for reference    http://github.com/tjfontaine/linode-python
+
+Linode(R) is a registered trademark of Linode, LLC.
+
+"""
+import itertools
+import os
+
+from copy import copy
+
+try:
+    import json
+except:
+    import simplejson as json
+
+from libcloud.common.base import ConnectionKey, Response
+from libcloud.common.types import InvalidCredsError, MalformedResponseError
+from libcloud.compute.types import Provider, NodeState
+from libcloud.compute.base import NodeDriver, NodeSize, Node, NodeLocation
+from libcloud.compute.base import NodeAuthPassword, NodeAuthSSHKey
+from libcloud.compute.base import NodeImage
+
+# Where requests go - in beta situations, this information may change.
+LINODE_API = "api.linode.com"
+LINODE_ROOT = "/"
+
+# Map of TOTALRAM to PLANID, allows us to figure out what plan
+# a particular node is on (updated with new plan sizes 6/28/10)
+LINODE_PLAN_IDS = {512:'1',
+                   768:'2',
+                  1024:'3',
+                  1536:'4',
+                  2048:'5',
+                  4096:'6',
+                  8192:'7',
+                 12288:'8',
+                 16384:'9',
+                 20480:'10'}
+
+
+class LinodeException(Exception):
+    """Error originating from the Linode API
+
+    This class wraps a Linode API error, a list of which is available in the
+    API documentation.  All Linode API errors are a numeric code and a
+    human-readable description.
+    """
+    def __str__(self):
+        return "(%u) %s" % (self.args[0], self.args[1])
+    def __repr__(self):
+        return "<LinodeException code %u '%s'>" % (self.args[0], self.args[1])
+
+
+class LinodeResponse(Response):
+    """Linode API response
+
+    Wraps the HTTP response returned by the Linode API, which should be JSON in
+    this structure:
+
+       {
+         "ERRORARRAY": [ ... ],
+         "DATA": [ ... ],
+         "ACTION": " ... "
+       }
+
+    libcloud does not take advantage of batching, so a response will always
+    reflect the above format.  A few weird quirks are caught here as well."""
+    def __init__(self, response):
+        """Instantiate a LinodeResponse from the HTTP response
+
+        @keyword response: The raw response returned by urllib
+        @return: parsed L{LinodeResponse}"""
+        self.body = response.read()
+        self.status = response.status
+        self.headers = dict(response.getheaders())
+        self.error = response.reason
+        self.invalid = LinodeException(0xFF,
+                                       "Invalid JSON received from server")
+
+        # Move parse_body() to here;  we can't be sure of failure until we've
+        # parsed the body into JSON.
+        self.objects, self.errors = self.parse_body()
+        if not self.success():
+            # Raise the first error, as there will usually only be one
+            raise self.errors[0]
+
+    def parse_body(self):
+        """Parse the body of the response into JSON objects
+
+        If the response chokes the parser, action and data will be returned as
+        None and errorarray will indicate an invalid JSON exception.
+
+        @return: C{list} of objects and C{list} of errors"""
+        try:
+            js = json.loads(self.body)
+        except:
+            raise MalformedResponseError("Failed to parse JSON", body=self.body,
+                driver=LinodeNodeDriver)
+
+        try:
+            if isinstance(js, dict):
+                # solitary response - promote to list
+                js = [js]
+            ret = []
+            errs = []
+            for obj in js:
+                if ("DATA" not in obj or "ERRORARRAY" not in obj
+                    or "ACTION" not in obj):
+                    ret.append(None)
+                    errs.append(self.invalid)
+                    continue
+                ret.append(obj["DATA"])
+                errs.extend(self._make_excp(e) for e in obj["ERRORARRAY"])
+            return (ret, errs)
+        except:
+            return (None, [self.invalid])
+
+    def success(self):
+        """Check the response for success
+
+        The way we determine success is by the presence of an error in
+        ERRORARRAY.  If one is there, we assume the whole request failed.
+
+        @return: C{bool} indicating a successful request"""
+        return len(self.errors) == 0
+
+    def _make_excp(self, error):
+        """Convert an API error to a LinodeException instance
+
+        @keyword error: JSON object containing C{ERRORCODE} and C{ERRORMESSAGE}
+        @type error: dict"""
+        if "ERRORCODE" not in error or "ERRORMESSAGE" not in error:
+            return None
+        if error["ERRORCODE"] == 4:
+            return InvalidCredsError(error["ERRORMESSAGE"])
+        return LinodeException(error["ERRORCODE"], error["ERRORMESSAGE"])
+
+
+class LinodeConnection(ConnectionKey):
+    """A connection to the Linode API
+
+    Wraps SSL connections to the Linode API, automagically injecting the
+    parameters that the API needs for each request."""
+    host = LINODE_API
+    responseCls = LinodeResponse
+
+    def add_default_params(self, params):
+        """Add parameters that are necessary for every request
+
+        This method adds C{api_key} and C{api_responseFormat} to the request."""
+        params["api_key"] = self.key
+        # Be explicit about this in case the default changes.
+        params["api_responseFormat"] = "json"
+        return params
+
+
+class LinodeNodeDriver(NodeDriver):
+    """libcloud driver for the Linode API
+
+    Rough mapping of which is which:
+
+        list_nodes              linode.list
+        reboot_node             linode.reboot
+        destroy_node            linode.delete
+        create_node             linode.create, linode.update,
+                                linode.disk.createfromdistribution,
+                                linode.disk.create, linode.config.create,
+                                linode.ip.addprivate, linode.boot
+        list_sizes              avail.linodeplans
+        list_images             avail.distributions
+        list_locations          avail.datacenters
+
+    For more information on the Linode API, be sure to read the reference:
+
+        http://www.linode.com/api/
+    """
+    type = Provider.LINODE
+    name = "Linode"
+    connectionCls = LinodeConnection
+    _linode_plan_ids = LINODE_PLAN_IDS
+
+    def __init__(self, key):
+        """Instantiate the driver with the given API key
+
+        @keyword key: the API key to use
+        @type key: C{str}"""
+        self.datacenter = None
+        NodeDriver.__init__(self, key)
+
+    # Converts Linode's state from DB to a NodeState constant.
+    LINODE_STATES = {
+        -2: NodeState.UNKNOWN,              # Boot Failed
+        -1: NodeState.PENDING,              # Being Created
+         0: NodeState.PENDING,              # Brand New
+         1: NodeState.RUNNING,              # Running
+         2: NodeState.TERMINATED,           # Powered Off
+         3: NodeState.REBOOTING,            # Shutting Down
+         4: NodeState.UNKNOWN               # Reserved
+    }
+
+    def list_nodes(self):
+        """List all Linodes that the API key can access
+
+        This call will return all Linodes that the API key in use has access to.
+        If a node is in this list, rebooting will work; however, creation and
+        destruction are a separate grant.
+
+        @return: C{list} of L{Node} objects that the API key can access"""
+        params = { "api_action": "linode.list" }
+        data = self.connection.request(LINODE_ROOT, params=params).objects[0]
+        return self._to_nodes(data)
+
+    def reboot_node(self, node):
+        """Reboot the given Linode
+
+        Will issue a shutdown job followed by a boot job, using the last booted
+        configuration.  In most cases, this will be the only configuration.
+
+        @keyword node: the Linode to reboot
+        @type node: L{Node}"""
+        params = { "api_action": "linode.reboot", "LinodeID": node.id }
+        self.connection.request(LINODE_ROOT, params=params)
+        return True
+
+    def destroy_node(self, node):
+        """Destroy the given Linode
+
+        Will remove the Linode from the account and issue a prorated credit. A
+        grant for removing Linodes from the account is required, otherwise this
+        method will fail.
+
+        In most cases, all disk images must be removed from a Linode before the
+        Linode can be removed; however, this call explicitly skips those
+        safeguards.  There is no going back from this method.
+
+        @keyword node: the Linode to destroy
+        @type node: L{Node}"""
+        params = { "api_action": "linode.delete", "LinodeID": node.id,
+            "skipChecks": True }
+        self.connection.request(LINODE_ROOT, params=params)
+        return True
+
+    def create_node(self, **kwargs):
+        """Create a new Linode, deploy a Linux distribution, and boot
+
+        This call abstracts much of the functionality of provisioning a Linode
+        and getting it booted.  A global grant to add Linodes to the account is
+        required, as this call will result in a billing charge.
+
+        Note that there is a safety valve of 5 Linodes per hour, in order to
+        prevent a runaway script from ruining your day.
+
+        @keyword name: the name to assign the Linode (mandatory)
+        @type name: C{str}
+
+        @keyword image: which distribution to deploy on the Linode (mandatory)
+        @type image: L{NodeImage}
+
+        @keyword size: the plan size to create (mandatory)
+        @type size: L{NodeSize}
+
+        @keyword auth: an SSH key or root password (mandatory)
+        @type auth: L{NodeAuthSSHKey} or L{NodeAuthPassword}
+
+        @keyword location: which datacenter to create the Linode in
+        @type location: L{NodeLocation}
+
+        @keyword ex_swap: size of the swap partition in MB (128)
+        @type ex_swap: C{int}
+
+        @keyword ex_rsize: size of the root partition in MB (plan size - swap).
+        @type ex_rsize: C{int}
+
+        @keyword ex_kernel: a kernel ID from avail.kernels (Latest 2.6 Stable).
+        @type ex_kernel: C{str}
+
+        @keyword ex_payment: one of 1, 12, or 24; subscription length (1)
+        @type ex_payment: C{int}
+
+        @keyword ex_comment: a small comment for the configuration (libcloud)
+        @type ex_comment: C{str}
+
+        @keyword ex_private: whether or not to request a private IP (False)
+        @type ex_private: C{bool}
+
+        @keyword lconfig: what to call the configuration (generated)
+        @type lconfig: C{str}
+
+        @keyword lroot: what to call the root image (generated)
+        @type lroot: C{str}
+
+        @keyword lswap: what to call the swap space (generated)
+        @type lswap: C{str}
+
+        @return: a L{Node} representing the newly-created Linode
+        """
+        name = kwargs["name"]
+        image = kwargs["image"]
+        size = kwargs["size"]
+        auth = kwargs["auth"]
+
+        # Pick a location (resolves LIBCLOUD-41 in JIRA)
+        if "location" in kwargs:
+            chosen = kwargs["location"].id
+        elif self.datacenter:
+            chosen = self.datacenter
+        else:
+            raise LinodeException(0xFB, "Need to select a datacenter first")
+
+        # Step 0: Parameter validation before we purchase
+        # We're especially careful here so we don't fail after purchase, rather
+        # than getting halfway through the process and having the API fail.
+
+        # Plan ID
+        plans = self.list_sizes()
+        if size.id not in [p.id for p in plans]:
+            raise LinodeException(0xFB, "Invalid plan ID -- avail.plans")
+
+        # Payment schedule
+        payment = "1" if "ex_payment" not in kwargs else str(kwargs["ex_payment"])
+        if payment not in ["1", "12", "24"]:
+            raise LinodeException(0xFB, "Invalid subscription (1, 12, 24)")
+
+        ssh = None
+        root = None
+        # SSH key and/or root password
+        if isinstance(auth, NodeAuthSSHKey):
+            ssh = auth.pubkey
+        elif isinstance(auth, NodeAuthPassword):
+            root = auth.password
+
+        if not ssh and not root:
+            raise LinodeException(0xFB, "Need SSH key or root password")
+        if not root is None and len(root) < 6:
+            raise LinodeException(0xFB, "Root password is too short")
+
+        # Swap size
+        try: swap = 128 if "ex_swap" not in kwargs else int(kwargs["ex_swap"])
+        except: raise LinodeException(0xFB, "Need an integer swap size")
+
+        # Root partition size
+        imagesize = (size.disk - swap) if "ex_rsize" not in kwargs else \
+            int(kwargs["ex_rsize"])
+        if (imagesize + swap) > size.disk:
+            raise LinodeException(0xFB, "Total disk images are too big")
+
+        # Distribution ID
+        distros = self.list_images()
+        if image.id not in [d.id for d in distros]:
+            raise LinodeException(0xFB,
+                                  "Invalid distro -- avail.distributions")
+
+        # Kernel
+        if "ex_kernel" in kwargs:
+            kernel = kwargs["ex_kernel"]
+        else:
+            if image.extra['64bit']:
+                kernel = 111 if image.extra['pvops'] else 107
+            else:
+                kernel = 110 if image.extra['pvops'] else 60
+        params = { "api_action": "avail.kernels" }
+        kernels = self.connection.request(LINODE_ROOT, params=params).objects[0]
+        if kernel not in [z["KERNELID"] for z in kernels]:
+            raise LinodeException(0xFB, "Invalid kernel -- avail.kernels")
+
+        # Comments
+        comments = "Created by Apache libcloud <http://www.libcloud.org>" if \
+            "ex_comment" not in kwargs else kwargs["ex_comment"]
+
+        # Labels
+        label = {
+            "lconfig": "[%s] Configuration Profile" % name,
+            "lroot": "[%s] %s Disk Image" % (name, image.name),
+            "lswap": "[%s] Swap Space" % name
+        }
+        for what in ["lconfig", "lroot", "lswap"]:
+            if what in kwargs:
+                label[what] = kwargs[what]
+
+        # Step 1: linode.create
+        params = {
+            "api_action":   "linode.create",
+            "DatacenterID": chosen,
+            "PlanID":       size.id,
+            "PaymentTerm":  payment
+        }
+        data = self.connection.request(LINODE_ROOT, params=params).objects[0]
+        linode = { "id": data["LinodeID"] }
+
+        # Step 1b. linode.update to rename the Linode
+        params = {
+            "api_action": "linode.update",
+            "LinodeID": linode["id"],
+            "Label": name
+        }
+        self.connection.request(LINODE_ROOT, params=params)
+
+        # Step 1c. linode.ip.addprivate if it was requested
+        if "ex_private" in kwargs and kwargs["ex_private"]:
+            params = {
+                "api_action":   "linode.ip.addprivate",
+                "LinodeID":     linode["id"]
+            }
+            self.connection.request(LINODE_ROOT, params=params)
+
+        # Step 2: linode.disk.createfromdistribution
+        if not root:
+            root = os.urandom(8).encode('hex')
+        params = {
+            "api_action":       "linode.disk.createfromdistribution",
+            "LinodeID":         linode["id"],
+            "DistributionID":   image.id,
+            "Label":            label["lroot"],
+            "Size":             imagesize,
+            "rootPass":         root,
+        }
+        if ssh: params["rootSSHKey"] = ssh
+        data = self.connection.request(LINODE_ROOT, params=params).objects[0]
+        linode["rootimage"] = data["DiskID"]
+
+        # Step 3: linode.disk.create for swap
+        params = {
+            "api_action":       "linode.disk.create",
+            "LinodeID":         linode["id"],
+            "Label":            label["lswap"],
+            "Type":             "swap",
+            "Size":             swap
+        }
+        data = self.connection.request(LINODE_ROOT, params=params).objects[0]
+        linode["swapimage"] = data["DiskID"]
+
+        # Step 4: linode.config.create for main profile
+        disks = "%s,%s,,,,,,," % (linode["rootimage"], linode["swapimage"])
+        params = {
+            "api_action":       "linode.config.create",
+            "LinodeID":         linode["id"],
+            "KernelID":         kernel,
+            "Label":            label["lconfig"],
+            "Comments":         comments,
+            "DiskList":         disks
+        }
+        data = self.connection.request(LINODE_ROOT, params=params).objects[0]
+        linode["config"] = data["ConfigID"]
+
+        # Step 5: linode.boot
+        params = {
+            "api_action":       "linode.boot",
+            "LinodeID":         linode["id"],
+            "ConfigID":         linode["config"]
+        }
+        self.connection.request(LINODE_ROOT, params=params)
+
+        # Make a node out of it and hand it back
+        params = { "api_action": "linode.list", "LinodeID": linode["id"] }
+        data = self.connection.request(LINODE_ROOT, params=params).objects[0]
+        return self._to_nodes(data)
+
+    def list_sizes(self, location=None):
+        """List available Linode plans
+
+        Gets the sizes that can be used for creating a Linode.  Since available
+        Linode plans vary per-location, this method can also be passed a
+        location to filter the availability.
+
+        @keyword location: the facility to retrieve plans in
+        @type location: NodeLocation
+
+        @return: a C{list} of L{NodeSize}s"""
+        params = { "api_action": "avail.linodeplans" }
+        data = self.connection.request(LINODE_ROOT, params=params).objects[0]
+        sizes = []
+        for obj in data:
+            n = NodeSize(id=obj["PLANID"], name=obj["LABEL"], ram=obj["RAM"],
+                    disk=(obj["DISK"] * 1024), bandwidth=obj["XFER"],
+                    price=obj["PRICE"], driver=self.connection.driver)
+            sizes.append(n)
+        return sizes
+
+    def list_images(self):
+        """List available Linux distributions
+
+        Retrieve all Linux distributions that can be deployed to a Linode.
+
+        @return: a C{list} of L{NodeImage}s"""
+        params = { "api_action": "avail.distributions" }
+        data = self.connection.request(LINODE_ROOT, params=params).objects[0]
+        distros = []
+        for obj in data:
+            i = NodeImage(id=obj["DISTRIBUTIONID"],
+                          name=obj["LABEL"],
+                          driver=self.connection.driver,
+                          extra={'pvops': obj['REQUIRESPVOPSKERNEL'],
+                                 '64bit': obj['IS64BIT']})
+            distros.append(i)
+        return distros
+
+    def list_locations(self):
+        """List available facilities for deployment
+
+        Retrieve all facilities that a Linode can be deployed in.
+
+        @return: a C{list} of L{NodeLocation}s"""
+        params = { "api_action": "avail.datacenters" }
+        data = self.connection.request(LINODE_ROOT, params=params).objects[0]
+        nl = []
+        for dc in data:
+            country = None
+            if "USA" in dc["LOCATION"]: country = "US"
+            elif "UK" in dc["LOCATION"]: country = "GB"
+            else: country = "??"
+            nl.append(NodeLocation(dc["DATACENTERID"],
+                                   dc["LOCATION"],
+                                   country,
+                                   self))
+        return nl
+
+    def linode_set_datacenter(self, dc):
+        """Set the default datacenter for Linode creation
+
+        Since Linodes must be created in a facility, this function sets the
+        default that L{create_node} will use.  If a C{location} keyword is not
+        passed to L{create_node}, this method must have already been used.
+
+        @keyword dc: the datacenter to create Linodes in unless specified
+        @type dc: L{NodeLocation}"""
+        did = dc.id
+        params = { "api_action": "avail.datacenters" }
+        data = self.connection.request(LINODE_ROOT, params=params).objects[0]
+        for datacenter in data:
+            if did == dc["DATACENTERID"]:
+                self.datacenter = did
+                return
+
+        dcs = ", ".join([d["DATACENTERID"] for d in data])
+        self.datacenter = None
+        raise LinodeException(0xFD, "Invalid datacenter (use one of %s)" % dcs)
+
+    def _to_nodes(self, objs):
+        """Convert returned JSON Linodes into Node instances
+
+        @keyword objs: C{list} of JSON dictionaries representing the Linodes
+        @type objs: C{list}
+        @return: C{list} of L{Node}s"""
+
+        # Get the IP addresses for the Linodes
+        nodes = {}
+        batch = []
+        for o in objs:
+            lid = o["LINODEID"]
+            nodes[lid] = n = Node(id=lid, name=o["LABEL"], public_ip=[],
+                private_ip=[], state=self.LINODE_STATES[o["STATUS"]],
+                driver=self.connection.driver)
+            n.extra = copy(o)
+            n.extra["PLANID"] = self._linode_plan_ids.get(o.get("TOTALRAM"))
+            batch.append({"api_action": "linode.ip.list", "LinodeID": lid})
+
+        # Avoid batch limitation
+        ip_answers = []
+        args = [iter(batch)] * 25
+        izip_longest = getattr(itertools, 'izip_longest', _izip_longest)
+        for twenty_five in izip_longest(*args):
+            twenty_five = [q for q in twenty_five if q]
+            params = { "api_action": "batch",
+                "api_requestArray": json.dumps(twenty_five) }
+            req = self.connection.request(LINODE_ROOT, params=params)
+            if not req.success() or len(req.objects) == 0:
+                return None
+            ip_answers.extend(req.objects)
+
+        # Add the returned IPs to the nodes and return them
+        for ip_list in ip_answers:
+            for ip in ip_list:
+                lid = ip["LINODEID"]
+                which = nodes[lid].public_ip if ip["ISPUBLIC"] == 1 else \
+                    nodes[lid].private_ip
+                which.append(ip["IPADDRESS"])
+        return nodes.values()
+
+    features = {"create_node": ["ssh_key", "password"]}
+
+def _izip_longest(*args, **kwds):
+    """Taken from Python docs
+
+    http://docs.python.org/library/itertools.html#itertools.izip
+    """
+    fillvalue = kwds.get('fillvalue')
+    def sentinel(counter = ([fillvalue]*(len(args)-1)).pop):
+        yield counter() # yields the fillvalue, or raises IndexError
+    fillers = itertools.repeat(fillvalue)
+    iters = [itertools.chain(it, sentinel(), fillers) for it in args]
+    try:
+        for tup in itertools.izip(*iters):
+            yield tup
+    except IndexError:
+        pass

Added: incubator/libcloud/trunk/libcloud/compute/drivers/opennebula.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/libcloud/compute/drivers/opennebula.py?rev=1079029&view=auto
==============================================================================
--- incubator/libcloud/trunk/libcloud/compute/drivers/opennebula.py (added)
+++ incubator/libcloud/trunk/libcloud/compute/drivers/opennebula.py Mon Mar  7 23:44:06 2011
@@ -0,0 +1,219 @@
+# Copyright 2002-2009, Distributed Systems Architecture Group, Universidad
+# Complutense de Madrid (dsa-research.org)
+#
+# 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.
+"""
+OpenNebula driver
+"""
+
+from base64 import b64encode
+import hashlib
+from xml.etree import ElementTree as ET
+
+from libcloud.common.base import ConnectionUserAndKey, Response
+from libcloud.common.types import InvalidCredsError
+from libcloud.compute.providers import Provider
+from libcloud.compute.types import NodeState
+from libcloud.compute.base import NodeDriver, Node, NodeLocation
+from libcloud.compute.base import NodeImage, NodeSize
+
+API_HOST = ''
+API_PORT = (4567, 443)
+API_SECURE = True
+
+
+class OpenNebulaResponse(Response):
+
+    def success(self):
+        i = int(self.status)
+        return i >= 200 and i <= 299
+
+    def parse_body(self):
+        if not self.body:
+            return None
+        return ET.XML(self.body)
+
+    def parse_error(self):
+        if int(self.status) == 401:
+            raise InvalidCredsError(self.body)
+        return self.body
+
+
+class OpenNebulaConnection(ConnectionUserAndKey):
+    """
+    Connection class for the OpenNebula driver
+    """
+
+    host = API_HOST
+    port = API_PORT
+    secure = API_SECURE
+    responseCls = OpenNebulaResponse
+
+    def add_default_headers(self, headers):
+        pass_sha1 = hashlib.sha1(self.key).hexdigest()
+        headers['Authorization'] = ("Basic %s" % b64encode("%s:%s" % (self.user_id, pass_sha1)))
+        return headers
+
+
+class OpenNebulaNodeDriver(NodeDriver):
+    """
+    OpenNebula node driver
+    """
+
+    connectionCls = OpenNebulaConnection
+    type = Provider.OPENNEBULA
+    name = 'OpenNebula'
+
+    NODE_STATE_MAP = {
+        'PENDING': NodeState.PENDING,
+        'ACTIVE': NodeState.RUNNING,
+        'DONE': NodeState.TERMINATED,
+        'STOPPED': NodeState.TERMINATED
+    }
+
+    def list_sizes(self, location=None):
+        return [
+          NodeSize(id=1,
+                   name="small",
+                   ram=None,
+                   disk=None,
+                   bandwidth=None,
+                   price=None,
+                   driver=self),
+          NodeSize(id=2,
+                   name="medium",
+                   ram=None,
+                   disk=None,
+                   bandwidth=None,
+                   price=None,
+                   driver=self),
+          NodeSize(id=3,
+                   name="large",
+                   ram=None,
+                   disk=None,
+                   bandwidth=None,
+                   price=None,
+                   driver=self),
+        ]
+
+    def list_nodes(self):
+        return self._to_nodes(self.connection.request('/compute').object)
+
+    def list_images(self, location=None):
+        return self._to_images(self.connection.request('/storage').object)
+
+    def list_locations(self):
+        return [NodeLocation(0,  'OpenNebula', 'ONE', self)]
+
+    def reboot_node(self, node):
+        compute_id = str(node.id)
+
+        url = '/compute/%s' % compute_id
+        resp1 = self.connection.request(url,method='PUT',data=self._xml_action(compute_id,'STOPPED'))
+
+        if resp1.status == 400:
+            return False
+
+        resp2 = self.connection.request(url,method='PUT',data=self._xml_action(compute_id,'RESUME'))
+
+        if resp2.status == 400:
+            return False
+
+        return True
+
+    def destroy_node(self, node):
+        url = '/compute/%s' % (str(node.id))
+        resp = self.connection.request(url,method='DELETE')
+
+        return resp.status == 204
+
+    def create_node(self, **kwargs):
+        """Create a new OpenNebula node
+
+        See L{NodeDriver.create_node} for more keyword args.
+        """
+        compute = ET.Element('COMPUTE')
+
+        name = ET.SubElement(compute, 'NAME')
+        name.text = kwargs['name']
+
+        # """
+        # Other extractable (but unused) information
+        # """
+        # instance_type = ET.SubElement(compute, 'INSTANCE_TYPE')
+        # instance_type.text = kwargs['size'].name
+        #
+        # storage = ET.SubElement(compute, 'STORAGE')
+        # disk = ET.SubElement(storage, 'DISK', {'image': str(kwargs['image'].id),
+        #                                        'dev': 'sda1'})
+
+        xml = ET.tostring(compute)
+
+        node = self.connection.request('/compute',method='POST',data=xml).object
+
+        return self._to_node(node)
+
+    def _to_images(self, object):
+        images = []
+        for element in object.findall("DISK"):
+            image_id = element.attrib["href"].partition("/storage/")[2]
+            image = self.connection.request(("/storage/%s" % (image_id))).object
+            images.append(self._to_image(image))
+
+        return images
+
+    def _to_image(self, image):
+        return NodeImage(id=image.findtext("ID"),
+                         name=image.findtext("NAME"),
+                         driver=self.connection.driver)
+
+    def _to_nodes(self, object):
+        computes = []
+        for element in object.findall("COMPUTE"):
+            compute_id = element.attrib["href"].partition("/compute/")[2]
+            compute = self.connection.request(("/compute/%s" % (compute_id))).object
+            computes.append(self._to_node(compute))
+
+        return computes
+
+    def _to_node(self, compute):
+        try:
+            state = self.NODE_STATE_MAP[compute.findtext("STATE")]
+        except KeyError:
+            state = NodeState.UNKNOWN
+
+        networks = []
+        for element in compute.findall("NIC"):
+            networks.append(element.attrib["ip"])
+
+        return Node(id=compute.findtext("ID"),
+                    name=compute.findtext("NAME"),
+                    state=state,
+                    public_ip=networks,
+                    private_ip=[],
+                    driver=self.connection.driver)
+
+    def _xml_action(self, compute_id, action):
+        compute = ET.Element('COMPUTE')
+
+        compute_id = ET.SubElement(compute, 'ID')
+        compute_id.text = str(compute_id)
+
+        state = ET.SubElement(compute, 'STATE')
+        state.text = action
+
+        xml = ET.tostring(compute)
+        return xml

Added: incubator/libcloud/trunk/libcloud/compute/drivers/rackspace.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/libcloud/compute/drivers/rackspace.py?rev=1079029&view=auto
==============================================================================
--- incubator/libcloud/trunk/libcloud/compute/drivers/rackspace.py (added)
+++ incubator/libcloud/trunk/libcloud/compute/drivers/rackspace.py Mon Mar  7 23:44:06 2011
@@ -0,0 +1,601 @@
+# 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.
+"""
+Rackspace driver
+"""
+import os
+
+import base64
+import urlparse
+
+from xml.etree import ElementTree as ET
+from xml.parsers.expat import ExpatError
+
+from libcloud.common.base import ConnectionUserAndKey, Response
+from libcloud.common.types import InvalidCredsError, MalformedResponseError
+from libcloud.compute.types import NodeState, Provider
+from libcloud.compute.base import NodeDriver, Node
+from libcloud.compute.base import NodeSize, NodeImage, NodeLocation
+
+RACKSPACE_US_AUTH_HOST='auth.api.rackspacecloud.com'
+RACKSPACE_UK_AUTH_HOST='lon.auth.api.rackspacecloud.com'
+
+NAMESPACE = 'http://docs.rackspacecloud.com/servers/api/v1.0'
+
+#
+# Prices need to be hardcoded as Rackspace doesn't expose them through
+# the API. Prices are associated with flavors, of which there are 7.
+# See - http://www.rackspacecloud.com/cloud_hosting_products/servers/pricing
+#
+RACKSPACE_PRICES = {
+    '1':'.015',
+    '2':'.030',
+    '3':'.060',
+    '4':'.120',
+    '5':'.240',
+    '6':'.480',
+    '7':'.960',
+}
+
+class RackspaceResponse(Response):
+
+    def success(self):
+        i = int(self.status)
+        return i >= 200 and i <= 299
+
+    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=RackspaceNodeDriver)
+        return body
+    def parse_error(self):
+        # TODO: fixup, Rackspace only uses response codes really!
+        try:
+            body = ET.XML(self.body)
+        except:
+            raise MalformedResponseError("Failed to parse XML", body=self.body, driver=RackspaceNodeDriver)
+        try:
+            text = "; ".join([ err.text or ''
+                               for err in
+                               body.getiterator()
+                               if err.text])
+        except ExpatError:
+            text = self.body
+        return '%s %s %s' % (self.status, self.error, text)
+
+
+class RackspaceConnection(ConnectionUserAndKey):
+    """
+    Connection class for the Rackspace driver
+    """
+
+    api_version = 'v1.0'
+    auth_host = RACKSPACE_US_AUTH_HOST
+    responseCls = RackspaceResponse
+
+    def __init__(self, user_id, key, secure=True):
+        self.__host = None
+        self.path = None
+        self.token = None
+        super(RackspaceConnection, self).__init__(user_id, key, secure)
+
+    def add_default_headers(self, headers):
+        headers['X-Auth-Token'] = self.token;
+        headers['Accept'] = 'application/xml'
+        return headers
+
+    @property
+    def host(self):
+        """
+        Rackspace uses a separate host for API calls which is only provided
+        after an initial authentication request. If we haven't made that
+        request yet, do it here. Otherwise, just return the management host.
+
+        TODO: Fixup for when our token expires (!!!)
+        """
+        if not self.__host:
+            # Initial connection used for authentication
+            conn = self.conn_classes[self.secure](self.auth_host, self.port[self.secure])
+            conn.request(
+                method='GET',
+                url='/%s' % self.api_version,
+                headers={
+                    'X-Auth-User': self.user_id,
+                    'X-Auth-Key': self.key
+                }
+            )
+            resp = conn.getresponse()
+            headers = dict(resp.getheaders())
+            try:
+                self.token = headers['x-auth-token']
+                endpoint = headers['x-server-management-url']
+            except KeyError:
+                raise InvalidCredsError()
+
+            scheme, server, self.path, param, query, fragment = (
+                urlparse.urlparse(endpoint)
+            )
+            if scheme is "https" and self.secure is not 1:
+                # TODO: Custom exception (?)
+                raise InvalidCredsError()
+
+            # Set host to where we want to make further requests to;
+            # close auth conn
+            self.__host = server
+            conn.close()
+
+        return self.__host
+
+    def request(self, action, params=None, data='', headers=None, method='GET'):
+        if not headers:
+            headers = {}
+        if not params:
+            params = {}
+        # Due to first-run authentication request, we may not have a path
+        if self.path:
+            action = self.path + action
+        if method in ("POST", "PUT"):
+            headers = {'Content-Type': 'application/xml; charset=UTF-8'}
+        if method == "GET":
+            params['cache-busting'] = os.urandom(8).encode('hex')
+        return super(RackspaceConnection, self).request(
+            action=action,
+            params=params, data=data,
+            method=method, headers=headers
+        )
+
+
+class RackspaceSharedIpGroup(object):
+    """
+    Shared IP group info.
+    """
+
+    def __init__(self, id, name, servers=None):
+        self.id = str(id)
+        self.name = name
+        self.servers = servers
+
+
+class RackspaceNodeIpAddresses(object):
+    """
+    List of public and private IP addresses of a Node.
+    """
+
+    def __init__(self, public_addresses, private_addresses):
+        self.public_addresses = public_addresses
+        self.private_addresses = private_addresses
+
+
+class RackspaceNodeDriver(NodeDriver):
+    """
+    Rackspace node driver.
+
+    Extra node attributes:
+        - password: root password, available after create.
+        - hostId: represents the host your cloud server runs on
+        - imageId: id of image
+        - flavorId: id of flavor
+    """
+    connectionCls = RackspaceConnection
+    type = Provider.RACKSPACE
+    name = 'Rackspace'
+
+    _rackspace_prices = RACKSPACE_PRICES
+
+    features = {"create_node": ["generates_password"]}
+
+    NODE_STATE_MAP = { 'BUILD': NodeState.PENDING,
+                       'REBUILD': NodeState.PENDING,
+                       'ACTIVE': NodeState.RUNNING,
+                       'SUSPENDED': NodeState.TERMINATED,
+                       'QUEUE_RESIZE': NodeState.PENDING,
+                       'PREP_RESIZE': NodeState.PENDING,
+                       'VERIFY_RESIZE': NodeState.RUNNING,
+                       'PASSWORD': NodeState.PENDING,
+                       'RESCUE': NodeState.PENDING,
+                       'REBUILD': NodeState.PENDING,
+                       'REBOOT': NodeState.REBOOTING,
+                       'HARD_REBOOT': NodeState.REBOOTING,
+                       'SHARE_IP': NodeState.PENDING,
+                       'SHARE_IP_NO_CONFIG': NodeState.PENDING,
+                       'DELETE_IP': NodeState.PENDING,
+                       'UNKNOWN': NodeState.UNKNOWN}
+
+    def list_nodes(self):
+        return self._to_nodes(self.connection.request('/servers/detail').object)
+
+    def list_sizes(self, location=None):
+        return self._to_sizes(self.connection.request('/flavors/detail').object)
+
+    def list_images(self, location=None):
+        return self._to_images(self.connection.request('/images/detail').object)
+
+    def list_locations(self):
+        """Lists available locations
+
+        Locations cannot be set or retrieved via the API, but currently
+        there are two locations, DFW and ORD.
+        """
+        return [NodeLocation(0, "Rackspace DFW1/ORD1", 'US', self)]
+
+    def _change_password_or_name(self, node, name=None, password=None):
+        uri = '/servers/%s' % (node.id)
+
+        if not name:
+            name = node.name
+
+        body = { 'xmlns': NAMESPACE,
+                 'name': name}
+
+        if password != None:
+            body['adminPass'] = password
+
+        server_elm = ET.Element('server', body)
+
+        resp = self.connection.request(uri, method='PUT', data=ET.tostring(server_elm))
+
+        if resp.status == 204 and password != None:
+            node.extra['password'] = password
+
+        return resp.status == 204
+
+    def ex_set_password(self, node, password):
+        """
+        Sets the Node's root password.
+
+        This will reboot the instance to complete the operation.
+
+        L{node.extra['password']} will be set to the new value if the
+        operation was successful.
+        """
+        return self._change_password_or_name(node, password=password)
+
+    def ex_set_server_name(self, node, name):
+        """
+        Sets the Node's name.
+
+        This will reboot the instance to complete the operation.
+        """
+        return self._change_password_or_name(node, name=name)
+
+    def create_node(self, **kwargs):
+        """Create a new rackspace node
+
+        See L{NodeDriver.create_node} for more keyword args.
+        @keyword    ex_metadata: Key/Value metadata to associate with a node
+        @type       ex_metadata: C{dict}
+
+        @keyword    ex_files:   File Path => File contents to create on the node
+        @type       ex_files:   C{dict}
+        """
+        name = kwargs['name']
+        image = kwargs['image']
+        size = kwargs['size']
+        server_elm = ET.Element(
+            'server',
+            {'xmlns': NAMESPACE,
+             'name': name,
+             'imageId': str(image.id),
+             'flavorId': str(size.id)}
+        )
+
+        metadata_elm = self._metadata_to_xml(kwargs.get("ex_metadata", {}))
+        if metadata_elm:
+            server_elm.append(metadata_elm)
+
+        files_elm = self._files_to_xml(kwargs.get("ex_files", {}))
+        if files_elm:
+            server_elm.append(files_elm)
+
+        shared_ip_elm = self._shared_ip_group_to_xml(kwargs.get("ex_shared_ip_group", None))
+        if shared_ip_elm:
+            server_elm.append(shared_ip_elm)
+
+        resp = self.connection.request("/servers",
+                                       method='POST',
+                                       data=ET.tostring(server_elm))
+        return self._to_node(resp.object)
+
+    def ex_rebuild(self, node_id, image_id):
+        elm = ET.Element(
+            'rebuild',
+            {'xmlns': NAMESPACE,
+             'imageId': image_id,
+            }
+        )
+        resp = self.connection.request("/servers/%s/action" % node_id,
+                                       method='POST',
+                                       data=ET.tostring(elm))
+        return resp.status == 202
+
+    def ex_create_ip_group(self, group_name, node_id=None):
+        group_elm = ET.Element(
+            'sharedIpGroup',
+            {'xmlns': NAMESPACE,
+             'name': group_name,
+            }
+        )
+        if node_id:
+            ET.SubElement(group_elm,
+                'server',
+                {'id': node_id}
+            )
+
+        resp = self.connection.request('/shared_ip_groups',
+                                       method='POST',
+                                       data=ET.tostring(group_elm))
+        return self._to_shared_ip_group(resp.object)
+
+    def ex_list_ip_groups(self, details=False):
+        uri = '/shared_ip_groups/detail' if details else '/shared_ip_groups'
+        resp = self.connection.request(uri,
+                                       method='GET')
+        groups = self._findall(resp.object, 'sharedIpGroup')
+        return [self._to_shared_ip_group(el) for el in groups]
+
+    def ex_delete_ip_group(self, group_id):
+        uri = '/shared_ip_groups/%s' % group_id
+        resp = self.connection.request(uri, method='DELETE')
+        return resp.status == 204
+
+    def ex_share_ip(self, group_id, node_id, ip, configure_node=True):
+        if configure_node:
+            str_configure = 'true'
+        else:
+            str_configure = 'false'
+
+        elm = ET.Element(
+            'shareIp',
+            {'xmlns': NAMESPACE,
+             'sharedIpGroupId' : group_id,
+             'configureServer' : str_configure}
+        )
+
+        uri = '/servers/%s/ips/public/%s' % (node_id, ip)
+
+        resp = self.connection.request(uri,
+                                       method='PUT',
+                                       data=ET.tostring(elm))
+        return resp.status == 202
+
+    def ex_unshare_ip(self, node_id, ip):
+        uri = '/servers/%s/ips/public/%s' % (node_id, ip)
+
+        resp = self.connection.request(uri,
+                                       method='DELETE')
+        return resp.status == 202
+
+    def ex_list_ip_addresses(self, node_id):
+        uri = '/servers/%s/ips' % node_id
+        resp = self.connection.request(uri,
+                                       method='GET')
+        return self._to_ip_addresses(resp.object)
+
+    def _metadata_to_xml(self, metadata):
+        if len(metadata) == 0:
+            return None
+
+        metadata_elm = ET.Element('metadata')
+        for k, v in metadata.items():
+            meta_elm = ET.SubElement(metadata_elm, 'meta', {'key': str(k) })
+            meta_elm.text = str(v)
+
+        return metadata_elm
+
+    def _files_to_xml(self, files):
+        if len(files) == 0:
+            return None
+
+        personality_elm = ET.Element('personality')
+        for k, v in files.items():
+            file_elm = ET.SubElement(personality_elm,
+                                     'file',
+                                     {'path': str(k)})
+            file_elm.text = base64.b64encode(v)
+
+        return personality_elm
+
+    def _reboot_node(self, node, reboot_type='SOFT'):
+        resp = self._node_action(node, ['reboot', ('type', reboot_type)])
+        return resp.status == 202
+
+    def ex_soft_reboot_node(self, node):
+        return self._reboot_node(node, reboot_type='SOFT')
+
+    def ex_hard_reboot_node(self, node):
+        return self._reboot_node(node, reboot_type='HARD')
+
+    def reboot_node(self, node):
+        return self._reboot_node(node, reboot_type='HARD')
+
+    def destroy_node(self, node):
+        uri = '/servers/%s' % (node.id)
+        resp = self.connection.request(uri, method='DELETE')
+        return resp.status == 202
+
+    def ex_get_node_details(self, node_id):
+        uri = '/servers/%s' % (node_id)
+        resp = self.connection.request(uri, method='GET')
+        if resp.status == 404:
+            return None
+        return self._to_node(resp.object)
+
+    def _node_action(self, node, body):
+        if isinstance(body, list):
+            attr = ' '.join(['%s="%s"' % (item[0], item[1])
+                             for item in body[1:]])
+            body = '<%s xmlns="%s" %s/>' % (body[0], NAMESPACE, attr)
+        uri = '/servers/%s/action' % (node.id)
+        resp = self.connection.request(uri, method='POST', data=body)
+        return resp
+
+    def _to_nodes(self, object):
+        node_elements = self._findall(object, 'server')
+        return [ self._to_node(el) for el in node_elements ]
+
+    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 _findall(self, element, xpath):
+        return element.findall(self._fixxpath(xpath))
+
+    def _to_node(self, el):
+        def get_ips(el):
+            return [ip.get('addr') for ip in el]
+
+        def get_meta_dict(el):
+            d = {}
+            for meta in el:
+                d[meta.get('key')] =  meta.text
+            return d
+
+        public_ip = get_ips(self._findall(el,
+                                          'addresses/public/ip'))
+        private_ip = get_ips(self._findall(el,
+                                          'addresses/private/ip'))
+        metadata = get_meta_dict(self._findall(el, 'metadata/meta'))
+
+        n = Node(id=el.get('id'),
+                 name=el.get('name'),
+                 state=self.NODE_STATE_MAP.get(el.get('status'), NodeState.UNKNOWN),
+                 public_ip=public_ip,
+                 private_ip=private_ip,
+                 driver=self.connection.driver,
+                 extra={
+                    'password': el.get('adminPass'),
+                    'hostId': el.get('hostId'),
+                    'imageId': el.get('imageId'),
+                    'flavorId': el.get('flavorId'),
+                    'uri': "https://%s%s/servers/%s" % (self.connection.host, self.connection.path, el.get('id')),
+                    'metadata': metadata,
+                 })
+        return n
+
+    def _to_sizes(self, object):
+        elements = self._findall(object, 'flavor')
+        return [ self._to_size(el) for el in elements ]
+
+    def _to_size(self, el):
+        s = NodeSize(id=el.get('id'),
+                     name=el.get('name'),
+                     ram=int(el.get('ram')),
+                     disk=int(el.get('disk')),
+                     bandwidth=None, # XXX: needs hardcode
+                     price=self._rackspace_prices.get(el.get('id')), # Hardcoded,
+                     driver=self.connection.driver)
+        return s
+
+    def _to_images(self, object):
+        elements = self._findall(object, "image")
+        return [ self._to_image(el)
+                 for el in elements
+                 if el.get('status') == 'ACTIVE' ]
+
+    def _to_image(self, el):
+        i = NodeImage(id=el.get('id'),
+                     name=el.get('name'),
+                     driver=self.connection.driver,
+                     extra={'serverId': el.get('serverId')})
+        return i
+
+    def ex_limits(self):
+        """
+        Extra call to get account's limits, such as
+        rates (for example amount of POST requests per day)
+        and absolute limits like total amount of available
+        RAM to be used by servers.
+        
+        @return: C{dict} with keys 'rate' and 'absolute'
+        """
+
+        def _to_rate(el):
+            rate = {}
+            for item in el.items():
+                rate[item[0]] = item[1]
+
+            return rate
+
+        def _to_absolute(el):
+            return {el.get('name'): el.get('value')}
+
+        limits = self.connection.request("/limits").object
+        rate = [ _to_rate(el) for el in self._findall(limits, 'rate/limit') ]
+        absolute = {}
+        for item in self._findall(limits, 'absolute/limit'):
+            absolute.update(_to_absolute(item))
+
+        return {"rate": rate, "absolute": absolute}
+
+    def ex_save_image(self, node, name):
+        """Create an image for node.
+
+        @keyword    node: node to use as a base for image
+        @param      node: L{Node}
+        @keyword    name: name for new image
+        @param      name: C{string}
+        """
+
+        image_elm = ET.Element(
+                'image',
+                {'xmlns': NAMESPACE,
+                    'name': name,
+                    'serverId': node.id}
+        )
+
+        return self._to_image(self.connection.request("/images",
+                    method="POST",
+                    data=ET.tostring(image_elm)).object)
+
+    def _to_shared_ip_group(self, el):
+        servers_el = self._findall(el, 'servers')
+        if servers_el:
+            servers = [s.get('id') for s in self._findall(servers_el[0], 'server')]
+        else:
+            servers = None
+        return RackspaceSharedIpGroup(id=el.get('id'),
+                                      name=el.get('name'),
+                                      servers=servers)
+
+    def _to_ip_addresses(self, el):
+        return RackspaceNodeIpAddresses(
+            [ip.get('addr') for ip in self._findall(self._findall(el, 'public')[0], 'ip')],
+            [ip.get('addr') for ip in self._findall(self._findall(el, 'private')[0], 'ip')]
+        )
+
+    def _shared_ip_group_to_xml(self, shared_ip_group):
+        if not shared_ip_group:
+            return None
+
+        return ET.Element('sharedIpGroupId', shared_ip_group)
+
+class RackspaceUKConnection(RackspaceConnection):
+    """
+    Connection class for the Rackspace UK driver
+    """
+    auth_host = RACKSPACE_UK_AUTH_HOST
+
+class RackspaceUKNodeDriver(RackspaceNodeDriver):
+    """Driver for Rackspace in the UK (London)
+    """
+
+    name = 'Rackspace (UK)'
+    connectionCls = RackspaceUKConnection
+
+    def list_locations(self):
+        return [NodeLocation(0, 'Rackspace UK London', 'UK', self)]

Added: incubator/libcloud/trunk/libcloud/compute/drivers/rimuhosting.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/libcloud/compute/drivers/rimuhosting.py?rev=1079029&view=auto
==============================================================================
--- incubator/libcloud/trunk/libcloud/compute/drivers/rimuhosting.py (added)
+++ incubator/libcloud/trunk/libcloud/compute/drivers/rimuhosting.py Mon Mar  7 23:44:06 2011
@@ -0,0 +1,313 @@
+# 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.
+"""
+RimuHosting 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.types import Provider, NodeState
+from libcloud.compute.base import NodeDriver, NodeSize, Node, NodeLocation
+from libcloud.compute.base import NodeImage, NodeAuthPassword
+
+API_CONTEXT = '/r'
+API_HOST = 'rimuhosting.com'
+API_PORT = (80,443)
+API_SECURE = True
+
+class RimuHostingException(Exception):
+    """
+    Exception class for RimuHosting driver
+    """
+
+    def __str__(self):
+        return self.args[0]
+
+    def __repr__(self):
+        return "<RimuHostingException '%s'>" % (self.args[0])
+
+class RimuHostingResponse(Response):
+    def __init__(self, response):
+        self.body = response.read()
+        self.status = response.status
+        self.headers = dict(response.getheaders())
+        self.error = response.reason
+
+        if self.success():
+            self.object = self.parse_body()
+
+    def success(self):
+        if self.status == 403:
+            raise InvalidCredsError()
+        return True
+    def parse_body(self):
+        try:
+            js = json.loads(self.body)
+            if js[js.keys()[0]]['response_type'] == "ERROR":
+                raise RimuHostingException(
+                    js[js.keys()[0]]['human_readable_message']
+                )
+            return js[js.keys()[0]]
+        except ValueError:
+            raise RimuHostingException('Could not parse body: %s'
+                                       % (self.body))
+        except KeyError:
+            raise RimuHostingException('Could not parse body: %s'
+                                       % (self.body))
+
+class RimuHostingConnection(ConnectionKey):
+    """
+    Connection class for the RimuHosting driver
+    """
+
+    api_context = API_CONTEXT
+    host = API_HOST
+    port = API_PORT
+    responseCls = RimuHostingResponse
+
+    def __init__(self, key, secure=True):
+        # override __init__ so that we can set secure of False for testing
+        ConnectionKey.__init__(self,key,secure)
+
+    def add_default_headers(self, headers):
+        # We want JSON back from the server. Could be application/xml
+        # (but JSON is better).
+        headers['Accept'] = 'application/json'
+        # Must encode all data as json, or override this header.
+        headers['Content-Type'] = 'application/json'
+
+        headers['Authorization'] = 'rimuhosting apikey=%s' % (self.key)
+        return headers;
+
+    def request(self, action, params=None, data='', headers=None, method='GET'):
+        if not headers:
+            headers = {}
+        if not params:
+            params = {}
+        # Override this method to prepend the api_context
+        return ConnectionKey.request(self, self.api_context + action,
+                                     params, data, headers, method)
+
+class RimuHostingNodeDriver(NodeDriver):
+    """
+    RimuHosting node driver
+    """
+
+    type = Provider.RIMUHOSTING
+    name = 'RimuHosting'
+    connectionCls = RimuHostingConnection
+
+    def __init__(self, key, host=API_HOST, port=API_PORT,
+                 api_context=API_CONTEXT, secure=API_SECURE):
+        # Pass in some extra vars so that
+        self.key = key
+        self.secure = secure
+        self.connection = self.connectionCls(key ,secure)
+        self.connection.host = host
+        self.connection.api_context = api_context
+        self.connection.port = port
+        self.connection.driver = self
+        self.connection.connect()
+
+    def _order_uri(self, node,resource):
+        # Returns the order uri with its resourse appended.
+        return "/orders/%s/%s" % (node.id,resource)
+
+    # TODO: Get the node state.
+    def _to_node(self, order):
+        n = Node(id=order['slug'],
+                name=order['domain_name'],
+                state=NodeState.RUNNING,
+                public_ip=(
+                    [order['allocated_ips']['primary_ip']]
+                    + order['allocated_ips']['secondary_ips']
+                ),
+                private_ip=[],
+                driver=self.connection.driver,
+                extra={'order_oid': order['order_oid'],
+                       'monthly_recurring_fee': order.get('billing_info').get('monthly_recurring_fee')})
+        return n
+
+    def _to_size(self,plan):
+        return NodeSize(
+            id=plan['pricing_plan_code'],
+            name=plan['pricing_plan_description'],
+            ram=plan['minimum_memory_mb'],
+            disk=plan['minimum_disk_gb'],
+            bandwidth=plan['minimum_data_transfer_allowance_gb'],
+            price=plan['monthly_recurring_amt']['amt_usd'],
+            driver=self.connection.driver
+        )
+
+    def _to_image(self,image):
+        return NodeImage(id=image['distro_code'],
+            name=image['distro_description'],
+            driver=self.connection.driver)
+
+    def list_sizes(self, location=None):
+        # Returns a list of sizes (aka plans)
+        # Get plans. Note this is really just for libcloud.
+        # We are happy with any size.
+        if location == None:
+            location = '';
+        else:
+            location = ";dc_location=%s" % (location.id)
+
+        res = self.connection.request('/pricing-plans;server-type=VPS%s' % (location)).object
+        return map(lambda x : self._to_size(x), res['pricing_plan_infos'])
+
+    def list_nodes(self):
+        # Returns a list of Nodes
+        # Will only include active ones.
+        res = self.connection.request('/orders;include_inactive=N').object
+        return map(lambda x : self._to_node(x), res['about_orders'])
+
+    def list_images(self, location=None):
+        # Get all base images.
+        # TODO: add other image sources. (Such as a backup of a VPS)
+        # All Images are available for use at all locations
+        res = self.connection.request('/distributions').object
+        return map(lambda x : self._to_image(x), res['distro_infos'])
+
+    def reboot_node(self, node):
+        # Reboot
+        # PUT the state of RESTARTING to restart a VPS.
+        # All data is encoded as JSON
+        data = {'reboot_request':{'running_state':'RESTARTING'}}
+        uri = self._order_uri(node,'vps/running-state')
+        self.connection.request(uri,data=json.dumps(data),method='PUT')
+        # XXX check that the response was actually successful
+        return True
+
+    def destroy_node(self, node):
+        # Shutdown a VPS.
+        uri = self._order_uri(node,'vps')
+        self.connection.request(uri,method='DELETE')
+        # XXX check that the response was actually successful
+        return True
+
+    def create_node(self, **kwargs):
+        """Creates a RimuHosting instance
+
+        See L{NodeDriver.create_node} for more keyword args.
+
+        @keyword    name: Must be a FQDN. e.g example.com.
+        @type       name: C{string}
+
+        @keyword    ex_billing_oid: If not set, a billing method is automatically picked.
+        @type       ex_billing_oid: C{string}
+
+        @keyword    ex_host_server_oid: The host server to set the VPS up on.
+        @type       ex_host_server_oid: C{string}
+
+        @keyword    ex_vps_order_oid_to_clone: Clone another VPS to use as the image for the new VPS.
+        @type       ex_vps_order_oid_to_clone: C{string}
+
+        @keyword    ex_num_ips: Number of IPs to allocate. Defaults to 1.
+        @type       ex_num_ips: C{int}
+
+        @keyword    ex_extra_ip_reason: Reason for needing the extra IPs.
+        @type       ex_extra_ip_reason: C{string}
+
+        @keyword    ex_memory_mb: Memory to allocate to the VPS.
+        @type       ex_memory_mb: C{int}
+
+        @keyword    ex_disk_space_mb: Diskspace to allocate to the VPS. Defaults to 4096 (4GB).
+        @type       ex_disk_space_mb: C{int}
+
+        @keyword    ex_disk_space_2_mb: Secondary disk size allocation. Disabled by default.
+        @type       ex_disk_space_2_mb: C{int}
+
+        @keyword    ex_control_panel: Control panel to install on the VPS.
+        @type       ex_control_panel: C{string}
+        """
+        # Note we don't do much error checking in this because we
+        # expect the API to error out if there is a problem.
+        name = kwargs['name']
+        image = kwargs['image']
+        size = kwargs['size']
+
+        data = {
+            'instantiation_options':{
+                'domain_name': name, 'distro': image.id
+            },
+            'pricing_plan_code': size.id,
+        }
+
+        if kwargs.has_key('ex_control_panel'):
+            data['instantiation_options']['control_panel'] = kwargs['ex_control_panel']
+
+        if kwargs.has_key('auth'):
+            auth = kwargs['auth']
+            if not isinstance(auth, NodeAuthPassword):
+                raise ValueError('auth must be of NodeAuthPassword type')
+            data['instantiation_options']['password'] = auth.password
+
+        if kwargs.has_key('ex_billing_oid'):
+            #TODO check for valid oid.
+            data['billing_oid'] = kwargs['ex_billing_oid']
+
+        if kwargs.has_key('ex_host_server_oid'):
+            data['host_server_oid'] = kwargs['ex_host_server_oid']
+
+        if kwargs.has_key('ex_vps_order_oid_to_clone'):
+            data['vps_order_oid_to_clone'] = kwargs['ex_vps_order_oid_to_clone']
+
+        if kwargs.has_key('ex_num_ips') and int(kwargs['ex_num_ips']) > 1:
+            if not kwargs.has_key('ex_extra_ip_reason'):
+                raise RimuHostingException('Need an reason for having an extra IP')
+            else:
+                if not data.has_key('ip_request'):
+                    data['ip_request'] = {}
+                data['ip_request']['num_ips'] = int(kwargs['ex_num_ips'])
+                data['ip_request']['extra_ip_reason'] = kwargs['ex_extra_ip_reason']
+
+        if kwargs.has_key('ex_memory_mb'):
+            if not data.has_key('vps_parameters'):
+                data['vps_parameters'] = {}
+            data['vps_parameters']['memory_mb'] = kwargs['ex_memory_mb']
+
+        if kwargs.has_key('ex_disk_space_mb'):
+            if not data.has_key('ex_vps_parameters'):
+                data['vps_parameters'] = {}
+            data['vps_parameters']['disk_space_mb'] = kwargs['ex_disk_space_mb']
+
+        if kwargs.has_key('ex_disk_space_2_mb'):
+            if not data.has_key('vps_parameters'):
+                data['vps_parameters'] = {}
+            data['vps_parameters']['disk_space_2_mb'] = kwargs['ex_disk_space_2_mb']
+
+        res = self.connection.request(
+            '/orders/new-vps',
+            method='POST',
+            data=json.dumps({"new-vps":data})
+        ).object
+        node = self._to_node(res['about_order'])
+        node.extra['password'] = res['new_order_request']['instantiation_options']['password']
+        return node
+
+    def list_locations(self):
+        return [
+            NodeLocation('DCAUCKLAND', "RimuHosting Auckland", 'NZ', self),
+            NodeLocation('DCDALLAS', "RimuHosting Dallas", 'US', self),
+            NodeLocation('DCLONDON', "RimuHosting London", 'GB', self),
+            NodeLocation('DCSYDNEY', "RimuHosting Sydney", 'AU', self),
+        ]
+
+    features = {"create_node": ["password"]}

Added: incubator/libcloud/trunk/libcloud/compute/drivers/slicehost.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/libcloud/compute/drivers/slicehost.py?rev=1079029&view=auto
==============================================================================
--- incubator/libcloud/trunk/libcloud/compute/drivers/slicehost.py (added)
+++ incubator/libcloud/trunk/libcloud/compute/drivers/slicehost.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.
+"""
+Slicehost Driver
+"""
+import base64
+import socket
+
+from xml.etree import ElementTree as ET
+from xml.parsers.expat import ExpatError
+
+from libcloud.common.base import ConnectionUserAndKey, ConnectionKey, Response
+from libcloud.compute.types import NodeState, Provider, InvalidCredsError, MalformedResponseError
+from libcloud.compute.base import NodeSize, NodeDriver, NodeImage, NodeLocation
+from libcloud.compute.base import Node, is_private_subnet
+
+class SlicehostResponse(Response):
+
+    def parse_body(self):
+        # length of 1 can't be valid XML, but on destroy node, slicehost returns
+        # a 1 byte response with a "Content-Type: application/xml" header. booya.
+        if not self.body or len(self.body) <= 1:
+            return None
+        try:
+            body = ET.XML(self.body)
+        except:
+            raise MalformedResponseError("Failed to parse XML", body=self.body, driver=SlicehostNodeDriver)
+        return body
+
+    def parse_error(self):
+        if self.status == 401:
+            raise InvalidCredsError(self.body)
+
+        try:
+            body = ET.XML(self.body)
+        except:
+            raise MalformedResponseError("Failed to parse XML", body=self.body, driver=SlicehostNodeDriver)
+        try:
+            return "; ".join([ err.text
+                               for err in
+                               body.findall('error') ])
+        except ExpatError:
+            return self.body
+
+
+class SlicehostConnection(ConnectionKey):
+    """
+    Connection class for the Slicehost driver
+    """
+
+    host = 'api.slicehost.com'
+    responseCls = SlicehostResponse
+
+    def add_default_headers(self, headers):
+        headers['Authorization'] = ('Basic %s'
+                              % (base64.b64encode('%s:' % self.key)))
+        return headers
+
+
+class SlicehostNodeDriver(NodeDriver):
+    """
+    Slicehost node driver
+    """
+
+    connectionCls = SlicehostConnection
+
+    type = Provider.SLICEHOST
+    name = 'Slicehost'
+
+    features = {"create_node": ["generates_password"]}
+
+    NODE_STATE_MAP = { 'active': NodeState.RUNNING,
+                       'build': NodeState.PENDING,
+                       'reboot': NodeState.REBOOTING,
+                       'hard_reboot': NodeState.REBOOTING,
+                       'terminated': NodeState.TERMINATED }
+
+    def list_nodes(self):
+        return self._to_nodes(self.connection.request('/slices.xml').object)
+
+    def list_sizes(self, location=None):
+        return self._to_sizes(self.connection.request('/flavors.xml').object)
+
+    def list_images(self, location=None):
+        return self._to_images(self.connection.request('/images.xml').object)
+
+    def list_locations(self):
+        return [
+            NodeLocation(0, 'Slicehost St. Louis (STL-A)', 'US', self),
+            NodeLocation(0, 'Slicehost St. Louis (STL-B)', 'US', self),
+            NodeLocation(0, 'Slicehost Dallas-Fort Worth (DFW-1)', 'US', self)
+        ]
+
+    def create_node(self, **kwargs):
+        name = kwargs['name']
+        image = kwargs['image']
+        size = kwargs['size']
+        uri = '/slices.xml'
+
+        # create a slice obj
+        root = ET.Element('slice')
+        el_name = ET.SubElement(root, 'name')
+        el_name.text = name
+        flavor_id = ET.SubElement(root, 'flavor-id')
+        flavor_id.text = str(size.id)
+        image_id = ET.SubElement(root, 'image-id')
+        image_id.text = str(image.id)
+        xml = ET.tostring(root)
+
+        node = self._to_nodes(
+            self.connection.request(
+                uri,
+                method='POST',
+                data=xml,
+                headers={'Content-Type': 'application/xml'}
+            ).object
+        )[0]
+        return node
+
+    def reboot_node(self, node):
+        """Reboot the node by passing in the node object"""
+
+        # 'hard' could bubble up as kwarg depending on how reboot_node
+        # turns out. Defaulting to soft reboot.
+        #hard = False
+        #reboot = self.api.hard_reboot if hard else self.api.reboot
+        #expected_status = 'hard_reboot' if hard else 'reboot'
+
+        uri = '/slices/%s/reboot.xml' % (node.id)
+        node = self._to_nodes(
+            self.connection.request(uri, method='PUT').object
+        )[0]
+        return node.state == NodeState.REBOOTING
+
+    def destroy_node(self, node):
+        """Destroys the node
+
+        Requires 'Allow Slices to be deleted or rebuilt from the API' to be
+        ticked at https://manage.slicehost.com/api, otherwise returns::
+            <errors>
+              <error>You must enable slice deletes in the SliceManager</error>
+              <error>Permission denied</error>
+            </errors>
+        """
+        uri = '/slices/%s/destroy.xml' % (node.id)
+        self.connection.request(uri, method='PUT')
+        return True
+
+    def _to_nodes(self, object):
+        if object.tag == 'slice':
+            return [ self._to_node(object) ]
+        node_elements = object.findall('slice')
+        return [ self._to_node(el) for el in node_elements ]
+
+    def _to_node(self, element):
+
+        attrs = [ 'name', 'image-id', 'progress', 'id', 'bw-out', 'bw-in',
+                  'flavor-id', 'status', 'ip-address', 'root-password' ]
+
+        node_attrs = {}
+        for attr in attrs:
+            node_attrs[attr] = element.findtext(attr)
+
+        # slicehost does not determine between public and private, so we
+        # have to figure it out
+        primary_ip = element.findtext('ip-address')
+        public_ip = []
+        private_ip = []
+        for addr in element.findall('addresses/address'):
+            ip = addr.text
+            try:
+                socket.inet_aton(ip)
+            except socket.error:
+                # not a valid ip
+                continue
+            if is_private_subnet(ip):
+                private_ip.append(ip)
+            else:
+                public_ip.append(ip)
+
+        public_ip.append(primary_ip)
+
+        public_ip = list(set(public_ip))
+
+        try:
+            state = self.NODE_STATE_MAP[element.findtext('status')]
+        except:
+            state = NodeState.UNKNOWN
+
+        # for consistency with other drivers, we put this in two places.
+        node_attrs['password'] = node_attrs['root-password']
+        extra = {}
+        for k in node_attrs.keys():
+            ek = k.replace("-", "_")
+            extra[ek] = node_attrs[k]
+        n = Node(id=element.findtext('id'),
+                 name=element.findtext('name'),
+                 state=state,
+                 public_ip=public_ip,
+                 private_ip=private_ip,
+                 driver=self.connection.driver,
+                 extra=extra)
+        return n
+
+    def _to_sizes(self, object):
+        if object.tag == 'flavor':
+            return [ self._to_size(object) ]
+        elements = object.findall('flavor')
+        return [ self._to_size(el) for el in elements ]
+
+    def _to_size(self, element):
+        s = NodeSize(id=int(element.findtext('id')),
+                     name=str(element.findtext('name')),
+                     ram=int(element.findtext('ram')),
+                     disk=None, # XXX: needs hardcode
+                     bandwidth=None, # XXX: needs hardcode
+                     price=float(element.findtext('price'))/(100*24*30),
+                     driver=self.connection.driver)
+        return s
+
+    def _to_images(self, object):
+        if object.tag == 'image':
+            return [ self._to_image(object) ]
+        elements = object.findall('image')
+        return [ self._to_image(el) for el in elements ]
+
+    def _to_image(self, element):
+        i = NodeImage(id=int(element.findtext('id')),
+                     name=str(element.findtext('name')),
+                     driver=self.connection.driver)
+        return i



Mime
View raw message