From d84798b93a8f63b3374bf0c9375601a6a9cbd794 Mon Sep 17 00:00:00 2001 From: rabi Date: Tue, 19 Dec 2017 14:26:24 +0530 Subject: [PATCH] Add octavia Loadbalancer Resource Change-Id: I05f105d290696e1dedc5c9135a5ee36a1271a7ad Partial-Bug: #1737567 --- .../openstack/octavia/loadbalancer.py | 163 +++++++++++++++++ .../openstack/octavia/inline_templates.py | 16 ++ .../openstack/octavia/test_loadbalancer.py | 170 ++++++++++++++++++ 3 files changed, 349 insertions(+) create mode 100644 heat/engine/resources/openstack/octavia/loadbalancer.py create mode 100644 heat/tests/openstack/octavia/test_loadbalancer.py diff --git a/heat/engine/resources/openstack/octavia/loadbalancer.py b/heat/engine/resources/openstack/octavia/loadbalancer.py new file mode 100644 index 0000000000..c4d477f7e2 --- /dev/null +++ b/heat/engine/resources/openstack/octavia/loadbalancer.py @@ -0,0 +1,163 @@ +# +# Licensed 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 heat.common.i18n import _ +from heat.engine import attributes +from heat.engine import constraints +from heat.engine import properties +from heat.engine.resources.openstack.octavia import octavia_base +from heat.engine import translation + + +class LoadBalancer(octavia_base.OctaviaBase): + """A resource for creating octavia Load Balancers. + + This resource creates and manages octavia Load Balancers, + which allows traffic to be directed between servers. + """ + + PROPERTIES = ( + DESCRIPTION, NAME, PROVIDER, VIP_ADDRESS, VIP_SUBNET, + ADMIN_STATE_UP, TENANT_ID + ) = ( + 'description', 'name', 'provider', 'vip_address', 'vip_subnet', + 'admin_state_up', 'tenant_id' + ) + + ATTRIBUTES = ( + VIP_ADDRESS_ATTR, VIP_PORT_ATTR, VIP_SUBNET_ATTR, POOLS_ATTR + ) = ( + 'vip_address', 'vip_port_id', 'vip_subnet_id', 'pools' + ) + + properties_schema = { + DESCRIPTION: properties.Schema( + properties.Schema.STRING, + _('Description of this Load Balancer.'), + update_allowed=True, + default='' + ), + NAME: properties.Schema( + properties.Schema.STRING, + _('Name of this Load Balancer.'), + update_allowed=True + ), + PROVIDER: properties.Schema( + properties.Schema.STRING, + _('Provider for this Load Balancer.'), + ), + VIP_ADDRESS: properties.Schema( + properties.Schema.STRING, + _('IP address for the VIP.'), + constraints=[ + constraints.CustomConstraint('ip_addr') + ], + ), + VIP_SUBNET: properties.Schema( + properties.Schema.STRING, + _('The name or ID of the subnet on which to allocate the VIP ' + 'address.'), + constraints=[ + constraints.CustomConstraint('neutron.subnet') + ], + required=True + ), + ADMIN_STATE_UP: properties.Schema( + properties.Schema.BOOLEAN, + _('The administrative state of this Load Balancer.'), + default=True, + update_allowed=True + ), + TENANT_ID: properties.Schema( + properties.Schema.STRING, + _('The ID of the tenant who owns the Load Balancer. Only ' + 'administrative users can specify a tenant ID other than ' + 'their own.'), + constraints=[ + constraints.CustomConstraint('keystone.project') + ], + ) + } + + attributes_schema = { + VIP_ADDRESS_ATTR: attributes.Schema( + _('The VIP address of the LoadBalancer.'), + type=attributes.Schema.STRING + ), + VIP_PORT_ATTR: attributes.Schema( + _('The VIP port of the LoadBalancer.'), + type=attributes.Schema.STRING + ), + VIP_SUBNET_ATTR: attributes.Schema( + _('The VIP subnet of the LoadBalancer.'), + type=attributes.Schema.STRING + ), + POOLS_ATTR: attributes.Schema( + _('Pools this LoadBalancer is associated with.'), + type=attributes.Schema.LIST, + ), + } + + def translation_rules(self, props): + return [ + translation.TranslationRule( + props, + translation.TranslationRule.RESOLVE, + [self.VIP_SUBNET], + client_plugin=self.client_plugin('neutron'), + finder='find_resourceid_by_name_or_id', + entity='subnet' + ), + ] + + def _prepare_args(self, properties): + props = dict((k, v) for k, v in properties.items() + if v is not None) + if self.NAME not in props: + props[self.NAME] = self.physical_resource_name() + props['vip_subnet_id'] = props.pop(self.VIP_SUBNET) + return props + + def handle_create(self): + properties = self._prepare_args(self.properties) + lb = self.client().load_balancer_create( + json={'loadbalancer': properties})['loadbalancer'] + self.resource_id_set(lb['id']) + + def check_create_complete(self, data): + return self._check_status() + + def handle_update(self, json_snippet, tmpl_diff, prop_diff): + if prop_diff: + self.client().load_balancer_set( + self.resource_id, + json={'loadbalancer': prop_diff}) + return prop_diff + + def check_update_complete(self, prop_diff): + if prop_diff: + return self._check_status() + return True + + def _resource_delete(self): + self.client().load_balancer_delete(self.resource_id) + + def _show_resource(self): + return self.client().load_balancer_show( + self.resource_id) + + +def resource_mapping(): + return { + 'OS::Octavia::LoadBalancer': LoadBalancer + } diff --git a/heat/tests/openstack/octavia/inline_templates.py b/heat/tests/openstack/octavia/inline_templates.py index e1cb170dfb..2dc40b7aa1 100644 --- a/heat/tests/openstack/octavia/inline_templates.py +++ b/heat/tests/openstack/octavia/inline_templates.py @@ -11,6 +11,22 @@ # License for the specific language governing permissions and limitations # under the License. +LB_TEMPLATE = ''' +heat_template_version: 2016-04-08 +description: Create a loadbalancer +resources: + lb: + type: OS::Octavia::LoadBalancer + properties: + name: my_lb + description: my loadbalancer + vip_address: 10.0.0.4 + vip_subnet: sub123 + provider: octavia + tenant_id: 1234 + admin_state_up: True +''' + LISTENER_TEMPLATE = ''' heat_template_version: 2016-04-08 description: Create a listener diff --git a/heat/tests/openstack/octavia/test_loadbalancer.py b/heat/tests/openstack/octavia/test_loadbalancer.py new file mode 100644 index 0000000000..969ce3b669 --- /dev/null +++ b/heat/tests/openstack/octavia/test_loadbalancer.py @@ -0,0 +1,170 @@ +# +# Licensed 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 mock + +from neutronclient.neutron import v2_0 as neutronV20 +from osc_lib import exceptions + +from heat.common import exception +from heat.common import template_format +from heat.engine.resources.openstack.octavia import loadbalancer +from heat.tests import common +from heat.tests.openstack.octavia import inline_templates +from heat.tests import utils + + +class LoadBalancerTest(common.HeatTestCase): + + def test_resource_mapping(self): + mapping = loadbalancer.resource_mapping() + self.assertEqual(loadbalancer.LoadBalancer, + mapping['OS::Octavia::LoadBalancer']) + + def _create_stack(self, tmpl=inline_templates.LB_TEMPLATE): + self.t = template_format.parse(tmpl) + self.stack = utils.parse_stack(self.t) + self.lb = self.stack['lb'] + self.octavia_client = mock.MagicMock() + self.lb.client = mock.MagicMock() + self.lb.client.return_value = self.octavia_client + + self.patchobject(neutronV20, 'find_resourceid_by_name_or_id', + return_value='123') + + self.lb.client_plugin().client = mock.MagicMock( + return_value=self.octavia_client) + self.lb.translate_properties(self.lb.properties) + self.lb.resource_id_set('1234') + + def test_create(self): + self._create_stack() + expected = { + 'loadbalancer': { + 'name': 'my_lb', + 'description': 'my loadbalancer', + 'vip_address': '10.0.0.4', + 'vip_subnet_id': '123', + 'provider': 'octavia', + 'tenant_id': '1234', + 'admin_state_up': True, + } + } + + self.lb.handle_create() + + self.octavia_client.load_balancer_create.assert_called_with( + json=expected) + + def test_check_create_complete(self): + self._create_stack() + self.octavia_client.load_balancer_show.side_effect = [ + {'provisioning_status': 'ACTIVE'}, + {'provisioning_status': 'PENDING_CREATE'}, + {'provisioning_status': 'ERROR'}, + ] + + self.assertTrue(self.lb.check_create_complete(None)) + self.assertFalse(self.lb.check_create_complete(None)) + self.assertRaises(exception.ResourceInError, + self.lb.check_create_complete, None) + + def test_show_resource(self): + self._create_stack() + self.octavia_client.load_balancer_show.return_value = {'id': '1234'} + self.assertEqual({'id': '1234'}, self.lb._show_resource()) + + self.octavia_client.load_balancer_show.assert_called_with('1234') + + def test_update(self): + self._create_stack() + prop_diff = { + 'name': 'lb', + 'description': 'a loadbalancer', + 'admin_state_up': False, + } + + prop_diff = self.lb.handle_update(None, None, prop_diff) + + self.octavia_client.load_balancer_set.assert_called_once_with( + '1234', json={'loadbalancer': prop_diff}) + + def test_update_complete(self): + self._create_stack() + prop_diff = { + 'name': 'lb', + 'description': 'a loadbalancer', + 'admin_state_up': False, + } + self.octavia_client.load_balancer_show.side_effect = [ + {'provisioning_status': 'ACTIVE'}, + {'provisioning_status': 'PENDING_UPDATE'}, + ] + + self.lb.handle_update(None, None, prop_diff) + + self.assertTrue(self.lb.check_update_complete(prop_diff)) + self.assertFalse(self.lb.check_update_complete(prop_diff)) + self.assertTrue(self.lb.check_update_complete({})) + + def test_delete(self): + self._create_stack() + self.octavia_client.load_balancer_show.side_effect = [ + {'provisioning_status': 'DELETE_PENDING'}, + {'provisioning_status': 'DELETE_PENDING'}, + {'provisioning_status': 'DELETED'}, + ] + + self.octavia_client.load_balancer_delete.side_effect = [ + exceptions.Conflict(409), + None + ] + + self.lb.handle_delete() + + self.assertFalse(self.lb.check_delete_complete(None)) + self.assertFalse(self.lb._delete_called) + self.assertFalse(self.lb.check_delete_complete(None)) + self.assertTrue(self.lb._delete_called) + self.assertTrue(self.lb.check_delete_complete(None)) + self.octavia_client.load_balancer_delete.assert_called_with('1234') + self.assertEqual( + 2, self.octavia_client.load_balancer_delete.call_count) + + def test_delete_error(self): + self._create_stack() + self.octavia_client.load_balancer_show.side_effect = [ + {'provisioning_status': 'DELETE_PENDING'}, + ] + + self.octavia_client.load_balancer_delete.side_effect = [ + exceptions.Conflict(409), + exceptions.NotFound(404) + ] + + self.lb.handle_delete() + + self.assertFalse(self.lb.check_delete_complete(None)) + self.assertTrue(self.lb.check_delete_complete(None)) + self.octavia_client.load_balancer_delete.assert_called_with('1234') + self.assertEqual( + 2, self.octavia_client.load_balancer_delete.call_count) + + def test_delete_failed(self): + self._create_stack() + self.octavia_client.load_balancer_delete.side_effect = ( + exceptions.Unauthorized(403)) + + self.lb.handle_delete() + self.assertRaises(exceptions.Unauthorized, + self.lb.check_delete_complete, None)