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())
+