diff --git libcloud/common/brightbox.py libcloud/common/brightbox.py new file mode 100644 index 0000000..3be7922 --- /dev/null +++ libcloud/common/brightbox.py @@ -0,0 +1,89 @@ +# 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 base64 + +from libcloud.common.base import ConnectionUserAndKey, JsonResponse +from libcloud.compute.types import InvalidCredsError + +from libcloud.utils.py3 import b + +try: + import simplejson as json +except ImportError: + import json + + +class BrightboxResponse(JsonResponse): + def success(self): + return self.status >= 200 and self.status < 400 + + def parse_body(self): + if self.headers['content-type'].split('; ')[0] == 'application/json': + return super(BrightboxResponse, self).parse_body() + else: + return self.body + + def parse_error(self): + response = super(BrightboxResponse, self).parse_body() + + return '%s: %s' % (response['error_name'], response['errors'][0]) + + +class BrightboxConnection(ConnectionUserAndKey): + """ + Connection class for the Brightbox driver + """ + + host = 'api.gb1.brightbox.com' + responseCls = BrightboxResponse + + def _fetch_oauth_token(self): + body = json.dumps({'client_id': self.user_id, 'grant_type': 'none'}) + + authorization = 'Basic ' + str(base64.encodestring(b('%s:%s' % + (self.user_id, self.key)))).rstrip() + + self.connect() + + response = self.connection.request(method='POST', url='/token', body=body, headers={ + 'Host': self.host, + 'User-Agent': self._user_agent(), + 'Authorization': authorization, + 'Content-Type': 'application/json', + 'Content-Length': str(len(body)) + }) + + response = self.connection.getresponse() + + if response.status == 200: + return json.loads(response.read())['access_token'] + else: + message = '%s (%s)' % (json.loads(response.read())['error'], response.status) + + raise InvalidCredsError(message) + + def add_default_headers(self, headers): + try: + headers['Authorization'] = 'OAuth ' + self.token + except AttributeError: + self.token = self._fetch_oauth_token() + + headers['Authorization'] = 'OAuth ' + self.token + + return headers + + def encode_data(self, data): + return json.dumps(data) diff --git libcloud/compute/drivers/brightbox.py libcloud/compute/drivers/brightbox.py index f59b5be..9aa3e47 100644 --- libcloud/compute/drivers/brightbox.py +++ libcloud/compute/drivers/brightbox.py @@ -16,84 +16,16 @@ Brightbox Driver """ from libcloud.utils.py3 import httplib -import base64 -from libcloud.utils.py3 import b - -from libcloud.common.base import ConnectionUserAndKey, JsonResponse -from libcloud.compute.types import Provider, NodeState, InvalidCredsError +from libcloud.common.brightbox import BrightboxConnection +from libcloud.compute.types import Provider, NodeState from libcloud.compute.base import NodeDriver from libcloud.compute.base import Node, NodeImage, NodeSize, NodeLocation -try: - import simplejson as json -except ImportError: - import json API_VERSION = '1.0' -class BrightboxResponse(JsonResponse): - def success(self): - return self.status >= 200 and self.status < 400 - - def parse_body(self): - if self.headers['content-type'].split('; ')[0] == 'application/json': - return super(BrightboxResponse, self).parse_body() - else: - return self.body - - def parse_error(self): - return super(BrightboxResponse, self).parse_body()['error'] - - -class BrightboxConnection(ConnectionUserAndKey): - """ - Connection class for the Brightbox driver - """ - - host = 'api.gb1.brightbox.com' - responseCls = BrightboxResponse - - def _fetch_oauth_token(self): - body = json.dumps({'client_id': self.user_id, 'grant_type': 'none'}) - - authorization = 'Basic ' + str(base64.encodestring(b('%s:%s' % - (self.user_id, self.key)))).rstrip() - - self.connect() - - response = self.connection.request(method='POST', url='/token', body=body, headers={ - 'Host': self.host, - 'User-Agent': self._user_agent(), - 'Authorization': authorization, - 'Content-Type': 'application/json', - 'Content-Length': str(len(body)) - }) - - response = self.connection.getresponse() - - if response.status == 200: - return json.loads(response.read())['access_token'] - else: - message = '%s (%s)' % (json.loads(response.read())['error'], response.status) - - raise InvalidCredsError(message) - - def add_default_headers(self, headers): - try: - headers['Authorization'] = 'OAuth ' + self.token - except AttributeError: - self.token = self._fetch_oauth_token() - - headers['Authorization'] = 'OAuth ' + self.token - - return headers - - def encode_data(self, data): - return json.dumps(data) - - class BrightboxNodeDriver(NodeDriver): """ Brightbox node driver diff --git libcloud/loadbalancer/drivers/brightbox.py libcloud/loadbalancer/drivers/brightbox.py new file mode 100644 index 0000000..fd2eb9d --- /dev/null +++ libcloud/loadbalancer/drivers/brightbox.py @@ -0,0 +1,127 @@ +# 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. + +from libcloud.common.brightbox import BrightboxConnection +from libcloud.loadbalancer.base import Driver, Algorithm, Member +from libcloud.loadbalancer.base import LoadBalancer +from libcloud.loadbalancer.types import State +from libcloud.utils.misc import reverse_dict + +import httplib + + +API_VERSION = '1.0' + + +class BrightboxLBDriver(Driver): + connectionCls = BrightboxConnection + + name = 'Brightbox' + + LB_STATE_MAP = { + 'creating': State.PENDING, + 'active': State.RUNNING, + 'deleting': State.UNKNOWN, + 'deleted': State.UNKNOWN, + 'failing': State.UNKNOWN, + 'failed': State.UNKNOWN, + } + + _VALUE_TO_ALGORITHM_MAP = { + 'round-robin': Algorithm.ROUND_ROBIN, + 'least-connections': Algorithm.LEAST_CONNECTIONS + } + + _ALGORITHM_TO_VALUE_MAP = reverse_dict(_VALUE_TO_ALGORITHM_MAP) + + def list_protocols(self): + return ['tcp', 'http'] + + def list_balancers(self): + data = self.connection.request('/%s/load_balancers' % API_VERSION).object + + return map(self._to_balancer, data) + + def create_balancer(self, name, port, protocol, algorithm, members): + response = self._post('/%s/load_balancers' % API_VERSION, { + 'name': name, + 'nodes': map(self._member_to_node, members), + 'policy': self._algorithm_to_value(algorithm), + 'listeners': [{'in': port, 'out': port, 'protocol': protocol}], + 'healthcheck': {'type': protocol, 'port': port} + }) + + return self._to_balancer(response.object) + + def destroy_balancer(self, balancer): + response = self.connection.request('/%s/load_balancers/%s' % (API_VERSION, balancer.id), method='DELETE') + + return response.status == httplib.ACCEPTED + + def get_balancer(self, balancer_id): + data = self.connection.request('/%s/load_balancers/%s' % (API_VERSION, balancer_id)).object + + return self._to_balancer(data) + + def balancer_attach_compute_node(self, balancer, node): + return self.balancer_attach_member(balancer, node) + + def balancer_attach_member(self, balancer, member): + path = '/%s/load_balancers/%s/add_nodes' % (API_VERSION, balancer.id) + + response = self._post(path, {'nodes': [self._member_to_node(member)]}) + + return member + + def balancer_detach_member(self, balancer, member): + path = '/%s/load_balancers/%s/remove_nodes' % (API_VERSION, balancer.id) + + response = self._post(path, {'nodes': [self._member_to_node(member)]}) + + return response.status == httplib.ACCEPTED + + def balancer_list_members(self, balancer): + path = '/%s/load_balancers/%s' % (API_VERSION, balancer.id) + + data = self.connection.request(path).object + + return map(self._node_to_member, data['nodes']) + + def _post(self, path, data={}): + headers = {'Content-Type': 'application/json'} + + return self.connection.request(path, data=data, headers=headers, method='POST') + + def _to_balancer(self, data): + return LoadBalancer( + id=data['id'], + name=data['name'], + state=self.LB_STATE_MAP.get(data['status'], State.UNKNOWN), + ip=self._public_ip(data), + port=data['listeners'][0]['in'], + driver=self.connection.driver + ) + + def _member_to_node(self, member): + return {'node': member.id} + + def _node_to_member(self, data): + return Member(data['id'], None, None) + + def _public_ip(self, data): + if len(data['cloud_ips']) > 0: + ip = data['cloud_ips'][0]['public_ip'] + else: + ip = None diff --git libcloud/loadbalancer/providers.py libcloud/loadbalancer/providers.py index 8924c80..47d6570 100644 --- libcloud/loadbalancer/providers.py +++ libcloud/loadbalancer/providers.py @@ -31,6 +31,8 @@ DRIVERS = { ('libcloud.loadbalancer.drivers.gogrid', 'GoGridLBDriver'), Provider.NINEFOLD: ('libcloud.loadbalancer.drivers.ninefold', 'NinefoldLBDriver'), + Provider.BRIGHTBOX: + ('libcloud.loadbalancer.drivers.brightbox', 'BrightboxLBDriver'), } def get_driver(provider): diff --git libcloud/loadbalancer/types.py libcloud/loadbalancer/types.py index 75fa2ee..23f55c0 100644 --- libcloud/loadbalancer/types.py +++ libcloud/loadbalancer/types.py @@ -36,6 +36,7 @@ class Provider(object): GOGRID = 1 NINEFOLD = 2 RACKSPACE_UK = 3 + BRIGHTBOX = 4 class State(object): diff --git test/loadbalancer/fixtures/brightbox/load_balancers.json test/loadbalancer/fixtures/brightbox/load_balancers.json new file mode 100644 index 0000000..f66b88d --- /dev/null +++ test/loadbalancer/fixtures/brightbox/load_balancers.json @@ -0,0 +1,22 @@ +[{"id": "lba-1235f", + "resource_type": "load_balancer", + "url": "https://api.gb1.brightbox.com/1.0/load_balancers/lba-1235f", + "name": "lb1", + "created_at": "2011-10-06T14:50:28Z", + "deleted_at": null, + "status": "active", + "listeners": [{"out": 80, "protocol": "http", "in": 80}], + "cloud_ips": + [{"id": "cip-c2v98", + "public_ip": "109.107.37.179", + "resource_type": "cloud_ip", + "reverse_dns": "cip-109-107-37-179.gb1.brightbox.com", + "status": "mapped", + "url": "https://api.gb1.brightbox.com/1.0/cloud_ips/cip-c2v98"}], + "account": + {"id": "acc-43ks4", + "resource_type": "account", + "url": "https://api.gb1.brightbox.com/1.0/account", + "name": "Brightbox", + "status": "active"}, + "nodes": []}] diff --git test/loadbalancer/fixtures/brightbox/load_balancers_lba_1235f.json test/loadbalancer/fixtures/brightbox/load_balancers_lba_1235f.json new file mode 100644 index 0000000..3304133 --- /dev/null +++ test/loadbalancer/fixtures/brightbox/load_balancers_lba_1235f.json @@ -0,0 +1,38 @@ +{"id": "lba-1235f", + "resource_type": "load_balancer", + "url": "https://api.gb1.brightbox.com/1.0/load_balancers/lba-1235f", + "policy": "least-connections", + "name": "lb1", + "created_at": "2011-10-01T01:00:00Z", + "deleted_at": null, + "healthcheck": + {"threshold_down": 3, + "timeout": 5000, + "port": 80, + "request": "/", + "type": "http", + "interval": 5000, + "threshold_up": 3}, + "listeners": + [{"out": 80, + "protocol": "http", + "in": 80}], + "status": "active", + "cloud_ips": + [], + "account": + {"id": "acc-43ks4", + "resource_type": "account", + "url": "https://api.gb1.brightbox.com/1.0/account", + "name": "Brightbox", + "status": "active"}, + "nodes": + [{"id": "srv-lv426", + "resource_type": "server", + "url": "https://api.gb1.brightbox.com/1.0/servers/srv-lv426", + "name": "web1", + "created_at": "2011-10-01T01:00:00Z", + "deleted_at": null, + "hostname": "srv-lv426", + "started_at": "2011-10-01T01:01:00Z", + "status": "active"}]} \ No newline at end of file diff --git test/loadbalancer/fixtures/brightbox/load_balancers_post.json test/loadbalancer/fixtures/brightbox/load_balancers_post.json new file mode 100644 index 0000000..9fefd09 --- /dev/null +++ test/loadbalancer/fixtures/brightbox/load_balancers_post.json @@ -0,0 +1,38 @@ +{"id": "lba-o466u", + "resource_type": "load_balancer", + "url": "https://api.gb1.brightbox.com/1.0/load_balancers/lba-o466u", + "policy": "least-connections", + "name": "lb2", + "created_at": "2011-10-01T01:00:00Z", + "deleted_at": null, + "healthcheck": + {"threshold_down": 3, + "timeout": 5000, + "port": 80, + "request": "/", + "type": "http", + "interval": 5000, + "threshold_up": 3}, + "listeners": + [{"out": 80, + "protocol": "http", + "in": 80}], + "status": "creating", + "cloud_ips": + [], + "account": + {"id": "acc-43ks4", + "resource_type": "account", + "url": "https://api.gb1.brightbox.com/1.0/account", + "name": "Brightbox", + "status": "active"}, + "nodes": + [{"id": "srv-lv426", + "resource_type": "server", + "url": "https://api.gb1.brightbox.com/1.0/servers/srv-lv426", + "name": "web1", + "created_at": "2011-10-01T01:00:00Z", + "deleted_at": null, + "hostname": "srv-lv426", + "started_at": "2011-10-01T01:01:00Z", + "status": "active"}]} \ No newline at end of file diff --git test/loadbalancer/fixtures/brightbox/token.json test/loadbalancer/fixtures/brightbox/token.json new file mode 100644 index 0000000..4508c29 --- /dev/null +++ test/loadbalancer/fixtures/brightbox/token.json @@ -0,0 +1 @@ +{"access_token": "k1bjflpsaj8wnrbrwzad0eqo36nxiha", "expires_in": 3600} diff --git test/loadbalancer/test_brightbox.py test/loadbalancer/test_brightbox.py new file mode 100644 index 0000000..794c2db --- /dev/null +++ test/loadbalancer/test_brightbox.py @@ -0,0 +1,122 @@ +# 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 httplib +import sys +import unittest + +from libcloud.loadbalancer.base import Member, Algorithm +from libcloud.loadbalancer.drivers.brightbox import BrightboxLBDriver +from libcloud.loadbalancer.types import State + +from test import MockHttpTestCase +from test.file_fixtures import LoadBalancerFileFixtures + + +class BrightboxLBTests(unittest.TestCase): + def setUp(self): + BrightboxLBDriver.connectionCls.conn_classes = (None, BrightboxLBMockHttp) + BrightboxLBMockHttp.type = None + self.driver = BrightboxLBDriver('user', 'key') + + def test_list_protocols(self): + protocols = self.driver.list_protocols() + + self.assertEqual(len(protocols), 2) + self.assertTrue('tcp' in protocols) + self.assertTrue('http' in protocols) + + def test_list_balancers(self): + balancers = self.driver.list_balancers() + + self.assertEquals(len(balancers), 1) + self.assertEquals(balancers[0].id, 'lba-1235f') + self.assertEquals(balancers[0].name, 'lb1') + + def test_get_balancer(self): + balancer = self.driver.get_balancer(balancer_id='lba-1235f') + + self.assertEquals(balancer.id, 'lba-1235f') + self.assertEquals(balancer.name, 'lb1') + self.assertEquals(balancer.state, State.RUNNING) + + def test_destroy_balancer(self): + balancer = self.driver.get_balancer(balancer_id='lba-1235f') + + self.assertTrue(self.driver.destroy_balancer(balancer)) + + def test_create_balancer(self): + members = [Member('srv-lv426', None, None)] + + balancer = self.driver.create_balancer(name='lb2', port=80, + protocol='http', algorithm=Algorithm.ROUND_ROBIN, members=members) + + self.assertEquals(balancer.name, 'lb2') + self.assertEquals(balancer.port, 80) + self.assertEquals(balancer.state, State.PENDING) + + def test_balancer_list_members(self): + balancer = self.driver.get_balancer(balancer_id='lba-1235f') + members = balancer.list_members() + + self.assertEquals(len(members), 1) + self.assertEquals('srv-lv426', members[0].id) + + def test_balancer_attach_member(self): + balancer = self.driver.get_balancer(balancer_id='lba-1235f') + member = balancer.attach_member(Member('srv-kg983', ip=None, port=None)) + + self.assertEquals(member.id, 'srv-kg983') + + def test_balancer_detach_member(self): + balancer = self.driver.get_balancer(balancer_id='lba-1235f') + member = Member('srv-lv426', None, None) + + self.assertTrue(balancer.detach_member(member)) + + +class BrightboxLBMockHttp(MockHttpTestCase): + fixtures = LoadBalancerFileFixtures('brightbox') + + def _token(self, method, url, body, headers): + if method == 'POST': + return self.response(httplib.OK, self.fixtures.load('token.json')) + + def _1_0_load_balancers(self, method, url, body, headers): + if method == 'GET': + return self.response(httplib.OK, self.fixtures.load('load_balancers.json')) + elif method == 'POST': + return self.response(httplib.ACCEPTED, self.fixtures.load('load_balancers_post.json')) + + def _1_0_load_balancers_lba_1235f(self, method, url, body, headers): + if method == 'GET': + return self.response(httplib.OK, self.fixtures.load('load_balancers_lba_1235f.json')) + elif method == 'DELETE': + return self.response(httplib.ACCEPTED, '') + + def _1_0_load_balancers_lba_1235f_add_nodes(self, method, url, body, headers): + if method == 'POST': + return self.response(httplib.ACCEPTED, '') + + def _1_0_load_balancers_lba_1235f_remove_nodes(self, method, url, body, headers): + if method == 'POST': + return self.response(httplib.ACCEPTED, '') + + def response(self, status, body): + return (status, body, {'content-type': 'application/json'}, httplib.responses[status]) + + +if __name__ == "__main__": + sys.exit(unittest.main())