You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@libcloud.apache.org by or...@apache.org on 2010/05/06 22:10:55 UTC

svn commit: r941891 - in /incubator/libcloud/trunk: libcloud/drivers/dreamhost.py libcloud/providers.py libcloud/types.py test/__init__.py test/test_dreamhost.py

Author: oremj
Date: Thu May  6 20:10:55 2010
New Revision: 941891

URL: http://svn.apache.org/viewvc?rev=941891&view=rev
Log:
LIBCLOUD-18: New Libcloud Driver for DreamHost Private Servers

Author:    Kyle Marsh <ky...@hq.newdream.net>
Signed-off-by: Jeremy Orem <je...@gmail.com>

Added:
    incubator/libcloud/trunk/libcloud/drivers/dreamhost.py
    incubator/libcloud/trunk/test/test_dreamhost.py
Modified:
    incubator/libcloud/trunk/libcloud/providers.py
    incubator/libcloud/trunk/libcloud/types.py
    incubator/libcloud/trunk/test/__init__.py

Added: incubator/libcloud/trunk/libcloud/drivers/dreamhost.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/libcloud/drivers/dreamhost.py?rev=941891&view=auto
==============================================================================
--- incubator/libcloud/trunk/libcloud/drivers/dreamhost.py (added)
+++ incubator/libcloud/trunk/libcloud/drivers/dreamhost.py Thu May  6 20:10:55 2010
@@ -0,0 +1,245 @@
+# 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
+"""
+import uuid
+
+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
+
+# 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 'data' in response:
+            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):
+        """Create a new Dreamhost node
+
+        See L{NodeDriver.create_node} for more keyword args.
+
+        @keyword    ex_movedata: Copy all your existing users to this new PS
+        @type       ex_movedata: C{str}
+        """
+        size = kwargs['size'].ram
+        params = {
+            'cmd' : 'dreamhost_ps-add_ps',
+            'movedata' : kwargs.get('movedata', 'no'),
+            'type' : kwargs['image'].name,
+            'size' : size
+        }
+        data = self.connection.request('/', params).object
+        return Node(
+            id = data['added_' + 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']
+            }
+        )

Modified: incubator/libcloud/trunk/libcloud/providers.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/libcloud/providers.py?rev=941891&r1=941890&r2=941891&view=diff
==============================================================================
--- incubator/libcloud/trunk/libcloud/providers.py (original)
+++ incubator/libcloud/trunk/libcloud/providers.py Thu May  6 20:10:55 2010
@@ -51,6 +51,8 @@ DRIVERS = {
         ('libcloud.drivers.ibm_sbc', 'IBMNodeDriver'),
     Provider.OPENNEBULA:
         ('libcloud.drivers.opennebula', 'OpenNebulaNodeDriver'),
+    Provider.DREAMHOST:
+        ('libcloud.drivers.dreamhost', 'DreamhostNodeDriver'),
 }
 
 def get_driver(provider):

Modified: incubator/libcloud/trunk/libcloud/types.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/libcloud/types.py?rev=941891&r1=941890&r2=941891&view=diff
==============================================================================
--- incubator/libcloud/trunk/libcloud/types.py (original)
+++ incubator/libcloud/trunk/libcloud/types.py Thu May  6 20:10:55 2010
@@ -34,6 +34,7 @@ class Provider(object):
     @cvar ECP: Enomaly
     @cvar IBM: IBM Developer Cloud
     @cvar OPENNEBULA: OpenNebula.org
+    @cvar DREAMHOST: DreamHost Private Server
     """
     DUMMY = 0
     EC2 = 1  # deprecated name
@@ -54,6 +55,7 @@ class Provider(object):
     ECP = 14
     IBM = 15
     OPENNEBULA = 16
+    DREAMHOST = 17
 
 class NodeState(object):
     """

Modified: incubator/libcloud/trunk/test/__init__.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/test/__init__.py?rev=941891&r1=941890&r2=941891&view=diff
==============================================================================
--- incubator/libcloud/trunk/test/__init__.py (original)
+++ incubator/libcloud/trunk/test/__init__.py Thu May  6 20:10:55 2010
@@ -117,7 +117,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)

Added: incubator/libcloud/trunk/test/test_dreamhost.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/test/test_dreamhost.py?rev=941891&view=auto
==============================================================================
--- incubator/libcloud/trunk/test/test_dreamhost.py (added)
+++ incubator/libcloud/trunk/test/test_dreamhost.py Thu May  6 20:10:55 2010
@@ -0,0 +1,278 @@
+# 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, TestCaseMixin):
+
+    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
+
+    def test_list_locations_response(self):
+        self.assertRaises(NotImplementedError, self.driver.list_locations)
+
+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())
+