From a328c003e8f661f9235d2dfa37f1822b791e85f8 Mon Sep 17 00:00:00 2001 From: Thomas Herve Date: Wed, 7 Aug 2013 13:48:04 +0200 Subject: [PATCH] Implement neutron pool resource The branch adds a new resource creating pool instances in Neutron LBAAS. Change-Id: Ie55dd0cc5d2a67296ab175dc3bea11ffb63ba928 Implements: blueprint lbaas-resource --- heat/engine/resources/neutron/loadbalancer.py | 156 +++++++- heat/tests/test_neutron_loadbalancer.py | 367 +++++++++++++++++- 2 files changed, 512 insertions(+), 11 deletions(-) diff --git a/heat/engine/resources/neutron/loadbalancer.py b/heat/engine/resources/neutron/loadbalancer.py index a6dc9ffb4..51b78b3e7 100644 --- a/heat/engine/resources/neutron/loadbalancer.py +++ b/heat/engine/resources/neutron/loadbalancer.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from heat.common import exception from heat.engine import clients from heat.engine import scheduler from heat.engine.resources.neutron import neutron @@ -44,7 +45,7 @@ class HealthMonitor(neutron.NeutronResource): 'expected_codes', 'url_path') attributes_schema = { - 'admin_state_up': 'the administrative state of this port', + 'admin_state_up': 'the administrative state of this health monitor', 'delay': 'the minimum time in seconds between regular connections ' 'of the member', 'expected_codes': 'the list of HTTP status codes expected in ' @@ -75,8 +76,9 @@ class HealthMonitor(neutron.NeutronResource): self.resource_id)['health_monitor'] def handle_update(self, json_snippet, tmpl_diff, prop_diff): - self.neutron().update_health_monitor( - self.resource_id, {'health_monitor': prop_diff}) + if prop_diff: + self.neutron().update_health_monitor( + self.resource_id, {'health_monitor': prop_diff}) def handle_delete(self): try: @@ -88,10 +90,158 @@ class HealthMonitor(neutron.NeutronResource): return scheduler.TaskRunner(self._confirm_delete)() +class Pool(neutron.NeutronResource): + """ + A resource for managing load balancer pools in Neutron. + """ + + vip_schema = { + 'name': {'Type': 'String'}, + 'description': {'Type': 'String'}, + 'address': {'Type': 'String'}, + 'connection_limit': {'Type': 'Integer'}, + 'protocol_port': {'Type': 'Integer', 'Required': True}, + 'admin_state_up': {'Default': True, 'Type': 'Boolean'}, + } + + properties_schema = { + 'protocol': {'Type': 'String', 'Required': True, + 'AllowedValues': ['TCP', 'HTTP', 'HTTPS']}, + 'subnet_id': {'Type': 'String', 'Required': True}, + 'lb_method': {'Type': 'String', 'Required': True, + 'AllowedValues': ['ROUND_ROBIN', 'LEAST_CONNECTIONS', + 'SOURCE_IP']}, + 'name': {'Type': 'String'}, + 'description': {'Type': 'String'}, + 'admin_state_up': {'Default': True, 'Type': 'Boolean'}, + 'vip': {'Type': 'Map', 'Schema': vip_schema, 'Required': True}, + 'monitors': {'Type': 'List'}, + } + + update_allowed_keys = ('Properties',) + update_allowed_properties = ('description', 'admin_state_up', 'lb_method', + 'monitors') + + attributes_schema = { + 'admin_state_up': 'the administrative state of this pool', + 'id': 'unique identifier for this pool', + 'name': 'friendly name of the pool', + 'protocol': 'protocol to balance', + 'subnet_id': 'the subnet on which the members of the pool ' + 'will be located', + 'lb_method': 'the algorithm used to distribute load between the ' + 'members of the pool', + 'description': 'description of the pool', + 'tenant_id': 'tenant owning the pool', + 'vip': 'ip of the pool', + } + + def handle_create(self): + properties = self.prepare_properties( + self.properties, + self.physical_resource_name()) + vip_properties = properties.pop('vip') + monitors = properties.pop('monitors', []) + client = self.neutron() + pool = client.create_pool({'pool': properties})['pool'] + self.resource_id_set(pool['id']) + + for monitor in monitors: + client.associate_health_monitor( + pool['id'], {'health_monitor': {'id': monitor}}) + + vip_arguments = self.prepare_properties( + vip_properties, + '%s.vip' % (self.name,)) + vip_arguments['protocol'] = self.properties['protocol'] + vip_arguments['subnet_id'] = self.properties['subnet_id'] + vip_arguments['pool_id'] = pool['id'] + vip = client.create_vip({'vip': vip_arguments})['vip'] + + self.metadata = {'vip': vip['id']} + + def _show_resource(self): + return self.neutron().show_pool(self.resource_id)['pool'] + + def check_create_complete(self, data): + attributes = self._show_resource() + if attributes['status'] == 'PENDING_CREATE': + return False + elif attributes['status'] == 'ACTIVE': + vip_attributes = self.neutron().show_vip( + self.metadata['vip'])['vip'] + if vip_attributes['status'] == 'PENDING_CREATE': + return False + elif vip_attributes['status'] == 'ACTIVE': + return True + raise exception.Error( + 'neutron reported unexpected vip resource[%s] status[%s]' % + (vip_attributes['name'], vip_attributes['status'])) + raise exception.Error( + 'neutron report unexpected pool resource[%s] status[%s]' % + (attributes['name'], attributes['status'])) + + def handle_update(self, json_snippet, tmpl_diff, prop_diff): + if prop_diff: + client = self.neutron() + monitors = set(prop_diff.pop('monitors', [])) + if monitors: + old_monitors = set(self.t['Properties'].get('monitors', [])) + for monitor in old_monitors - monitors: + client.disassociate_health_monitor( + self.resource_id, {'health_monitor': {'id': monitor}}) + for monitor in monitors - old_monitors: + client.associate_health_monitor( + self.resource_id, {'health_monitor': {'id': monitor}}) + + if prop_diff: + client.update_pool(self.resource_id, {'pool': prop_diff}) + + def _resolve_attribute(self, name): + if name == 'vip': + return self.neutron().show_vip(self.metadata['vip'])['vip'] + return super(Pool, self)._resolve_attribute(name) + + def _confirm_vip_delete(self): + client = self.neutron() + while True: + try: + yield + client.show_vip(self.metadata['vip']) + except NeutronClientException as ex: + if ex.status_code != 404: + raise ex + break + self._delete_pool() + + def _delete_pool(self): + try: + self.neutron().delete_pool(self.resource_id) + except NeutronClientException as ex: + if ex.status_code != 404: + raise ex + else: + return scheduler.TaskRunner(self._confirm_delete)() + + def handle_delete(self): + if self.metadata: + try: + self.neutron().delete_vip(self.metadata['vip']) + except NeutronClientException as ex: + if ex.status_code != 404: + raise ex + self._delete_pool() + else: + return scheduler.TaskRunner(self._confirm_vip_delete)() + else: + self._delete_pool() + + def resource_mapping(): if clients.neutronclient is None: return {} return { 'OS::Neutron::HealthMonitor': HealthMonitor, + 'OS::Neutron::Pool': Pool, } diff --git a/heat/tests/test_neutron_loadbalancer.py b/heat/tests/test_neutron_loadbalancer.py index a4cbbe8b4..af4409a76 100644 --- a/heat/tests/test_neutron_loadbalancer.py +++ b/heat/tests/test_neutron_loadbalancer.py @@ -47,6 +47,27 @@ health_monitor_template = ''' } ''' +pool_template = ''' +{ + "AWSTemplateFormatVersion" : "2010-09-09", + "Description" : "Template to test load balancer resources", + "Parameters" : {}, + "Resources" : { + "pool": { + "Type": "OS::Neutron::Pool", + "Properties": { + "protocol": "HTTP", + "subnet_id": "sub123", + "lb_method": "ROUND_ROBIN", + "vip": { + "protocol_port": 80 + } + } + } + } +} +''' + @skipIf(neutronclient is None, 'neutronclient unavailable') class HealthMonitorTest(HeatTestCase): @@ -95,13 +116,16 @@ class HealthMonitorTest(HeatTestCase): stack = utils.parse_stack(snippet) rsrc = loadbalancer.HealthMonitor( 'monitor', snippet['Resources']['monitor'], stack) - self.assertRaises(exception.ResourceFailure, - scheduler.TaskRunner(rsrc.create)) + error = self.assertRaises(exception.ResourceFailure, + scheduler.TaskRunner(rsrc.create)) + self.assertEqual( + 'NeutronClientException: An unknown exception occurred.', + str(error)) self.assertEqual((rsrc.CREATE, rsrc.FAILED), rsrc.state) self.m.VerifyAll() def test_delete(self): - neutronclient.Client.delete_health_monitor('5678').AndReturn(None) + neutronclient.Client.delete_health_monitor('5678') neutronclient.Client.show_health_monitor('5678').AndRaise( loadbalancer.NeutronClientException(status_code=404)) @@ -130,8 +154,11 @@ class HealthMonitorTest(HeatTestCase): rsrc = self.create_health_monitor() self.m.ReplayAll() scheduler.TaskRunner(rsrc.create)() - self.assertRaises(exception.ResourceFailure, - scheduler.TaskRunner(rsrc.delete)) + error = self.assertRaises(exception.ResourceFailure, + scheduler.TaskRunner(rsrc.delete)) + self.assertEqual( + 'NeutronClientException: An unknown exception occurred.', + str(error)) self.assertEqual((rsrc.DELETE, rsrc.FAILED), rsrc.state) self.m.VerifyAll() @@ -150,14 +177,17 @@ class HealthMonitorTest(HeatTestCase): rsrc = self.create_health_monitor() self.m.ReplayAll() scheduler.TaskRunner(rsrc.create)() - self.assertRaises(exception.InvalidTemplateAttribute, - rsrc.FnGetAtt, 'subnet_id') + error = self.assertRaises(exception.InvalidTemplateAttribute, + rsrc.FnGetAtt, 'subnet_id') + self.assertEqual( + 'The Referenced Attribute (monitor subnet_id) is incorrect.', + str(error)) self.m.VerifyAll() def test_update(self): rsrc = self.create_health_monitor() neutronclient.Client.update_health_monitor( - '5678', {'health_monitor': {'delay': 10}}).AndReturn(None) + '5678', {'health_monitor': {'delay': 10}}) self.m.ReplayAll() scheduler.TaskRunner(rsrc.create)() @@ -166,3 +196,324 @@ class HealthMonitorTest(HeatTestCase): self.assertEqual(None, rsrc.update(update_template)) self.m.VerifyAll() + + +@skipIf(neutronclient is None, 'neutronclient unavailable') +class PoolTest(HeatTestCase): + + def setUp(self): + super(PoolTest, self).setUp() + self.m.StubOutWithMock(neutronclient.Client, 'create_pool') + self.m.StubOutWithMock(neutronclient.Client, 'delete_pool') + self.m.StubOutWithMock(neutronclient.Client, 'show_pool') + self.m.StubOutWithMock(neutronclient.Client, 'update_pool') + self.m.StubOutWithMock(neutronclient.Client, + 'associate_health_monitor') + self.m.StubOutWithMock(neutronclient.Client, + 'disassociate_health_monitor') + self.m.StubOutWithMock(neutronclient.Client, 'create_vip') + self.m.StubOutWithMock(neutronclient.Client, 'delete_vip') + self.m.StubOutWithMock(neutronclient.Client, 'show_vip') + self.m.StubOutWithMock(clients.OpenStackClients, 'keystone') + utils.setup_dummy_db() + + def create_pool(self): + clients.OpenStackClients.keystone().AndReturn( + fakes.FakeKeystoneClient()) + neutronclient.Client.create_pool({ + 'pool': { + 'subnet_id': 'sub123', 'protocol': u'HTTP', + 'name': utils.PhysName('test_stack', 'pool'), + 'lb_method': 'ROUND_ROBIN', 'admin_state_up': True}} + ).AndReturn({'pool': {'id': '5678'}}) + neutronclient.Client.create_vip({ + 'vip': { + 'protocol': u'HTTP', 'name': 'pool.vip', + 'admin_state_up': True, 'subnet_id': u'sub123', + 'pool_id': '5678', 'protocol_port': 80}} + ).AndReturn({'vip': {'id': 'xyz'}}) + neutronclient.Client.show_pool('5678').AndReturn( + {'pool': {'status': 'ACTIVE'}}) + neutronclient.Client.show_vip('xyz').AndReturn( + {'vip': {'status': 'ACTIVE'}}) + + snippet = template_format.parse(pool_template) + stack = utils.parse_stack(snippet) + return loadbalancer.Pool( + 'pool', snippet['Resources']['pool'], stack) + + def test_create(self): + rsrc = self.create_pool() + self.m.ReplayAll() + scheduler.TaskRunner(rsrc.create)() + self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) + self.m.VerifyAll() + + def test_create_pending(self): + clients.OpenStackClients.keystone().AndReturn( + fakes.FakeKeystoneClient()) + neutronclient.Client.create_pool({ + 'pool': { + 'subnet_id': 'sub123', 'protocol': u'HTTP', + 'name': utils.PhysName('test_stack', 'pool'), + 'lb_method': 'ROUND_ROBIN', 'admin_state_up': True}} + ).AndReturn({'pool': {'id': '5678'}}) + neutronclient.Client.create_vip({ + 'vip': { + 'protocol': u'HTTP', 'name': 'pool.vip', + 'admin_state_up': True, 'subnet_id': u'sub123', + 'pool_id': '5678', 'protocol_port': 80}} + ).AndReturn({'vip': {'id': 'xyz'}}) + neutronclient.Client.show_pool('5678').AndReturn( + {'pool': {'status': 'PENDING_CREATE'}}) + neutronclient.Client.show_pool('5678').MultipleTimes().AndReturn( + {'pool': {'status': 'ACTIVE'}}) + neutronclient.Client.show_vip('xyz').AndReturn( + {'vip': {'status': 'PENDING_CREATE'}}) + neutronclient.Client.show_vip('xyz').AndReturn( + {'vip': {'status': 'ACTIVE'}}) + + snippet = template_format.parse(pool_template) + stack = utils.parse_stack(snippet) + rsrc = loadbalancer.Pool( + 'pool', snippet['Resources']['pool'], stack) + self.m.ReplayAll() + scheduler.TaskRunner(rsrc.create)() + self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) + self.m.VerifyAll() + + def test_create_failed_unexpected_status(self): + clients.OpenStackClients.keystone().AndReturn( + fakes.FakeKeystoneClient()) + neutronclient.Client.create_pool({ + 'pool': { + 'subnet_id': 'sub123', 'protocol': u'HTTP', + 'name': utils.PhysName('test_stack', 'pool'), + 'lb_method': 'ROUND_ROBIN', 'admin_state_up': True}} + ).AndReturn({'pool': {'id': '5678'}}) + neutronclient.Client.create_vip({ + 'vip': { + 'protocol': u'HTTP', 'name': 'pool.vip', + 'admin_state_up': True, 'subnet_id': u'sub123', + 'pool_id': '5678', 'protocol_port': 80}} + ).AndReturn({'vip': {'id': 'xyz'}}) + neutronclient.Client.show_pool('5678').AndReturn( + {'pool': {'status': 'ERROR', 'name': '5678'}}) + + snippet = template_format.parse(pool_template) + stack = utils.parse_stack(snippet) + rsrc = loadbalancer.Pool( + 'pool', snippet['Resources']['pool'], stack) + self.m.ReplayAll() + error = self.assertRaises(exception.ResourceFailure, + scheduler.TaskRunner(rsrc.create)) + self.assertEqual( + 'Error: neutron report unexpected pool ' + 'resource[5678] status[ERROR]', + str(error)) + self.assertEqual((rsrc.CREATE, rsrc.FAILED), rsrc.state) + self.m.VerifyAll() + + def test_create_failed_unexpected_vip_status(self): + clients.OpenStackClients.keystone().AndReturn( + fakes.FakeKeystoneClient()) + neutronclient.Client.create_pool({ + 'pool': { + 'subnet_id': 'sub123', 'protocol': u'HTTP', + 'name': utils.PhysName('test_stack', 'pool'), + 'lb_method': 'ROUND_ROBIN', 'admin_state_up': True}} + ).AndReturn({'pool': {'id': '5678'}}) + neutronclient.Client.create_vip({ + 'vip': { + 'protocol': u'HTTP', 'name': 'pool.vip', + 'admin_state_up': True, 'subnet_id': u'sub123', + 'pool_id': '5678', 'protocol_port': 80}} + ).AndReturn({'vip': {'id': 'xyz'}}) + neutronclient.Client.show_pool('5678').MultipleTimes().AndReturn( + {'pool': {'status': 'ACTIVE'}}) + neutronclient.Client.show_vip('xyz').AndReturn( + {'vip': {'status': 'ERROR', 'name': 'xyz'}}) + + snippet = template_format.parse(pool_template) + stack = utils.parse_stack(snippet) + rsrc = loadbalancer.Pool( + 'pool', snippet['Resources']['pool'], stack) + self.m.ReplayAll() + error = self.assertRaises(exception.ResourceFailure, + scheduler.TaskRunner(rsrc.create)) + self.assertEqual( + 'Error: neutron reported unexpected vip ' + 'resource[xyz] status[ERROR]', + str(error)) + self.assertEqual((rsrc.CREATE, rsrc.FAILED), rsrc.state) + self.m.VerifyAll() + + def test_create_failed(self): + clients.OpenStackClients.keystone().AndReturn( + fakes.FakeKeystoneClient()) + neutronclient.Client.create_pool({ + 'pool': { + 'subnet_id': 'sub123', 'protocol': u'HTTP', + 'name': utils.PhysName('test_stack', 'pool'), + 'lb_method': 'ROUND_ROBIN', 'admin_state_up': True}} + ).AndRaise(loadbalancer.NeutronClientException()) + self.m.ReplayAll() + + snippet = template_format.parse(pool_template) + stack = utils.parse_stack(snippet) + rsrc = loadbalancer.Pool( + 'pool', snippet['Resources']['pool'], stack) + error = self.assertRaises(exception.ResourceFailure, + scheduler.TaskRunner(rsrc.create)) + self.assertEqual( + 'NeutronClientException: An unknown exception occurred.', + str(error)) + self.assertEqual((rsrc.CREATE, rsrc.FAILED), rsrc.state) + self.m.VerifyAll() + + def test_delete(self): + rsrc = self.create_pool() + neutronclient.Client.delete_vip('xyz') + neutronclient.Client.show_vip('xyz').AndRaise( + loadbalancer.NeutronClientException(status_code=404)) + neutronclient.Client.delete_pool('5678') + neutronclient.Client.show_pool('5678').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() + + def test_delete_already_gone(self): + neutronclient.Client.delete_vip('xyz').AndRaise( + loadbalancer.NeutronClientException(status_code=404)) + neutronclient.Client.delete_pool('5678').AndRaise( + loadbalancer.NeutronClientException(status_code=404)) + + rsrc = self.create_pool() + 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_vip_failed(self): + neutronclient.Client.delete_vip('xyz').AndRaise( + loadbalancer.NeutronClientException(status_code=400)) + + rsrc = self.create_pool() + self.m.ReplayAll() + scheduler.TaskRunner(rsrc.create)() + error = self.assertRaises(exception.ResourceFailure, + scheduler.TaskRunner(rsrc.delete)) + self.assertEqual( + 'NeutronClientException: An unknown exception occurred.', + str(error)) + self.assertEqual((rsrc.DELETE, rsrc.FAILED), rsrc.state) + self.m.VerifyAll() + + def test_delete_failed(self): + neutronclient.Client.delete_vip('xyz').AndRaise( + loadbalancer.NeutronClientException(status_code=404)) + neutronclient.Client.delete_pool('5678').AndRaise( + loadbalancer.NeutronClientException(status_code=400)) + + rsrc = self.create_pool() + self.m.ReplayAll() + scheduler.TaskRunner(rsrc.create)() + error = self.assertRaises(exception.ResourceFailure, + scheduler.TaskRunner(rsrc.delete)) + self.assertEqual( + 'NeutronClientException: An unknown exception occurred.', + str(error)) + self.assertEqual((rsrc.DELETE, rsrc.FAILED), rsrc.state) + self.m.VerifyAll() + + def test_attribute(self): + rsrc = self.create_pool() + neutronclient.Client.show_pool('5678').MultipleTimes( + ).AndReturn( + {'pool': {'admin_state_up': True, 'lb_method': 'ROUND_ROBIN'}}) + self.m.ReplayAll() + scheduler.TaskRunner(rsrc.create)() + self.assertEqual(True, rsrc.FnGetAtt('admin_state_up')) + self.assertEqual('ROUND_ROBIN', rsrc.FnGetAtt('lb_method')) + self.m.VerifyAll() + + def test_vip_attribute(self): + rsrc = self.create_pool() + neutronclient.Client.show_vip('xyz').AndReturn( + {'vip': {'address': '10.0.0.3', 'name': 'xyz'}}) + self.m.ReplayAll() + scheduler.TaskRunner(rsrc.create)() + self.assertEqual({'address': '10.0.0.3', 'name': 'xyz'}, + rsrc.FnGetAtt('vip')) + self.m.VerifyAll() + + def test_attribute_failed(self): + rsrc = self.create_pool() + self.m.ReplayAll() + scheduler.TaskRunner(rsrc.create)() + error = self.assertRaises(exception.InvalidTemplateAttribute, + rsrc.FnGetAtt, 'net_id') + self.assertEqual( + 'The Referenced Attribute (pool net_id) is incorrect.', + str(error)) + self.m.VerifyAll() + + def test_update(self): + rsrc = self.create_pool() + neutronclient.Client.update_pool( + '5678', {'pool': {'admin_state_up': False}}) + self.m.ReplayAll() + scheduler.TaskRunner(rsrc.create)() + + update_template = copy.deepcopy(rsrc.t) + update_template['Properties']['admin_state_up'] = False + self.assertEqual(None, rsrc.update(update_template)) + + self.m.VerifyAll() + + def test_update_monitors(self): + clients.OpenStackClients.keystone().AndReturn( + fakes.FakeKeystoneClient()) + neutronclient.Client.create_pool({ + 'pool': { + 'subnet_id': 'sub123', 'protocol': u'HTTP', + 'name': utils.PhysName('test_stack', 'pool'), + 'lb_method': 'ROUND_ROBIN', 'admin_state_up': True}} + ).AndReturn({'pool': {'id': '5678'}}) + neutronclient.Client.associate_health_monitor( + '5678', {'health_monitor': {'id': 'mon123'}}) + neutronclient.Client.associate_health_monitor( + '5678', {'health_monitor': {'id': 'mon456'}}) + neutronclient.Client.create_vip({ + 'vip': { + 'protocol': u'HTTP', 'name': 'pool.vip', + 'admin_state_up': True, 'subnet_id': u'sub123', + 'pool_id': '5678', 'protocol_port': 80}} + ).AndReturn({'vip': {'id': 'xyz'}}) + neutronclient.Client.show_pool('5678').AndReturn( + {'pool': {'status': 'ACTIVE'}}) + neutronclient.Client.show_vip('xyz').AndReturn( + {'vip': {'status': 'ACTIVE'}}) + neutronclient.Client.disassociate_health_monitor( + '5678', {'health_monitor': {'id': 'mon456'}}) + neutronclient.Client.associate_health_monitor( + '5678', {'health_monitor': {'id': 'mon789'}}) + + snippet = template_format.parse(pool_template) + stack = utils.parse_stack(snippet) + snippet['Resources']['pool']['Properties']['monitors'] = [ + 'mon123', 'mon456'] + rsrc = loadbalancer.Pool( + 'pool', snippet['Resources']['pool'], stack) + self.m.ReplayAll() + scheduler.TaskRunner(rsrc.create)() + + update_template = copy.deepcopy(rsrc.t) + update_template['Properties']['monitors'] = ['mon123', 'mon789'] + self.assertEqual(None, rsrc.update(update_template)) + + self.m.VerifyAll()