The Dreamhost Driver patch has been updated as per Jeremy's
suggestions.  Just like last time, there are two files,
"dreamhost_driver.patch" containing the output from `git-format-patch`
and "dreamhost_driver_squash.patch" containing the output from
`git-diff`.  The squashed patch is attached here, as well.

Any further comments would be appreciated.

~Kyle Marsh
diff --git a/libcloud/drivers/dreamhost.py b/libcloud/drivers/dreamhost.py
new file mode 100644
index 0000000..9001de6
--- /dev/null
+++ b/libcloud/drivers/dreamhost.py
@@ -0,0 +1,234 @@
+# 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.
+# libcloud.org 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
+"""
+from libcloud.interface import INodeDriver
+from libcloud.base import ConnectionKey, Response, NodeDriver, Node
+from libcloud.base import NodeSize, NodeImage, NodeLocation
+from libcloud.types import Provider, NodeState, InvalidCredsException
+#from zope.interface import implements
+import uuid
+
+# JSON is included in the standard library starting with Python 2.6.  For 2.5
+# and 2.4, there's a simplejson egg at: http://pypi.python.org/pypi/simplejson
+try: import json
+except: import simplejson as json
+
+"""
+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 response.has_key('data'):
+            if response['data'] == 'invalid_api_key':
+                raise InvalidCredsException, "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):
+        size = kwargs['size'].ram
+        params = {
+            'cmd' : 'dreamhost_ps-add_ps',
+            'movedata' : kwargs['movedata'],
+            'type' : kwargs['image'].name,
+            'size' : size
+        }
+        data = self.connection.request('/', params).object
+        return Node(
+            id = data['added_' + kwargs['image'].name],
+            name = data['added_' + kwargs['image'].name],
+            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']
+            }
+        )
+
diff --git a/libcloud/providers.py b/libcloud/providers.py
index 3e5559e..e94f3e0 100644
--- a/libcloud/providers.py
+++ b/libcloud/providers.py
@@ -49,6 +49,8 @@ DRIVERS = {
         ('libcloud.drivers.ec2', 'EucNodeDriver'),
     Provider.IBM:
         ('libcloud.drivers.ibm', 'IBMNodeDriver'),
+    Provider.DREAMHOST:
+        ('libcloud.drivers.dreamhost', 'DreamhostNodeDriver'),
 }
 
 def get_driver(provider):
diff --git a/libcloud/types.py b/libcloud/types.py
index 85f358c..3b3781d 100644
--- a/libcloud/types.py
+++ b/libcloud/types.py
@@ -32,6 +32,7 @@ class Provider(object):
     @cvar VCLOUD: vmware vCloud
     @cvar RIMUHOSTING: RimuHosting.com
     @cvar ECP: Enomaly
+    @cvar DREAMHOST: DreamHost Private Server
     """
     DUMMY = 0
     EC2 = 1  # deprecated name
@@ -51,6 +52,7 @@ class Provider(object):
     EUCALYPTUS = 13
     ECP = 14
     IBM = 15
+    DREAMHOST = 16
 
 class NodeState(object):
     """
diff --git a/test/__init__.py b/test/__init__.py
index 2645fc6..9873810 100644
--- a/test/__init__.py
+++ b/test/__init__.py
@@ -119,7 +119,7 @@ class MockHttp(object):
         if self.type:
             meth_name = '%s_%s' % (meth_name, self.type) 
         if self.use_param:
-            param = qs[self.use_param][0].replace('.', '_')
+            param = qs[self.use_param][0].replace('.', '_').replace('-','_')
             meth_name = '%s_%s' % (meth_name, param)
         meth = getattr(self, meth_name)
         status, body, headers, reason = meth(method, url, body, headers)
diff --git a/test/test_dreamhost.py b/test/test_dreamhost.py
new file mode 100644
index 0000000..8149eb2
--- /dev/null
+++ b/test/test_dreamhost.py
@@ -0,0 +1,275 @@
+# 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.
+# libcloud.org 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.
+import sys
+import unittest
+
+from libcloud.drivers.dreamhost import DreamhostNodeDriver
+from libcloud.types import Provider, NodeState, InvalidCredsException
+from libcloud.base import Node, NodeImage, NodeSize
+
+import httplib
+
+try: import json
+except: import simplejson as json
+
+from test import MockHttp, multipleresponse, TestCaseMixin
+from secrets import DREAMHOST_KEY
+
+#class DreamhostTest(unittest.TestCase, TestCaseMixin):
+class DreamhostTest(unittest.TestCase):
+
+    def setUp(self):
+        DreamhostNodeDriver.connectionCls.conn_classes = (
+            None,
+            DreamhostMockHttp
+        )
+        DreamhostMockHttp.type = None
+        DreamhostMockHttp.use_param = 'cmd'
+        self.driver = DreamhostNodeDriver('foo')
+
+    def test_invalid_creds(self):
+        """
+        Tests the error-handling for passing a bad API Key to the DreamHost API
+        """
+        DreamhostMockHttp.type = 'BAD_AUTH'
+        try:
+            self.driver.list_nodes()
+            self.assertTrue(False) # Above command should have thrown an InvalidCredsException
+        except InvalidCredsException:
+            self.assertTrue(True)
+
+
+    def test_list_nodes(self):
+        """
+        Test list_nodes for DreamHost PS driver.  Should return a list of two nodes:
+            -   account_id: 000000
+                ip: 75.119.203.51
+                memory_mb: 500
+                ps: ps22174
+                start_date: 2010-02-25
+                type: web
+            -   account_id: 000000
+                ip: 75.119.203.52
+                memory_mb: 1500
+                ps: ps22175
+                start_date: 2010-02-25
+                type: mysql
+        """
+
+        nodes = self.driver.list_nodes()
+        self.assertEqual(len(nodes), 2)
+        web_node = nodes[0]
+        mysql_node = nodes[1]
+
+        # Web node tests
+        self.assertEqual(web_node.id, 'ps22174')
+        self.assertEqual(web_node.state, NodeState.UNKNOWN)
+        self.assertTrue('75.119.203.51' in web_node.public_ip)
+        self.assertTrue(
+            web_node.extra.has_key('current_size') and
+            web_node.extra['current_size'] == 500
+        )
+        self.assertTrue(
+            web_node.extra.has_key('account_id') and
+            web_node.extra['account_id'] == 000000
+        )
+        self.assertTrue(
+            web_node.extra.has_key('type') and
+            web_node.extra['type'] == 'web'
+        )
+        # MySql node tests
+        self.assertEqual(mysql_node.id, 'ps22175')
+        self.assertEqual(mysql_node.state, NodeState.UNKNOWN)
+        self.assertTrue('75.119.203.52' in mysql_node.public_ip)
+        self.assertTrue(
+            mysql_node.extra.has_key('current_size') and
+            mysql_node.extra['current_size'] == 1500
+        )
+        self.assertTrue(
+            mysql_node.extra.has_key('account_id') and
+            mysql_node.extra['account_id'] == 000000
+        )
+        self.assertTrue(
+            mysql_node.extra.has_key('type') and
+            mysql_node.extra['type'] == 'mysql'
+        )
+
+    def test_create_node(self):
+        """
+        Test create_node for DreamHost PS driver.
+        This is not remarkably compatible with libcloud.  The DH API allows
+        users to specify what image they want to create and whether to move
+        all their data to the (web) PS. It does NOT accept a name, size, or
+        location.  The only information it returns is the PS's context id
+        Once the PS is ready it will appear in the list generated by list_ps.
+        """
+        new_node = self.driver.create_node(
+            image = self.driver.list_images()[0],
+            size = self.driver.list_sizes()[0],
+            movedata = 'no',
+        )
+        self.assertEqual(new_node.id, 'ps12345')
+        self.assertEqual(new_node.state, NodeState.PENDING)
+        self.assertTrue(
+            new_node.extra.has_key('type') and
+            new_node.extra['type'] == 'web'
+        )
+
+    def test_destroy_node(self):
+        """
+        Test destroy_node for DreamHost PS driver
+        """
+        node = self.driver.list_nodes()[0]
+        self.assertTrue(self.driver.destroy_node(node))
+
+    def test_destroy_node_failure(self):
+        """
+        Test destroy_node failure for DreamHost PS driver
+        """
+        node = self.driver.list_nodes()[0]
+
+        DreamhostMockHttp.type = 'API_FAILURE'
+        self.assertFalse(self.driver.destroy_node(node))
+
+    def test_reboot_node(self):
+        """
+        Test reboot_node for DreamHost PS driver.
+        """
+        node = self.driver.list_nodes()[0]
+        self.assertTrue(self.driver.reboot_node(node))
+
+    def test_reboot_node_failure(self):
+        """
+        Test reboot_node failure for DreamHost PS driver
+        """
+        node = self.driver.list_nodes()[0]
+
+        DreamhostMockHttp.type = 'API_FAILURE'
+        self.assertFalse(self.driver.reboot_node(node))
+
+    def test_resize_node(self):
+        """
+        Test resize_node for DreamHost PS driver
+        """
+        node = self.driver.list_nodes()[0]
+        self.assertTrue(self.driver._resize_node(node, 400))
+
+    def test_resize_node_failure(self):
+        """
+        Test reboot_node faliure for DreamHost PS driver
+        """
+        node = self.driver.list_nodes()[0]
+
+        DreamhostMockHttp.type = 'API_FAILURE'
+        self.assertFalse(self.driver._resize_node(node, 400))
+
+    def test_list_images(self):
+        """
+        Test list_images for DreamHost PS driver.
+        """
+        images = self.driver.list_images()
+        self.assertEqual(len(images), 2)
+        self.assertEqual(images[0].id, 'web')
+        self.assertEqual(images[0].name, 'web')
+        self.assertEqual(images[1].id, 'mysql')
+        self.assertEqual(images[1].name, 'mysql')
+
+    def test_list_sizes(self):
+        sizes = self.driver.list_sizes()
+        self.assertEqual(len(sizes), 5)
+
+        self.assertEqual(sizes[0].id, 'default')
+        self.assertEqual(sizes[0].bandwidth, None)
+        self.assertEqual(sizes[0].disk, None)
+        self.assertEqual(sizes[0].ram, 2300)
+        self.assertEqual(sizes[0].price, 115)
+
+    def test_list_locations(self):
+        try:
+            self.driver.list_locations()
+        except NotImplementedError:
+            pass
+
+class DreamhostMockHttp(MockHttp):
+
+    def _BAD_AUTH_dreamhost_ps_list_ps(self, method, url, body, headers):
+        body = json.dumps({'data' : 'invalid_api_key', 'result' : 'error'})
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _dreamhost_ps_add_ps(self, method, url, body, headers):
+        body = json.dumps({'data' : {'added_web' : 'ps12345'}, 'result' : 'success'})
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _dreamhost_ps_list_ps(self, method, url, body, headers):
+        data = [{
+            'account_id' : 000000,
+            'ip': '75.119.203.51',
+            'memory_mb' : 500,
+            'ps' : 'ps22174',
+            'start_date' : '2010-02-25',
+            'type' : 'web'
+        },
+        {
+            'account_id' : 000000,
+            'ip' : '75.119.203.52',
+            'memory_mb' : 1500,
+            'ps' : 'ps22175',
+            'start_date' : '2010-02-25',
+            'type' : 'mysql'
+        }]
+        result = 'success'
+        body = json.dumps({'data' : data, 'result' : result})
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _dreamhost_ps_list_images(self, method, url, body, headers):
+        data = [{
+            'description' : 'Private web server',
+            'image' : 'web'
+        },
+        {
+            'description' : 'Private MySQL server',
+            'image' : 'mysql'
+        }]
+        result = 'success'
+        body = json.dumps({'data' : data, 'result' : result})
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _dreamhost_ps_reboot(self, method, url, body, headers):
+        body = json.dumps({'data' : 'reboot_scheduled', 'result' : 'success'})
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _API_FAILURE_dreamhost_ps_reboot(self, method, url, body, headers):
+        body = json.dumps({'data' : 'no_such_ps', 'result' : 'error'})
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _dreamhost_ps_set_size(self, method, url, body, headers):
+        body = json.dumps({'data' : {'memory-mb' : '500'}, 'result' : 'success'})
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _API_FAILURE_dreamhost_ps_set_size(self, method, url, body, headers):
+        body = json.dumps({'data' : 'internal_error_setting_size', 'result' : 'error'})
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _dreamhost_ps_remove_ps(self, method, url, body, headers):
+        body = json.dumps({'data' : 'removed_web', 'result' : 'success'})
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _API_FAILURE_dreamhost_ps_remove_ps(self, method, url, body, headers):
+        body = json.dumps({'data' : 'no_such_ps', 'result' : 'error'})
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+if __name__ == '__main__':
+    sys.exit(unittest.main())
+

Reply via email to