From 2d305d91dc2a3e2932238bd5d2faef65d6c4fbb0 Mon Sep 17 00:00:00 2001 From: Christian Paredes Date: Sat, 1 Jan 2011 19:28:09 -0800 Subject: [PATCH 01/13] Initial commit of bluebox driver. --- libcloud/drivers/bluebox.py | 289 +++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 289 insertions(+), 0 deletions(-) create mode 100644 libcloud/drivers/bluebox.py diff --git a/libcloud/drivers/bluebox.py b/libcloud/drivers/bluebox.py new file mode 100644 index 0000000..b9f0ebe --- /dev/null +++ b/libcloud/drivers/bluebox.py @@ -0,0 +1,289 @@ +# 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. + +""" +Bluebox Blocks driver +""" +from libcloud.providers import Provider +from libcloud.types import NodeState, InvalidCredsError +from libcloud.base import Node, Response, ConnectionUserAndKey, NodeDriver +from libcloud.base import NodeSize, NodeImage, NodeLocation +import datetime +import hashlib +from xml.etree import ElementTree as ET + +BLUEBOX_API_HOST = "boxpanel.blueboxgrp.com" +BLUEBOX_END_POINT = "api/domains" + +# Since Bluebox doesn't provide a list of available VPS types through their +# API, we list them explicitly here. + +BLUEBOX_VPS = { + '1gb': { + 'id': '94fd37a7-2606-47f7-84d5-9000deda52ae', + 'name': 'Block 1GB Virtual Server', + 'ram': 1024, + 'disk': 20, + 'cpu': 0.5, + 'cost': 0.15 + }, + '2gb': { + 'id': 'b412f354-5056-4bf0-a42f-6ddd998aa092', + 'name': 'Block 2GB Virtual Server', + 'ram': 2048, + 'disk': 25, + 'cpu': 1, + 'cost': 0.25 + }, + '4gb': { + 'id': '0cd183d3-0287-4b1a-8288-b3ea8302ed58', + 'name': 'Block 4GB Virtual Server', + 'ram': 4096, + 'disk': 50, + 'cpu': 2, + 'cost': 0.35 + }, + '8gb': { + 'id': 'b9b87a5b-2885-4a2e-b434-44a163ca6251', + 'name': 'Block 8GB Virtual Server', + 'ram': 8192, + 'disk': 100, + 'cpu': 4, + 'cost': 0.45 + } +} + +class BlueboxResponse(Response): + + def __init__(self, response): + self.parsed = None + super(BlueboxResponse, self).__init__(response) + + def parse_body(self): + if not self.body: + return None + if not self.parsed: + self.parsed = ET.XML(self.body) + return self.parsed + + def parse_error(self): + err_list = [] + if not self.body: + return None + if not self.parsed: + self.parsed = ET.XML(self.body) + for err in self.parsed.findall('err'): + code = err.get('code') + err_list.append("(%s) %s" % (code, err.get('msg'))) + # From voxel docs: + # 1: Invalid login or password + # 9: Permission denied: user lacks access rights for this method + if code == "1" or code == "9": + # sucks, but only way to detect + # bad authentication tokens so far + raise InvalidCredsError(err_list[-1]) + return "\n".join(err_list) + + def success(self): + if not self.parsed: + self.parsed = ET.XML(self.body) + stat = self.parsed.get('stat') + if stat != "ok": + return False + return True + +class BlueboxConnection(ConnectionUserAndKey): + """ + Connection class for the Bluebox driver + """ + + host = BLUEBOX_API_HOST + responseCls = BlueboxResponse + + def add_default_params(self, params): + params["key"] = self.user_id + params["timestamp"] = datetime.datetime.utcnow().isoformat()+"+0000" + + for param in params.keys(): + if params[param] is None: + del params[param] + + keys = params.keys() + keys.sort() + + md5 = hashlib.md5() + md5.update(self.key) + for key in keys: + if params[key]: + if not params[key] is None: + md5.update("%s%s"% (key, params[key])) + else: + md5.update(key) + params['api_sig'] = md5.hexdigest() + return params + +BLUEBOX_INSTANCE_TYPES = {} +RAM_PER_CPU = 2048 + +NODE_STATE_MAP = { 'IN_PROGRESS': NodeState.PENDING, + 'SUCCEEDED': NodeState.RUNNING, + 'shutting-down': NodeState.TERMINATED, + 'terminated': NodeState.TERMINATED } + +class BlueboxNodeDriver(NodeDriver): + """ + Bluebox Blocks node driver + """ + + connectionCls = BlueboxConnection + type = Provider.BLUEBOX + name = 'Bluebox Blocks' + + def _initialize_instance_types(): + for cpus in range(1,14): + if cpus == 1: + name = "Single CPU" + else: + name = "%d CPUs" % cpus + id = "%dcpu" % cpus + ram = cpus * RAM_PER_CPU + + VOXEL_INSTANCE_TYPES[id]= { + 'id': id, + 'name': name, + 'ram': ram, + 'disk': None, + 'bandwidth': None, + 'price': None} + + features = {"create_node": [], + "list_sizes": ["variable_disk"]} + + _initialize_instance_types() + + def list_nodes(self): + params = {"method": "voxel.devices.list"} + result = self.connection.request('/', params=params).object + return self._to_nodes(result) + + def list_sizes(self, location=None): + return [ NodeSize(driver=self.connection.driver, **i) + for i in VOXEL_INSTANCE_TYPES.values() ] + + def list_images(self, location=None): + params = {"method": "voxel.images.list"} + result = self.connection.request('/', params=params).object + return self._to_images(result) + + def create_node(self, **kwargs): + raise NotImplementedError, \ + 'create_node not finished for voxel yet' + size = kwargs["size"] + cores = size.ram / RAM_PER_CPU + params = {'method': 'voxel.voxcloud.create', + 'hostname': kwargs["name"], + 'disk_size': int(kwargs["disk"])/1024, + 'processing_cores': cores, + 'facility': kwargs["location"].id, + 'image_id': kwargs["image"], + 'backend_ip': kwargs.get("privateip", None), + 'frontend_ip': kwargs.get("publicip", None), + 'admin_password': kwargs.get("rootpass", None), + 'console_password': kwargs.get("consolepass", None), + 'ssh_username': kwargs.get("sshuser", None), + 'ssh_password': kwargs.get("sshpass", None), + 'voxel_access': kwargs.get("voxel_access", None)} + + object = self.connection.request('/', params=params).object + + if self._getstatus(object): + return Node( + id = object.findtext("device/id"), + name = kwargs["name"], + state = NODE_STATE_MAP[object.findtext("devices/status")], + public_ip = kwargs.get("publicip", None), + private_ip = kwargs.get("privateip", None), + driver = self.connection.driver + ) + else: + return None + + def reboot_node(self, node): + """ + Reboot the node by passing in the node object + """ + params = {'method': 'voxel.devices.power', + 'device_id': node.id, + 'power_action': 'reboot'} + return self._getstatus(self.connection.request('/', params=params).object) + + def destroy_node(self, node): + """ + Destroy node by passing in the node object + """ + params = {'method': 'voxel.voxcloud.delete', + 'device_id': node.id} + return self._getstatus(self.connection.request('/', params=params).object) + + def list_locations(self): + params = {"method": "voxel.voxcloud.facilities.list"} + result = self.connection.request('/', params=params).object + nodes = self._to_locations(result) + return nodes + + def _getstatus(self, element): + status = element.attrib["stat"] + return status == "ok" + + + def _to_locations(self, object): + return [NodeLocation(element.attrib["label"], + element.findtext("description"), + element.findtext("description"), + self) + for element in object.findall('facilities/facility')] + + def _to_nodes(self, object): + nodes = [] + for element in object.findall('devices/device'): + if element.findtext("type") == "Virtual Server": + try: + state = self.NODE_STATE_MAP[element.attrib['status']] + except KeyError: + state = NodeState.UNKNOWN + + public_ip = private_ip = None + ipassignments = element.findall("ipassignments/ipassignment") + for ip in ipassignments: + if ip.attrib["type"] =="frontend": + public_ip = ip.text + elif ip.attrib["type"] == "backend": + private_ip = ip.text + + nodes.append(Node(id= element.attrib['id'], + name=element.attrib['label'], + state=state, + public_ip= public_ip, + private_ip= private_ip, + driver=self.connection.driver)) + return nodes + + def _to_images(self, object): + images = [] + for element in object.findall("images/image"): + images.append(NodeImage(id = element.attrib["id"], + name = element.attrib["summary"], + driver = self.connection.driver)) + return images -- 1.7.3.3 From 9f34cd677d532779a2a475a493ed105fe07d8528 Mon Sep 17 00:00:00 2001 From: Christian Paredes Date: Sat, 1 Jan 2011 20:02:52 -0800 Subject: [PATCH 02/13] More changes. --- libcloud/drivers/bluebox.py | 60 +++++++++++------------------------------- 1 files changed, 16 insertions(+), 44 deletions(-) diff --git a/libcloud/drivers/bluebox.py b/libcloud/drivers/bluebox.py index b9f0ebe..ce630f6 100644 --- a/libcloud/drivers/bluebox.py +++ b/libcloud/drivers/bluebox.py @@ -25,12 +25,11 @@ import hashlib from xml.etree import ElementTree as ET BLUEBOX_API_HOST = "boxpanel.blueboxgrp.com" -BLUEBOX_END_POINT = "api/domains" # Since Bluebox doesn't provide a list of available VPS types through their # API, we list them explicitly here. -BLUEBOX_VPS = { +BLUEBOX_INSTANCE_TYPES = { '1gb': { 'id': '94fd37a7-2606-47f7-84d5-9000deda52ae', 'name': 'Block 1GB Virtual Server', @@ -151,40 +150,16 @@ class BlueboxNodeDriver(NodeDriver): type = Provider.BLUEBOX name = 'Bluebox Blocks' - def _initialize_instance_types(): - for cpus in range(1,14): - if cpus == 1: - name = "Single CPU" - else: - name = "%d CPUs" % cpus - id = "%dcpu" % cpus - ram = cpus * RAM_PER_CPU - - VOXEL_INSTANCE_TYPES[id]= { - 'id': id, - 'name': name, - 'ram': ram, - 'disk': None, - 'bandwidth': None, - 'price': None} - - features = {"create_node": [], - "list_sizes": ["variable_disk"]} - - _initialize_instance_types() - def list_nodes(self): - params = {"method": "voxel.devices.list"} - result = self.connection.request('/', params=params).object + result = self.connection.request('/api/blocks.xml').object return self._to_nodes(result) def list_sizes(self, location=None): return [ NodeSize(driver=self.connection.driver, **i) - for i in VOXEL_INSTANCE_TYPES.values() ] + for i in BLUEBOX_INSTANCE_TYPES.values() ] def list_images(self, location=None): - params = {"method": "voxel.images.list"} - result = self.connection.request('/', params=params).object + result = self.connection.request('/api/block_templates.xml').object return self._to_images(result) def create_node(self, **kwargs): @@ -192,21 +167,18 @@ class BlueboxNodeDriver(NodeDriver): 'create_node not finished for voxel yet' size = kwargs["size"] cores = size.ram / RAM_PER_CPU - params = {'method': 'voxel.voxcloud.create', - 'hostname': kwargs["name"], - 'disk_size': int(kwargs["disk"])/1024, - 'processing_cores': cores, - 'facility': kwargs["location"].id, - 'image_id': kwargs["image"], - 'backend_ip': kwargs.get("privateip", None), - 'frontend_ip': kwargs.get("publicip", None), - 'admin_password': kwargs.get("rootpass", None), - 'console_password': kwargs.get("consolepass", None), - 'ssh_username': kwargs.get("sshuser", None), - 'ssh_password': kwargs.get("sshpass", None), - 'voxel_access': kwargs.get("voxel_access", None)} - - object = self.connection.request('/', params=params).object + params = { + 'product': kwargs["product"], + 'template': kwargs["template"], + 'password': kwargs["password"], + 'ssh_key': kwargs["ssh_key"], + 'username': kwargs["username"] + } + + if params['username'] == "": + params['username'] = "deploy" + + object = self.connection.request('/api/blocks.xml', params=params, method='POST').object if self._getstatus(object): return Node( -- 1.7.3.3 From 97466aa0c71adb976d65fcac366d7303dc56141b Mon Sep 17 00:00:00 2001 From: Christian Paredes Date: Sat, 1 Jan 2011 21:05:03 -0800 Subject: [PATCH 03/13] Finished up a lot of changes, need to test with test suite. --- libcloud/drivers/bluebox.py | 82 +++++++++++++++++-------------------------- 1 files changed, 32 insertions(+), 50 deletions(-) diff --git a/libcloud/drivers/bluebox.py b/libcloud/drivers/bluebox.py index ce630f6..25189a5 100644 --- a/libcloud/drivers/bluebox.py +++ b/libcloud/drivers/bluebox.py @@ -112,7 +112,7 @@ class BlueboxConnection(ConnectionUserAndKey): responseCls = BlueboxResponse def add_default_params(self, params): - params["key"] = self.user_id + params["customer_id"] = self.customer_id params["timestamp"] = datetime.datetime.utcnow().isoformat()+"+0000" for param in params.keys(): @@ -136,10 +136,11 @@ class BlueboxConnection(ConnectionUserAndKey): BLUEBOX_INSTANCE_TYPES = {} RAM_PER_CPU = 2048 -NODE_STATE_MAP = { 'IN_PROGRESS': NodeState.PENDING, - 'SUCCEEDED': NodeState.RUNNING, - 'shutting-down': NodeState.TERMINATED, - 'terminated': NodeState.TERMINATED } +NODE_STATE_MAP = { 'queued': NodeState.PENDING, + 'building': NodeState.PENDING, + 'running': NodeState.RUNNING, + 'error': NodeState.TERMINATED, + 'unknown': NodeState.UNKNOWN } class BlueboxNodeDriver(NodeDriver): """ @@ -182,80 +183,61 @@ class BlueboxNodeDriver(NodeDriver): if self._getstatus(object): return Node( - id = object.findtext("device/id"), - name = kwargs["name"], - state = NODE_STATE_MAP[object.findtext("devices/status")], - public_ip = kwargs.get("publicip", None), - private_ip = kwargs.get("privateip", None), + id = object.findtext("hash/id"), + hostname = object.findtext("hash/hostname"), + state = NODE_STATE_MAP[object.findtext("hash/status")], + public_ip = object.findtext("hash/ips/ip"), driver = self.connection.driver ) else: return None - def reboot_node(self, node): - """ - Reboot the node by passing in the node object - """ - params = {'method': 'voxel.devices.power', - 'device_id': node.id, - 'power_action': 'reboot'} - return self._getstatus(self.connection.request('/', params=params).object) - def destroy_node(self, node): """ Destroy node by passing in the node object """ - params = {'method': 'voxel.voxcloud.delete', - 'device_id': node.id} - return self._getstatus(self.connection.request('/', params=params).object) + return self._getstatus(self.connection.request("/api/blocks/#{node.id}.xml", method='DELETE').object) def list_locations(self): - params = {"method": "voxel.voxcloud.facilities.list"} - result = self.connection.request('/', params=params).object - nodes = self._to_locations(result) - return nodes + raise NotImplementedError, \ + 'list_locations not finished for bluebox yet' def _getstatus(self, element): status = element.attrib["stat"] return status == "ok" - def _to_locations(self, object): - return [NodeLocation(element.attrib["label"], - element.findtext("description"), - element.findtext("description"), - self) - for element in object.findall('facilities/facility')] +# def _to_locations(self, object): +# return [NodeLocation(element.attrib["label"], +# element.findtext("description"), +# element.findtext("description"), +# self) +# for element in object.findall('facilities/facility')] def _to_nodes(self, object): nodes = [] - for element in object.findall('devices/device'): - if element.findtext("type") == "Virtual Server": - try: - state = self.NODE_STATE_MAP[element.attrib['status']] - except KeyError: - state = NodeState.UNKNOWN - - public_ip = private_ip = None - ipassignments = element.findall("ipassignments/ipassignment") - for ip in ipassignments: - if ip.attrib["type"] =="frontend": - public_ip = ip.text - elif ip.attrib["type"] == "backend": - private_ip = ip.text + for element in object.findall('records/record'): + try: + state = self.NODE_STATE_MAP[element.attrib['status']] + except KeyError: + state = NodeState.UNKNOWN + + public_ip = None + ips = element.findall("ips") + for ip in ips: + public_ip = ip.findtext("address") nodes.append(Node(id= element.attrib['id'], - name=element.attrib['label'], + name=element.attrib['hostname'], state=state, public_ip= public_ip, - private_ip= private_ip, driver=self.connection.driver)) return nodes def _to_images(self, object): images = [] - for element in object.findall("images/image"): + for element in object.findall("records/record"): images.append(NodeImage(id = element.attrib["id"], - name = element.attrib["summary"], + name = element.attrib["description"], driver = self.connection.driver)) return images -- 1.7.3.3 From 595e266a79f206a846a74c920829197a77c3839b Mon Sep 17 00:00:00 2001 From: Christian Paredes Date: Sat, 1 Jan 2011 21:22:47 -0800 Subject: [PATCH 04/13] Changing voxel's test suite for blue box. --- test/secrets.py-dist | 3 ++ test/test_bluebox.py | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 0 deletions(-) create mode 100644 test/test_bluebox.py diff --git a/test/secrets.py-dist b/test/secrets.py-dist index de78fef..6398e8e 100644 --- a/test/secrets.py-dist +++ b/test/secrets.py-dist @@ -58,3 +58,6 @@ OPENNEBULA_USER='' OPENNEBULA_KEY='' DREAMHOST_KEY='' + +BLUEBOX_CUSTOMER_ID='' +BLUEBOX_API_KEY='' diff --git a/test/test_bluebox.py b/test/test_bluebox.py new file mode 100644 index 0000000..5052189 --- /dev/null +++ b/test/test_bluebox.py @@ -0,0 +1,54 @@ +# 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. +import sys +import unittest + +from libcloud.drivers.voxel import VoxelNodeDriver as Voxel +from libcloud.types import InvalidCredsError + +import httplib + +from test import MockHttp +from test.file_fixtures import FileFixtures + +from secrets import BLUEBOX_CUSTOMER_ID, BLUEBOX_API_KEY + +class BlueboxTest(unittest.TestCase): + + def setUp(self): + + bluebox.connectionCls.conn_classes = (None, BlueboxMockHttp) + BlueboxMockHttp.type = None + self.driver = Bluebox(BLUEBOX_CUSTOMER_ID, BLUEBOX_API_KEY) + + def test_auth_failed(self): + BlueboxMockHttp.type = 'UNAUTHORIZED' + try: + self.driver.list_nodes() + except Exception, e: + self.assertTrue(isinstance(e, InvalidCredsError)) + else: + self.fail('test should have thrown') + +class BlueboxMockHttp(MockHttp): + + fixtures = FileFixtures('bluebox') + + def _UNAUTHORIZED(self, method, url, body, headers): + body = self.fixtures.load('unauthorized.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + +if __name__ == '__main__': + sys.exit(unittest.main()) -- 1.7.3.3 From 26572daf23a0b02fcf0ff6b0be4757d4ca2e96eb Mon Sep 17 00:00:00 2001 From: Christian Paredes Date: Sat, 1 Jan 2011 21:30:18 -0800 Subject: [PATCH 05/13] Adding Bluebox provider --- libcloud/providers.py | 2 ++ 1 files changed, 2 insertions(+), 0 deletions(-) diff --git a/libcloud/providers.py b/libcloud/providers.py index b073536..ff901e0 100644 --- a/libcloud/providers.py +++ b/libcloud/providers.py @@ -67,6 +67,8 @@ DRIVERS = { ('libcloud.drivers.dreamhost', 'DreamhostNodeDriver'), Provider.BRIGHTBOX: ('libcloud.drivers.brightbox', 'BrightboxNodeDriver'), + Provider.BLUEBOX: + ('libcloud.drivers.bluebox', 'BlueboxNodeDriver'), } def get_driver(provider): -- 1.7.3.3 From 35b352ed9be734316d5fa3f61cdd8569f713ecb0 Mon Sep 17 00:00:00 2001 From: Christian Paredes Date: Sat, 1 Jan 2011 21:55:01 -0800 Subject: [PATCH 06/13] Adding BLUEBOX type. --- libcloud/types.py | 2 ++ test/test_bluebox.py | 2 +- 2 files changed, 3 insertions(+), 1 deletions(-) diff --git a/libcloud/types.py b/libcloud/types.py index cdf32e7..29df04d 100644 --- a/libcloud/types.py +++ b/libcloud/types.py @@ -37,6 +37,7 @@ class Provider(object): @cvar OPENNEBULA: OpenNebula.org @cvar DREAMHOST: DreamHost Private Server @cvar CLOUDSIGMA: CloudSigma + @cvar BLUEBOX: Bluebox Blocks """ DUMMY = 0 EC2 = 1 # deprecated name @@ -66,6 +67,7 @@ class Provider(object): RACKSPACE_UK = 23 BRIGHTBOX = 24 CLOUDSIGMA = 25 + BLUEBOX = 26 class NodeState(object): """ diff --git a/test/test_bluebox.py b/test/test_bluebox.py index 5052189..2eb7d5d 100644 --- a/test/test_bluebox.py +++ b/test/test_bluebox.py @@ -15,7 +15,7 @@ import sys import unittest -from libcloud.drivers.voxel import VoxelNodeDriver as Voxel +from libcloud.drivers.bluebox import BlueboxNodeDriver as Bluebox from libcloud.types import InvalidCredsError import httplib -- 1.7.3.3 From 1b3ea1c84612c3fea4ecf67a3d698e32786f4ee6 Mon Sep 17 00:00:00 2001 From: Christian Paredes Date: Sat, 1 Jan 2011 21:56:06 -0800 Subject: [PATCH 07/13] Adding bluebox name. --- libcloud/drivers/__init__.py | 1 + 1 files changed, 1 insertions(+), 0 deletions(-) diff --git a/libcloud/drivers/__init__.py b/libcloud/drivers/__init__.py index b95d6be..71b445b 100644 --- a/libcloud/drivers/__init__.py +++ b/libcloud/drivers/__init__.py @@ -18,6 +18,7 @@ Drivers for working with different providers """ __all__ = [ + 'bluebox', 'brightbox', 'dummy', 'ec2', -- 1.7.3.3 From 5e5df24661b8c91b5fb5167028fef1e05788427e Mon Sep 17 00:00:00 2001 From: Christian Paredes Date: Sun, 2 Jan 2011 14:18:34 -0800 Subject: [PATCH 08/13] Fixed up some stuff. Still gotta test! Wooo --- libcloud/drivers/bluebox.py | 81 +++++++++++--------------------- test/fixtures/bluebox/unauthorized.xml | 5 ++ test/test_bluebox.py | 12 +++-- 3 files changed, 39 insertions(+), 59 deletions(-) create mode 100644 test/fixtures/bluebox/unauthorized.xml diff --git a/libcloud/drivers/bluebox.py b/libcloud/drivers/bluebox.py index 25189a5..86ceaa0 100644 --- a/libcloud/drivers/bluebox.py +++ b/libcloud/drivers/bluebox.py @@ -22,6 +22,7 @@ from libcloud.base import Node, Response, ConnectionUserAndKey, NodeDriver from libcloud.base import NodeSize, NodeImage, NodeLocation import datetime import hashlib +import base64 from xml.etree import ElementTree as ET BLUEBOX_API_HOST = "boxpanel.blueboxgrp.com" @@ -66,42 +67,30 @@ BLUEBOX_INSTANCE_TYPES = { class BlueboxResponse(Response): - def __init__(self, response): - self.parsed = None - super(BlueboxResponse, self).__init__(response) +# def __init__(self, response): +# self.parsed = None +# super(BlueboxResponse, self).__init__(response) def parse_body(self): if not self.body: return None - if not self.parsed: - self.parsed = ET.XML(self.body) - return self.parsed + return ET.XML(self.body) def parse_error(self): - err_list = [] - if not self.body: - return None - if not self.parsed: - self.parsed = ET.XML(self.body) - for err in self.parsed.findall('err'): - code = err.get('code') - err_list.append("(%s) %s" % (code, err.get('msg'))) - # From voxel docs: - # 1: Invalid login or password - # 9: Permission denied: user lacks access rights for this method - if code == "1" or code == "9": - # sucks, but only way to detect - # bad authentication tokens so far - raise InvalidCredsError(err_list[-1]) - return "\n".join(err_list) - - def success(self): - if not self.parsed: - self.parsed = ET.XML(self.body) - stat = self.parsed.get('stat') - if stat != "ok": - return False - return True + if int(self.status) == 401: + if not self.body: + raise InvalidCredsError(str(self.status) + ': ' + self.error) + else: + raise InvalidCredsError(self.body) + return self.body + + #def success(self): + # if not self.parsed: + # self.parsed = ET.XML(self.body) + # stat = self.parsed.get('stat') + # if stat != "ok": + # return False + # return True class BlueboxConnection(ConnectionUserAndKey): """ @@ -109,29 +98,13 @@ class BlueboxConnection(ConnectionUserAndKey): """ host = BLUEBOX_API_HOST + secure = True responseCls = BlueboxResponse - def add_default_params(self, params): - params["customer_id"] = self.customer_id - params["timestamp"] = datetime.datetime.utcnow().isoformat()+"+0000" - - for param in params.keys(): - if params[param] is None: - del params[param] - - keys = params.keys() - keys.sort() - - md5 = hashlib.md5() - md5.update(self.key) - for key in keys: - if params[key]: - if not params[key] is None: - md5.update("%s%s"% (key, params[key])) - else: - md5.update(key) - params['api_sig'] = md5.hexdigest() - return params + def add_default_headers(self, headers): + user_b64 = base64.b64encode('%s:%s' % (self.user_id, self.key)) + headers['Authorization'] = 'Basic %s' % (user_b64) + return headers BLUEBOX_INSTANCE_TYPES = {} RAM_PER_CPU = 2048 @@ -152,7 +125,7 @@ class BlueboxNodeDriver(NodeDriver): name = 'Bluebox Blocks' def list_nodes(self): - result = self.connection.request('/api/blocks.xml').object + result = self.connection.request('/api/blocks.xml', method='GET').object return self._to_nodes(result) def list_sizes(self, location=None): @@ -160,7 +133,7 @@ class BlueboxNodeDriver(NodeDriver): for i in BLUEBOX_INSTANCE_TYPES.values() ] def list_images(self, location=None): - result = self.connection.request('/api/block_templates.xml').object + result = self.connection.request('/api/block_templates.xml', method='GET').object return self._to_images(result) def create_node(self, **kwargs): @@ -179,7 +152,7 @@ class BlueboxNodeDriver(NodeDriver): if params['username'] == "": params['username'] = "deploy" - object = self.connection.request('/api/blocks.xml', params=params, method='POST').object + object = self.connection.request('/api/blocks.xml', method='POST').object if self._getstatus(object): return Node( diff --git a/test/fixtures/bluebox/unauthorized.xml b/test/fixtures/bluebox/unauthorized.xml new file mode 100644 index 0000000..8ece6af --- /dev/null +++ b/test/fixtures/bluebox/unauthorized.xml @@ -0,0 +1,5 @@ + + + API Error. Action unccessful. + 409 + diff --git a/test/test_bluebox.py b/test/test_bluebox.py index 2eb7d5d..3e3b1ad 100644 --- a/test/test_bluebox.py +++ b/test/test_bluebox.py @@ -29,16 +29,18 @@ class BlueboxTest(unittest.TestCase): def setUp(self): - bluebox.connectionCls.conn_classes = (None, BlueboxMockHttp) + Bluebox.connectionCls.conn_classes = (None, BlueboxMockHttp) BlueboxMockHttp.type = None self.driver = Bluebox(BLUEBOX_CUSTOMER_ID, BLUEBOX_API_KEY) - def test_auth_failed(self): + def test_auth(self): BlueboxMockHttp.type = 'UNAUTHORIZED' + try: self.driver.list_nodes() - except Exception, e: + except InvalidCredsError, e: self.assertTrue(isinstance(e, InvalidCredsError)) + self.assertEquals(e.value, '401: Unauthorized') else: self.fail('test should have thrown') @@ -46,9 +48,9 @@ class BlueboxMockHttp(MockHttp): fixtures = FileFixtures('bluebox') - def _UNAUTHORIZED(self, method, url, body, headers): + def _api_blocks_xml_UNAUTHORIZED(self, method, url, body, headers): body = self.fixtures.load('unauthorized.xml') - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + return (httplib.UNAUTHORIZED, body, {}, httplib.responses[httplib.UNAUTHORIZED]) if __name__ == '__main__': sys.exit(unittest.main()) -- 1.7.3.3 From 05d75f70fc0257d730ac8ca332b76fb1a2e75ee8 Mon Sep 17 00:00:00 2001 From: Christian Paredes Date: Sat, 8 Jan 2011 18:21:26 -0800 Subject: [PATCH 09/13] Converted everything to handle json. Still working on test framework. --- libcloud/drivers/bluebox.py | 116 +++++++++++++++++++------------------------ test/test_bluebox.py | 41 ++++++++++------ 2 files changed, 78 insertions(+), 79 deletions(-) diff --git a/libcloud/drivers/bluebox.py b/libcloud/drivers/bluebox.py index 86ceaa0..62543a4 100644 --- a/libcloud/drivers/bluebox.py +++ b/libcloud/drivers/bluebox.py @@ -23,7 +23,9 @@ from libcloud.base import NodeSize, NodeImage, NodeLocation import datetime import hashlib import base64 -from xml.etree import ElementTree as ET + +try: import json +except: import simplejson as json BLUEBOX_API_HOST = "boxpanel.blueboxgrp.com" @@ -72,9 +74,11 @@ class BlueboxResponse(Response): # super(BlueboxResponse, self).__init__(response) def parse_body(self): - if not self.body: - return None - return ET.XML(self.body) + try: + js = json.loads(self.body) + return js + except ValueError: + return self.body def parse_error(self): if int(self.status) == 401: @@ -125,20 +129,23 @@ class BlueboxNodeDriver(NodeDriver): name = 'Bluebox Blocks' def list_nodes(self): - result = self.connection.request('/api/blocks.xml', method='GET').object - return self._to_nodes(result) + result = self.connection.request('/api/blocks.json') + return [self._to_node(i) for i in result.object] def list_sizes(self, location=None): return [ NodeSize(driver=self.connection.driver, **i) for i in BLUEBOX_INSTANCE_TYPES.values() ] def list_images(self, location=None): - result = self.connection.request('/api/block_templates.xml', method='GET').object - return self._to_images(result) + result = self.connection.request('/api/block_templates.json') + images = [] + for image in result.object: + images.extend([self._to_image(image)]) + + return images def create_node(self, **kwargs): - raise NotImplementedError, \ - 'create_node not finished for voxel yet' + headers = { 'Content-Type': 'application/json' } size = kwargs["size"] cores = size.ram / RAM_PER_CPU params = { @@ -152,65 +159,46 @@ class BlueboxNodeDriver(NodeDriver): if params['username'] == "": params['username'] = "deploy" - object = self.connection.request('/api/blocks.xml', method='POST').object + if kwargs["hostname"]: + params['hostname'] = kwargs["hostname"] - if self._getstatus(object): - return Node( - id = object.findtext("hash/id"), - hostname = object.findtext("hash/hostname"), - state = NODE_STATE_MAP[object.findtext("hash/status")], - public_ip = object.findtext("hash/ips/ip"), - driver = self.connection.driver - ) - else: - return None + result = self.connection.request('/api/blocks.json', data=json.dumps(request), headers=headers, method='POST') + node = self._to_node(result.object) + return node def destroy_node(self, node): """ Destroy node by passing in the node object """ - return self._getstatus(self.connection.request("/api/blocks/#{node.id}.xml", method='DELETE').object) + result = self.connection.request("/api/blocks/#{node.id}.json", method='DELETE') + + return result.status == 200 def list_locations(self): - raise NotImplementedError, \ - 'list_locations not finished for bluebox yet' - - def _getstatus(self, element): - status = element.attrib["stat"] - return status == "ok" - - -# def _to_locations(self, object): -# return [NodeLocation(element.attrib["label"], -# element.findtext("description"), -# element.findtext("description"), -# self) -# for element in object.findall('facilities/facility')] - - def _to_nodes(self, object): - nodes = [] - for element in object.findall('records/record'): - try: - state = self.NODE_STATE_MAP[element.attrib['status']] - except KeyError: - state = NodeState.UNKNOWN - - public_ip = None - ips = element.findall("ips") - for ip in ips: - public_ip = ip.findtext("address") - - nodes.append(Node(id= element.attrib['id'], - name=element.attrib['hostname'], - state=state, - public_ip= public_ip, - driver=self.connection.driver)) - return nodes - - def _to_images(self, object): - images = [] - for element in object.findall("records/record"): - images.append(NodeImage(id = element.attrib["id"], - name = element.attrib["description"], - driver = self.connection.driver)) - return images + return [NodeLocation(0, "Blue Box Seattle US", 'US', self)] + + def reboot_node(self, node): + result = self.connection.request("/api/blocks/#{node.id}/reboot.json", method="PUT") + node = self._to_node(result.object) + return result.status == 200 + + def _to_node(self, vm): + if vm['status'] == "running": + state = NodeState.RUNNING + else: + state = NodeState.PENDING + + n = Node(id=vm['id'], + name=vm['hostname'], + state=state, + public_ip=[ i['address'] for i in vm['ips'] ], + private_ip=[], + extra={'storage':vm['storage'], 'cpu':vm['cpu']}, + driver=self.connection.driver) + return n + + def _to_image(self, image): + image = NodeImage(id=image['id'], + name=image['description'], + driver=self.connection.driver) + return image diff --git a/test/test_bluebox.py b/test/test_bluebox.py index 3e3b1ad..c96d8c2 100644 --- a/test/test_bluebox.py +++ b/test/test_bluebox.py @@ -14,9 +14,11 @@ # limitations under the License. import sys import unittest +import exceptions from libcloud.drivers.bluebox import BlueboxNodeDriver as Bluebox -from libcloud.types import InvalidCredsError +from libcloud.base import Node +from libcloud.types import NodeState import httplib @@ -28,29 +30,38 @@ from secrets import BLUEBOX_CUSTOMER_ID, BLUEBOX_API_KEY class BlueboxTest(unittest.TestCase): def setUp(self): - Bluebox.connectionCls.conn_classes = (None, BlueboxMockHttp) - BlueboxMockHttp.type = None self.driver = Bluebox(BLUEBOX_CUSTOMER_ID, BLUEBOX_API_KEY) - def test_auth(self): - BlueboxMockHttp.type = 'UNAUTHORIZED' + def test_create_node(self): + node = self.driver.create_node( + product='94fd37a7-2606-47f7-84d5-9000deda52ae', + template='c66b8145-f768-45ef-9878-395bf8b1b7ff', + password='testpass', + username='deploy', + hostname='foo' + ) + self.assertEqual(node.name, 'foo') + + def test_list_nodes(self): + node = self.driver.list_nodes()[0] + self.assertEqual(node.name, 'foo') + self.assertEqual(node.state, NodeState.RUNNING) + + def test_reboot_node(self): + node = self.driver.list_nodes()[0] + ret = self.driver.reboot_node(node) + self.assertTrue(ret) - try: - self.driver.list_nodes() - except InvalidCredsError, e: - self.assertTrue(isinstance(e, InvalidCredsError)) - self.assertEquals(e.value, '401: Unauthorized') - else: - self.fail('test should have thrown') + def test_destroy_node(self): + node = self.driver.list_nodes()[0] + ret = self.driver.destroy_node(node) + self.assertTrue(ret) class BlueboxMockHttp(MockHttp): fixtures = FileFixtures('bluebox') - def _api_blocks_xml_UNAUTHORIZED(self, method, url, body, headers): - body = self.fixtures.load('unauthorized.xml') - return (httplib.UNAUTHORIZED, body, {}, httplib.responses[httplib.UNAUTHORIZED]) if __name__ == '__main__': sys.exit(unittest.main()) -- 1.7.3.3 From 1fc8a0352c8d63166a7da0e033740323d7c3633b Mon Sep 17 00:00:00 2001 From: Christian Paredes Date: Sat, 8 Jan 2011 20:47:39 -0800 Subject: [PATCH 10/13] Got it working! --- libcloud/drivers/bluebox.py | 89 +++++++++++++++++++++++++++---------------- test/test_bluebox.py | 12 ++++++ 2 files changed, 68 insertions(+), 33 deletions(-) diff --git a/libcloud/drivers/bluebox.py b/libcloud/drivers/bluebox.py index 62543a4..5d78279 100644 --- a/libcloud/drivers/bluebox.py +++ b/libcloud/drivers/bluebox.py @@ -20,8 +20,10 @@ from libcloud.providers import Provider from libcloud.types import NodeState, InvalidCredsError from libcloud.base import Node, Response, ConnectionUserAndKey, NodeDriver from libcloud.base import NodeSize, NodeImage, NodeLocation +from libcloud.base import NodeAuthPassword, NodeAuthSSHKey import datetime import hashlib +import urllib import base64 try: import json @@ -39,7 +41,7 @@ BLUEBOX_INSTANCE_TYPES = { 'ram': 1024, 'disk': 20, 'cpu': 0.5, - 'cost': 0.15 + 'price': 0.15 }, '2gb': { 'id': 'b412f354-5056-4bf0-a42f-6ddd998aa092', @@ -47,7 +49,7 @@ BLUEBOX_INSTANCE_TYPES = { 'ram': 2048, 'disk': 25, 'cpu': 1, - 'cost': 0.25 + 'price': 0.25 }, '4gb': { 'id': '0cd183d3-0287-4b1a-8288-b3ea8302ed58', @@ -55,7 +57,7 @@ BLUEBOX_INSTANCE_TYPES = { 'ram': 4096, 'disk': 50, 'cpu': 2, - 'cost': 0.35 + 'price': 0.35 }, '8gb': { 'id': 'b9b87a5b-2885-4a2e-b434-44a163ca6251', @@ -63,7 +65,7 @@ BLUEBOX_INSTANCE_TYPES = { 'ram': 8192, 'disk': 100, 'cpu': 4, - 'cost': 0.45 + 'price': 0.45 } } @@ -88,13 +90,19 @@ class BlueboxResponse(Response): raise InvalidCredsError(self.body) return self.body - #def success(self): - # if not self.parsed: - # self.parsed = ET.XML(self.body) - # stat = self.parsed.get('stat') - # if stat != "ok": - # return False - # return True +class BlueboxNodeSize(NodeSize): + def __init__(self, id, name, cpu, ram, disk, price, driver): + self.id = id + self.name = name + self.cpu = cpu + self.ram = ram + self.disk = disk + self.price = price + self.driver = driver + + def __repr__(self): + return (('') + % (self.id, self.name, self.cpu, self.ram, self.disk, self.price, self.driver.name)) class BlueboxConnection(ConnectionUserAndKey): """ @@ -110,7 +118,6 @@ class BlueboxConnection(ConnectionUserAndKey): headers['Authorization'] = 'Basic %s' % (user_b64) return headers -BLUEBOX_INSTANCE_TYPES = {} RAM_PER_CPU = 2048 NODE_STATE_MAP = { 'queued': NodeState.PENDING, @@ -133,7 +140,7 @@ class BlueboxNodeDriver(NodeDriver): return [self._to_node(i) for i in result.object] def list_sizes(self, location=None): - return [ NodeSize(driver=self.connection.driver, **i) + return [ BlueboxNodeSize(driver=self.connection.driver, **i) for i in BLUEBOX_INSTANCE_TYPES.values() ] def list_images(self, location=None): @@ -145,24 +152,39 @@ class BlueboxNodeDriver(NodeDriver): return images def create_node(self, **kwargs): - headers = { 'Content-Type': 'application/json' } + headers = { 'Content-Type': 'application/x-www-form-urlencoded' } size = kwargs["size"] cores = size.ram / RAM_PER_CPU - params = { - 'product': kwargs["product"], - 'template': kwargs["template"], - 'password': kwargs["password"], - 'ssh_key': kwargs["ssh_key"], - 'username': kwargs["username"] + + name = kwargs['name'] + image = kwargs['image'] + size = kwargs['size'] + auth = kwargs['auth'] + + data = { + 'hostname': name, + 'product': size.id, + 'template': image.id } - if params['username'] == "": - params['username'] = "deploy" + ssh = None + password = None + + if isinstance(auth, NodeAuthSSHKey): + ssh = auth.pubkey + data.update(ssh_public_key=ssh) + elif isinstance(auth, NodeAuthPassword): + password = auth.password + data.update(password=password) - if kwargs["hostname"]: - params['hostname'] = kwargs["hostname"] + if "ex_username" in kwargs: + data.update(username=kwargs["ex_username"]) - result = self.connection.request('/api/blocks.json', data=json.dumps(request), headers=headers, method='POST') + if not ssh and not password: + raise Exception("SSH public key or password required.") + + params = urllib.urlencode(data) + result = self.connection.request('/api/blocks.json', headers=headers, data=params, method='POST') node = self._to_node(result.object) return node @@ -170,7 +192,8 @@ class BlueboxNodeDriver(NodeDriver): """ Destroy node by passing in the node object """ - result = self.connection.request("/api/blocks/#{node.id}.json", method='DELETE') + url = '/api/blocks/%s.json' % (node.id) + result = self.connection.request(url, method='DELETE') return result.status == 200 @@ -178,20 +201,20 @@ class BlueboxNodeDriver(NodeDriver): return [NodeLocation(0, "Blue Box Seattle US", 'US', self)] def reboot_node(self, node): - result = self.connection.request("/api/blocks/#{node.id}/reboot.json", method="PUT") + url = '/api/blocks/%s/reboot.json' % (node.id) + result = self.connection.request(url, method="PUT") node = self._to_node(result.object) return result.status == 200 def _to_node(self, vm): - if vm['status'] == "running": - state = NodeState.RUNNING - else: - state = NodeState.PENDING - + try: + state = NODE_STATE_MAP[vm['status']] + except KeyError: + state = NodeState.UNKNOWN n = Node(id=vm['id'], name=vm['hostname'], state=state, - public_ip=[ i['address'] for i in vm['ips'] ], + public_ip=[ ip['address'] for ip in vm['ips'] ], private_ip=[], extra={'storage':vm['storage'], 'cpu':vm['cpu']}, driver=self.connection.driver) diff --git a/test/test_bluebox.py b/test/test_bluebox.py index c96d8c2..8b786e1 100644 --- a/test/test_bluebox.py +++ b/test/test_bluebox.py @@ -34,6 +34,7 @@ class BlueboxTest(unittest.TestCase): self.driver = Bluebox(BLUEBOX_CUSTOMER_ID, BLUEBOX_API_KEY) def test_create_node(self): + BlueboxMockHttp.type = 'create' node = self.driver.create_node( product='94fd37a7-2606-47f7-84d5-9000deda52ae', template='c66b8145-f768-45ef-9878-395bf8b1b7ff', @@ -41,20 +42,28 @@ class BlueboxTest(unittest.TestCase): username='deploy', hostname='foo' ) + self.assertTrue(isinstance(node, Node)) self.assertEqual(node.name, 'foo') def test_list_nodes(self): + BlueboxMockHttp.type = 'list' node = self.driver.list_nodes()[0] self.assertEqual(node.name, 'foo') self.assertEqual(node.state, NodeState.RUNNING) def test_reboot_node(self): + BlueboxMockHttp.type = 'list' node = self.driver.list_nodes()[0] + + BlueboxMockHttp.type = 'reboot' ret = self.driver.reboot_node(node) self.assertTrue(ret) def test_destroy_node(self): + BlueboxMockHttp.type = 'list' node = self.driver.list_nodes()[0] + + BlueboxMockHttp.type = 'delete' ret = self.driver.destroy_node(node) self.assertTrue(ret) @@ -62,6 +71,9 @@ class BlueboxMockHttp(MockHttp): fixtures = FileFixtures('bluebox') + def _api_blocks_json_list(self, method, url, body, headers): + body = """[]""" + return (httplib.OK, body, headers, httplib.responses[httplib.OK]) if __name__ == '__main__': sys.exit(unittest.main()) -- 1.7.3.3 From 05087a9a589d2644d840d7b982d6f5a728b1991c Mon Sep 17 00:00:00 2001 From: Christian Paredes Date: Sun, 9 Jan 2011 17:40:03 -0800 Subject: [PATCH 11/13] Adding try/except clause for auth var assignment. --- libcloud/drivers/bluebox.py | 3 ++- 1 files changed, 2 insertions(+), 1 deletions(-) diff --git a/libcloud/drivers/bluebox.py b/libcloud/drivers/bluebox.py index 5d78279..44600c7 100644 --- a/libcloud/drivers/bluebox.py +++ b/libcloud/drivers/bluebox.py @@ -159,7 +159,8 @@ class BlueboxNodeDriver(NodeDriver): name = kwargs['name'] image = kwargs['image'] size = kwargs['size'] - auth = kwargs['auth'] + try: auth = kwargs['auth'] + except: raise Exception("SSH public key or password required.") data = { 'hostname': name, -- 1.7.3.3 From ec1c27d69e2728947eb94639ae414f7ee5ff27f6 Mon Sep 17 00:00:00 2001 From: Christian Paredes Date: Sun, 9 Jan 2011 20:58:00 -0800 Subject: [PATCH 12/13] Tests written up - API passed all tests. --- libcloud/drivers/bluebox.py | 46 +++++++------ test/fixtures/bluebox/api_block_products_json.json | 1 + .../fixtures/bluebox/api_block_templates_json.json | 1 + ..._99df878c_6e5c_4945_a635_d94da9fd3146_json.json | 1 + ...8c_6e5c_4945_a635_d94da9fd3146_json_delete.json | 1 + ...8c_6e5c_4945_a635_d94da9fd3146_reboot_json.json | 1 + test/fixtures/bluebox/api_blocks_json.json | 1 + test/fixtures/bluebox/api_blocks_json_post.json | 1 + test/fixtures/bluebox/unauthorized.xml | 5 -- test/test_bluebox.py | 68 ++++++++++++++----- 10 files changed, 82 insertions(+), 44 deletions(-) create mode 100644 test/fixtures/bluebox/api_block_products_json.json create mode 100644 test/fixtures/bluebox/api_block_templates_json.json create mode 100644 test/fixtures/bluebox/api_blocks_99df878c_6e5c_4945_a635_d94da9fd3146_json.json create mode 100644 test/fixtures/bluebox/api_blocks_99df878c_6e5c_4945_a635_d94da9fd3146_json_delete.json create mode 100644 test/fixtures/bluebox/api_blocks_99df878c_6e5c_4945_a635_d94da9fd3146_reboot_json.json create mode 100644 test/fixtures/bluebox/api_blocks_json.json create mode 100644 test/fixtures/bluebox/api_blocks_json_post.json delete mode 100644 test/fixtures/bluebox/unauthorized.xml diff --git a/libcloud/drivers/bluebox.py b/libcloud/drivers/bluebox.py index 44600c7..a84924d 100644 --- a/libcloud/drivers/bluebox.py +++ b/libcloud/drivers/bluebox.py @@ -14,7 +14,14 @@ # limitations under the License. """ -Bluebox Blocks driver +libcloud driver for the Blue Box Blocks API + +This driver implements all libcloud functionality for the Blue Box Blocks API. + +Blue Box home page http://bluebox.net +Blue Box API documentation https://boxpanel.bluebox.net/public/the_vault/index.php/Blocks_API + +Maintainer: Christian Paredes """ from libcloud.providers import Provider from libcloud.types import NodeState, InvalidCredsError @@ -26,13 +33,18 @@ import hashlib import urllib import base64 +# 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 -BLUEBOX_API_HOST = "boxpanel.blueboxgrp.com" +# Current end point for Blue Box API. +BLUEBOX_API_HOST = "boxpanel.bluebox.net" -# Since Bluebox doesn't provide a list of available VPS types through their -# API, we list them explicitly here. +# The API doesn't currently expose all of the required values for libcloud, +# so we simply list what's available right now, along with all of the various +# attributes that are needed by libcloud. BLUEBOX_INSTANCE_TYPES = { '1gb': { @@ -69,12 +81,15 @@ BLUEBOX_INSTANCE_TYPES = { } } -class BlueboxResponse(Response): +RAM_PER_CPU = 2048 -# def __init__(self, response): -# self.parsed = None -# super(BlueboxResponse, self).__init__(response) +NODE_STATE_MAP = { 'queued': NodeState.PENDING, + 'building': NodeState.PENDING, + 'running': NodeState.RUNNING, + 'error': NodeState.TERMINATED, + 'unknown': NodeState.UNKNOWN } +class BlueboxResponse(Response): def parse_body(self): try: js = json.loads(self.body) @@ -118,14 +133,6 @@ class BlueboxConnection(ConnectionUserAndKey): headers['Authorization'] = 'Basic %s' % (user_b64) return headers -RAM_PER_CPU = 2048 - -NODE_STATE_MAP = { 'queued': NodeState.PENDING, - 'building': NodeState.PENDING, - 'running': NodeState.RUNNING, - 'error': NodeState.TERMINATED, - 'unknown': NodeState.UNKNOWN } - class BlueboxNodeDriver(NodeDriver): """ Bluebox Blocks node driver @@ -140,6 +147,7 @@ class BlueboxNodeDriver(NodeDriver): return [self._to_node(i) for i in result.object] def list_sizes(self, location=None): + result = self.connection.request('/api/block_products.json') return [ BlueboxNodeSize(driver=self.connection.driver, **i) for i in BLUEBOX_INSTANCE_TYPES.values() ] @@ -204,14 +212,10 @@ class BlueboxNodeDriver(NodeDriver): def reboot_node(self, node): url = '/api/blocks/%s/reboot.json' % (node.id) result = self.connection.request(url, method="PUT") - node = self._to_node(result.object) return result.status == 200 def _to_node(self, vm): - try: - state = NODE_STATE_MAP[vm['status']] - except KeyError: - state = NodeState.UNKNOWN + state = NODE_STATE_MAP[vm.get('status', NodeState.UNKNOWN)] n = Node(id=vm['id'], name=vm['hostname'], state=state, diff --git a/test/fixtures/bluebox/api_block_products_json.json b/test/fixtures/bluebox/api_block_products_json.json new file mode 100644 index 0000000..b3baa12 --- /dev/null +++ b/test/fixtures/bluebox/api_block_products_json.json @@ -0,0 +1 @@ +[{"cost": 0.15, "id": "94fd37a7-2606-47f7-84d5-9000deda52ae", "description": "Block 1GB Virtual Server"}, {"cost": 0.25, "id": "b412f354-5056-4bf0-a42f-6ddd998aa092", "description": "Block 2GB Virtual Server"}, {"cost": 0.35, "id": "0cd183d3-0287-4b1a-8288-b3ea8302ed58", "description": "Block 4GB Virtual Server"}, {"cost": 0.45, "id": "b9b87a5b-2885-4a2e-b434-44a163ca6251", "description": "Block 8GB Virtual Server"}] diff --git a/test/fixtures/bluebox/api_block_templates_json.json b/test/fixtures/bluebox/api_block_templates_json.json new file mode 100644 index 0000000..2ea7cb6 --- /dev/null +++ b/test/fixtures/bluebox/api_block_templates_json.json @@ -0,0 +1 @@ +[{"public": true, "id": "c66b8145-f768-45ef-9878-395bf8b1b7ff", "description": "CentOS 5 (Latest Release)", "created": "2009/04/20 15:46:34 -0700"}, {"public": true, "id": "1fc24f51-6d7d-4fa9-9a6e-0d6f36b692e2", "description": "Ubuntu 8.10 64bit", "created": "2009/04/20 15:46:34 -0700"}, {"public": true, "id": "b6f152db-988c-4194-b292-d6dd2aa2dbab", "description": "Debian 5.0 64bit", "created": "2009/04/20 15:46:34 -0700"}, {"public": true, "id": "4b697e48-282b-4140-8cf8-142e2a2711ee", "description": "Ubuntu 8.04 LTS 64bit", "created": "2009/07/31 15:58:20 -0700"}, {"public": true, "id": "a6a141bf-592a-4fa6-b130-4c14f69e82d0", "description": "Ubuntu 8.04 LTS 32Bit", "created": "2009/04/20 15:46:34 -0700"}, {"public": true, "id": "b181033f-aea7-4e6c-8bb4-11169775c0f8", "description": "Ubuntu 9.04 64bit", "created": "2010/01/26 11:31:19 -0800"}, {"public": true, "id": "b5371c5a-9da2-43ee-a745-99a4723f624c", "description": "ArchLinux 2009.08 64bit", "created": "2010/02/13 18:07:01 -0800"}, {"public": true, "id": "a00baa8f-b5d0-4815-8238-b471c4c4bf72", "description": "Ubuntu 9.10 64bit", "created": "2010/02/17 22:06:21 -0800"}, {"public": true, "id": "03807e08-a13d-44e4-b011-ebec7ef2c928", "description": "Ubuntu 10.04 LTS 64bit", "created": "2010/05/04 14:43:30 -0700"}, {"public": true, "id": "8b60e6de-7cbc-4c8e-b7df-5e2f9c4ffd6b", "description": "Ubuntu 10.04 LTS 32bit", "created": "2010/05/04 14:43:30 -0700"}] diff --git a/test/fixtures/bluebox/api_blocks_99df878c_6e5c_4945_a635_d94da9fd3146_json.json b/test/fixtures/bluebox/api_blocks_99df878c_6e5c_4945_a635_d94da9fd3146_json.json new file mode 100644 index 0000000..9db716d --- /dev/null +++ b/test/fixtures/bluebox/api_blocks_99df878c_6e5c_4945_a635_d94da9fd3146_json.json @@ -0,0 +1 @@ +{"ips": [{"address": "67.214.214.212"}], "memory": 1073741824, "template": "centos", "id": "99df878c-6e5c-4945-a635-d94da9fd3146", "storage": 21474836480, "hostname": "apitest.c44905.c44905.blueboxgrid.com", "description": "1 GB RAM + 20 GB Disk", "cpu": 0.5, "status": "running", "product": {"cost": 0.15, "id": "94fd37a7-2606-47f7-84d5-9000deda52ae", "description": "Block 1GB Virtual Server"}} diff --git a/test/fixtures/bluebox/api_blocks_99df878c_6e5c_4945_a635_d94da9fd3146_json_delete.json b/test/fixtures/bluebox/api_blocks_99df878c_6e5c_4945_a635_d94da9fd3146_json_delete.json new file mode 100644 index 0000000..934a00f --- /dev/null +++ b/test/fixtures/bluebox/api_blocks_99df878c_6e5c_4945_a635_d94da9fd3146_json_delete.json @@ -0,0 +1 @@ +{"text":"Block destroyed."} diff --git a/test/fixtures/bluebox/api_blocks_99df878c_6e5c_4945_a635_d94da9fd3146_reboot_json.json b/test/fixtures/bluebox/api_blocks_99df878c_6e5c_4945_a635_d94da9fd3146_reboot_json.json new file mode 100644 index 0000000..2ea54d2 --- /dev/null +++ b/test/fixtures/bluebox/api_blocks_99df878c_6e5c_4945_a635_d94da9fd3146_reboot_json.json @@ -0,0 +1 @@ +{ "status": "ok", "text": "Reboot initiated." } diff --git a/test/fixtures/bluebox/api_blocks_json.json b/test/fixtures/bluebox/api_blocks_json.json new file mode 100644 index 0000000..eb7d35f --- /dev/null +++ b/test/fixtures/bluebox/api_blocks_json.json @@ -0,0 +1 @@ +[{"ips":[{"address":"67.214.214.212"}],"memory":1073741824,"id":"99df878c-6e5c-4945-a635-d94da9fd3146","storage":21474836480,"hostname":"foo.apitest.blueboxgrid.com","description":"1 GB RAM + 20 GB Disk","cpu":0.5,"status":"running"}] diff --git a/test/fixtures/bluebox/api_blocks_json_post.json b/test/fixtures/bluebox/api_blocks_json_post.json new file mode 100644 index 0000000..4452f71 --- /dev/null +++ b/test/fixtures/bluebox/api_blocks_json_post.json @@ -0,0 +1 @@ +{"ips":[{"address":"67.214.214.212"}],"memory":1073741824,"id":"99df878c-6e5c-4945-a635-d94da9fd3146","storage":21474836480,"hostname":"foo.apitest.blueboxgrid.com","description":"1 GB RAM + 20 GB Disk","cpu":0.5,"status":"queued", "product": {"cost": 0.15, "id": "94fd37a7-2606-47f7-84d5-9000deda52ae", "description": "Block 1GB Virtual Server"}} diff --git a/test/fixtures/bluebox/unauthorized.xml b/test/fixtures/bluebox/unauthorized.xml deleted file mode 100644 index 8ece6af..0000000 --- a/test/fixtures/bluebox/unauthorized.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - API Error. Action unccessful. - 409 - diff --git a/test/test_bluebox.py b/test/test_bluebox.py index 8b786e1..1dce48c 100644 --- a/test/test_bluebox.py +++ b/test/test_bluebox.py @@ -17,7 +17,7 @@ import unittest import exceptions from libcloud.drivers.bluebox import BlueboxNodeDriver as Bluebox -from libcloud.base import Node +from libcloud.base import Node, NodeAuthPassword from libcloud.types import NodeState import httplib @@ -34,36 +34,46 @@ class BlueboxTest(unittest.TestCase): self.driver = Bluebox(BLUEBOX_CUSTOMER_ID, BLUEBOX_API_KEY) def test_create_node(self): - BlueboxMockHttp.type = 'create' node = self.driver.create_node( - product='94fd37a7-2606-47f7-84d5-9000deda52ae', - template='c66b8145-f768-45ef-9878-395bf8b1b7ff', - password='testpass', - username='deploy', - hostname='foo' + name='foo', + size=self.driver.list_sizes()[0], + image=self.driver.list_images()[0], + auth=NodeAuthPassword("test123") ) self.assertTrue(isinstance(node, Node)) - self.assertEqual(node.name, 'foo') + self.assertEqual(node.state, NodeState.PENDING) + self.assertEqual(node.name, 'foo.apitest.blueboxgrid.com') def test_list_nodes(self): - BlueboxMockHttp.type = 'list' node = self.driver.list_nodes()[0] - self.assertEqual(node.name, 'foo') + self.assertEqual(node.name, 'foo.apitest.blueboxgrid.com') self.assertEqual(node.state, NodeState.RUNNING) + def test_list_sizes(self): + sizes = self.driver.list_sizes() + self.assertEqual(len(sizes), 4) + + ids = [s.id for s in sizes] + + self.assertTrue('94fd37a7-2606-47f7-84d5-9000deda52ae' in ids) + self.assertTrue('b412f354-5056-4bf0-a42f-6ddd998aa092' in ids) + self.assertTrue('0cd183d3-0287-4b1a-8288-b3ea8302ed58' in ids) + self.assertTrue('b9b87a5b-2885-4a2e-b434-44a163ca6251' in ids) + + def test_list_images(self): + images = self.driver.list_images() + image = images[0] + self.assertEqual(len(images), 10) + self.assertEqual(image.name, 'CentOS 5 (Latest Release)') + self.assertEqual(image.id, 'c66b8145-f768-45ef-9878-395bf8b1b7ff') + def test_reboot_node(self): - BlueboxMockHttp.type = 'list' node = self.driver.list_nodes()[0] - - BlueboxMockHttp.type = 'reboot' ret = self.driver.reboot_node(node) self.assertTrue(ret) def test_destroy_node(self): - BlueboxMockHttp.type = 'list' node = self.driver.list_nodes()[0] - - BlueboxMockHttp.type = 'delete' ret = self.driver.destroy_node(node) self.assertTrue(ret) @@ -71,8 +81,30 @@ class BlueboxMockHttp(MockHttp): fixtures = FileFixtures('bluebox') - def _api_blocks_json_list(self, method, url, body, headers): - body = """[]""" + def _api_blocks_json(self, method, url, body, headers): + if method == "POST": + body = self.fixtures.load('api_blocks_json_post.json') + else: + body = self.fixtures.load('api_blocks_json.json') + return (httplib.OK, body, headers, httplib.responses[httplib.OK]) + + def _api_block_products_json(self, method, url, body, headers): + body = self.fixtures.load('api_block_products_json.json') + return (httplib.OK, body, headers, httplib.responses[httplib.OK]) + + def _api_block_templates_json(self, method, url, body, headers): + body = self.fixtures.load('api_block_templates_json.json') + return (httplib.OK, body, headers, httplib.responses[httplib.OK]) + + def _api_blocks_99df878c_6e5c_4945_a635_d94da9fd3146_json(self, method, url, body, headers): + if method == 'DELETE': + body = self.fixtures.load('api_blocks_99df878c_6e5c_4945_a635_d94da9fd3146_json_delete.json') + else: + body = self.fixtures.load('api_blocks_99df878c_6e5c_4945_a635_d94da9fd3146_json.json') + return (httplib.OK, body, headers, httplib.responses[httplib.OK]) + + def _api_blocks_99df878c_6e5c_4945_a635_d94da9fd3146_reboot_json(self, method, url, body, headers): + body = self.fixtures.load('api_blocks_99df878c_6e5c_4945_a635_d94da9fd3146_reboot_json.json') return (httplib.OK, body, headers, httplib.responses[httplib.OK]) if __name__ == '__main__': -- 1.7.3.3 From 304a5990cca0c4e3ccb96a819e8ff4ec4edd0b7a Mon Sep 17 00:00:00 2001 From: Christian Paredes Date: Thu, 13 Jan 2011 16:05:46 -0800 Subject: [PATCH 13/13] Adding a new line --- libcloud/drivers/bluebox.py | 1 + 1 files changed, 1 insertions(+), 0 deletions(-) diff --git a/libcloud/drivers/bluebox.py b/libcloud/drivers/bluebox.py index a84924d..2c6cafc 100644 --- a/libcloud/drivers/bluebox.py +++ b/libcloud/drivers/bluebox.py @@ -167,6 +167,7 @@ class BlueboxNodeDriver(NodeDriver): name = kwargs['name'] image = kwargs['image'] size = kwargs['size'] + try: auth = kwargs['auth'] except: raise Exception("SSH public key or password required.") -- 1.7.3.3