From e9d1298e9266380fc501cdb8c02fe25fe07802d2 Mon Sep 17 00:00:00 2001 From: Thomas Herve Date: Mon, 12 Aug 2013 18:08:04 +0200 Subject: [PATCH] Implement a load balancer resource using new neutron pool It creates a new resource linking to a neutron pool, and taking a list of servers to be linked to it. Implements: blueprint lbaas-resource Change-Id: Ie1590e12449a8e086eee7e7960a45103cbf20860 --- heat/engine/resources/neutron/loadbalancer.py | 79 ++++++++++++- heat/tests/test_neutron_loadbalancer.py | 109 ++++++++++++++++++ 2 files changed, 187 insertions(+), 1 deletion(-) diff --git a/heat/engine/resources/neutron/loadbalancer.py b/heat/engine/resources/neutron/loadbalancer.py index 51b78b3e7e..7dfbbdf4b9 100644 --- a/heat/engine/resources/neutron/loadbalancer.py +++ b/heat/engine/resources/neutron/loadbalancer.py @@ -14,8 +14,11 @@ # under the License. from heat.common import exception +from heat.db.sqlalchemy import api as db_api from heat.engine import clients +from heat.engine import resource from heat.engine import scheduler +from heat.engine.resources import nova_utils from heat.engine.resources.neutron import neutron if clients.neutronclient is not None: @@ -186,7 +189,7 @@ class Pool(neutron.NeutronResource): client = self.neutron() monitors = set(prop_diff.pop('monitors', [])) if monitors: - old_monitors = set(self.t['Properties'].get('monitors', [])) + old_monitors = set(self.t['Properties']['monitors']) for monitor in old_monitors - monitors: client.disassociate_health_monitor( self.resource_id, {'health_monitor': {'id': monitor}}) @@ -237,6 +240,79 @@ class Pool(neutron.NeutronResource): self._delete_pool() +class LoadBalancer(resource.Resource): + """ + A resource to link a neutron pool with servers. + """ + + properties_schema = { + 'pool_id': { + 'Type': 'String', 'Required': True, + 'Description': _('The ID of the load balancing pool')}, + 'protocol_port': { + 'Type': 'Integer', 'Required': True, + 'Description': _('Port number on which the servers are ' + 'running on the members')}, + 'members': { + 'Type': 'List', + 'Description': _('The list of Nova server IDs load balanced')}, + } + + update_allowed_keys = ('Properties',) + + update_allowed_properties = ('members',) + + def handle_create(self): + pool = self.properties['pool_id'] + client = self.neutron() + nova_client = self.nova() + protocol_port = self.properties['protocol_port'] + for member in self.properties['members']: + address = nova_utils.server_to_ipaddress(nova_client, member) + lb_member = client.create_member({ + 'member': { + 'pool_id': pool, + 'address': address, + 'protocol_port': protocol_port}})['member'] + db_api.resource_data_set(self, member, lb_member['id']) + + def handle_update(self, json_snippet, tmpl_diff, prop_diff): + if 'members' in prop_diff: + members = set(prop_diff['members']) + old_members = set(self.t['Properties']['members']) + client = self.neutron() + for member in old_members - members: + member_id = db_api.resource_data_get(self, member) + try: + client.delete_member(member_id) + except NeutronClientException as ex: + if ex.status_code != 404: + raise ex + db_api.resource_data_delete(self, member) + pool = self.properties['pool_id'] + nova_client = self.nova() + protocol_port = self.properties['protocol_port'] + for member in members - old_members: + address = nova_utils.server_to_ipaddress(nova_client, member) + lb_member = client.create_member({ + 'member': { + 'pool_id': pool, + 'address': address, + 'protocol_port': protocol_port}})['member'] + db_api.resource_data_set(self, member, lb_member['id']) + + def handle_delete(self): + client = self.neutron() + for member in self.properties['members']: + member_id = db_api.resource_data_get(self, member) + try: + client.delete_member(member_id) + except NeutronClientException as ex: + if ex.status_code != 404: + raise ex + db_api.resource_data_delete(self, member) + + def resource_mapping(): if clients.neutronclient is None: return {} @@ -244,4 +320,5 @@ def resource_mapping(): return { 'OS::Neutron::HealthMonitor': HealthMonitor, 'OS::Neutron::Pool': Pool, + 'OS::Neutron::LoadBalancer': LoadBalancer, } diff --git a/heat/tests/test_neutron_loadbalancer.py b/heat/tests/test_neutron_loadbalancer.py index af4409a763..3fd64fba41 100644 --- a/heat/tests/test_neutron_loadbalancer.py +++ b/heat/tests/test_neutron_loadbalancer.py @@ -25,6 +25,7 @@ from heat.openstack.common.importutils import try_import from heat.tests import fakes from heat.tests import utils from heat.tests.common import HeatTestCase +from heat.tests.v1_1 import fakes as nova_fakes neutronclient = try_import('neutronclient.v2_0.client') @@ -68,6 +69,24 @@ pool_template = ''' } ''' +lb_template = ''' +{ + "AWSTemplateFormatVersion" : "2010-09-09", + "Description" : "Template to test load balancer resources", + "Parameters" : {}, + "Resources" : { + "lb": { + "Type": "OS::Neutron::LoadBalancer", + "Properties": { + "protocol_port": 8080, + "pool_id": "pool123", + "members": ["1234"] + } + } + } +} +''' + @skipIf(neutronclient is None, 'neutronclient unavailable') class HealthMonitorTest(HeatTestCase): @@ -517,3 +536,93 @@ class PoolTest(HeatTestCase): self.assertEqual(None, rsrc.update(update_template)) self.m.VerifyAll() + + +@skipIf(neutronclient is None, 'neutronclient unavailable') +class LoadBalancerTest(HeatTestCase): + + def setUp(self): + super(LoadBalancerTest, self).setUp() + self.fc = nova_fakes.FakeClient() + self.m.StubOutWithMock(neutronclient.Client, 'create_member') + self.m.StubOutWithMock(neutronclient.Client, 'delete_member') + self.m.StubOutWithMock(clients.OpenStackClients, 'keystone') + self.m.StubOutWithMock(clients.OpenStackClients, 'nova') + utils.setup_dummy_db() + + def create_load_balancer(self): + clients.OpenStackClients.keystone().AndReturn( + fakes.FakeKeystoneClient()) + clients.OpenStackClients.nova("compute").MultipleTimes().AndReturn( + self.fc) + neutronclient.Client.create_member({ + 'member': { + 'pool_id': 'pool123', 'protocol_port': 8080, + 'address': '1.2.3.4'}} + ).AndReturn({'member': {'id': 'member5678'}}) + snippet = template_format.parse(lb_template) + stack = utils.parse_stack(snippet) + return loadbalancer.LoadBalancer( + 'lb', snippet['Resources']['lb'], stack) + + def test_create(self): + rsrc = self.create_load_balancer() + + self.m.ReplayAll() + scheduler.TaskRunner(rsrc.create)() + self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) + self.m.VerifyAll() + + def test_update(self): + rsrc = self.create_load_balancer() + neutronclient.Client.delete_member(u'member5678') + neutronclient.Client.create_member({ + 'member': { + 'pool_id': 'pool123', 'protocol_port': 8080, + 'address': '4.5.6.7'}} + ).AndReturn({'member': {'id': 'memberxyz'}}) + + self.m.ReplayAll() + scheduler.TaskRunner(rsrc.create)() + + update_template = copy.deepcopy(rsrc.t) + update_template['Properties']['members'] = ['5678'] + + self.assertEqual(None, rsrc.update(update_template)) + self.m.VerifyAll() + + def test_update_missing_member(self): + rsrc = self.create_load_balancer() + neutronclient.Client.delete_member(u'member5678').AndRaise( + loadbalancer.NeutronClientException(status_code=404)) + + self.m.ReplayAll() + scheduler.TaskRunner(rsrc.create)() + + update_template = copy.deepcopy(rsrc.t) + update_template['Properties']['members'] = [] + + self.assertEqual(None, rsrc.update(update_template)) + self.assertEqual((rsrc.UPDATE, rsrc.COMPLETE), rsrc.state) + self.m.VerifyAll() + + def test_delete(self): + rsrc = self.create_load_balancer() + neutronclient.Client.delete_member(u'member5678') + + self.m.ReplayAll() + scheduler.TaskRunner(rsrc.create)() + scheduler.TaskRunner(rsrc.delete)() + self.assertEqual((rsrc.DELETE, rsrc.COMPLETE), rsrc.state) + self.m.VerifyAll() + + def test_delete_missing_member(self): + rsrc = self.create_load_balancer() + neutronclient.Client.delete_member(u'member5678').AndRaise( + loadbalancer.NeutronClientException(status_code=404)) + + self.m.ReplayAll() + scheduler.TaskRunner(rsrc.create)() + scheduler.TaskRunner(rsrc.delete)() + self.assertEqual((rsrc.DELETE, rsrc.COMPLETE), rsrc.state) + self.m.VerifyAll()