From 946c576026f1ecaaed81ec74e44d94b24f9b6fbf Mon Sep 17 00:00:00 2001 From: rabi Date: Wed, 3 Jan 2018 14:22:17 +0530 Subject: [PATCH] Add octavia L7Rule Resource This also adds releasenotes for the newly added resources. Change-Id: Ibaa618d14ea06f6ed2b2fc81d8d3ef270ee16a36 Closes-bug: #1737567 --- heat/engine/clients/os/octavia.py | 9 + .../resources/openstack/octavia/l7rule.py | 148 +++++++++++++++ .../openstack/octavia/inline_templates.py | 16 ++ heat/tests/openstack/octavia/test_l7rule.py | 178 ++++++++++++++++++ .../octavia-resources-0a25720e16dfe55d.yaml | 19 ++ setup.cfg | 1 + 6 files changed, 371 insertions(+) create mode 100644 heat/engine/resources/openstack/octavia/l7rule.py create mode 100644 heat/tests/openstack/octavia/test_l7rule.py create mode 100644 releasenotes/notes/octavia-resources-0a25720e16dfe55d.yaml diff --git a/heat/engine/clients/os/octavia.py b/heat/engine/clients/os/octavia.py index b8ade71168..c865e01ca6 100644 --- a/heat/engine/clients/os/octavia.py +++ b/heat/engine/clients/os/octavia.py @@ -73,6 +73,11 @@ class OctaviaClientPlugin(client_plugin.ClientPlugin): value=value, attr=DEFAULT_FIND_ATTR) return lb['id'] + def get_l7policy(self, value): + policy = self.client().find(path=constants.BASE_L7POLICY_URL, + value=value, attr=DEFAULT_FIND_ATTR) + return policy['id'] + class OctaviaConstraint(constraints.BaseCustomConstraint): @@ -96,3 +101,7 @@ class ListenerConstraint(OctaviaConstraint): class PoolConstraint(OctaviaConstraint): base_url = constants.BASE_POOL_URL + + +class L7PolicyConstraint(OctaviaConstraint): + base_url = constants.BASE_L7POLICY_URL diff --git a/heat/engine/resources/openstack/octavia/l7rule.py b/heat/engine/resources/openstack/octavia/l7rule.py new file mode 100644 index 0000000000..f1455359eb --- /dev/null +++ b/heat/engine/resources/openstack/octavia/l7rule.py @@ -0,0 +1,148 @@ +# +# 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 import exception +from heat.common.i18n import _ +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 L7Rule(octavia_base.OctaviaBase): + """A resource for managing octavia L7Rules. + + This resource manages L7Rules, which represent a set of attributes + that defines which part of the request should be matched and how + it should be matched. + """ + + PROPERTIES = ( + ADMIN_STATE_UP, L7POLICY, TYPE, COMPARE_TYPE, + INVERT, KEY, VALUE + ) = ( + 'admin_state_up', 'l7policy', 'type', 'compare_type', + 'invert', 'key', 'value' + ) + + L7RULE_TYPES = ( + HOST_NAME, PATH, FILE_TYPE, HEADER, COOKIE + ) = ( + 'HOST_NAME', 'PATH', 'FILE_TYPE', 'HEADER', 'COOKIE' + ) + + L7COMPARE_TYPES = ( + REGEX, STARTS_WITH, ENDS_WITH, CONTAINS, EQUAL_TO + ) = ( + 'REGEX', 'STARTS_WITH', 'ENDS_WITH', 'CONTAINS', 'EQUAL_TO' + ) + + properties_schema = { + ADMIN_STATE_UP: properties.Schema( + properties.Schema.BOOLEAN, + _('The administrative state of the rule.'), + default=True, + update_allowed=True + ), + L7POLICY: properties.Schema( + properties.Schema.STRING, + _('ID or name of L7 policy this rule belongs to.'), + constraints=[ + constraints.CustomConstraint('octavia.l7policy') + ], + required=True + ), + TYPE: properties.Schema( + properties.Schema.STRING, + _('Rule type.'), + constraints=[constraints.AllowedValues(L7RULE_TYPES)], + update_allowed=True, + required=True + ), + COMPARE_TYPE: properties.Schema( + properties.Schema.STRING, + _('Rule compare type.'), + constraints=[constraints.AllowedValues(L7COMPARE_TYPES)], + update_allowed=True, + required=True + ), + INVERT: properties.Schema( + properties.Schema.BOOLEAN, + _('Invert the compare type.'), + default=False, + update_allowed=True + ), + KEY: properties.Schema( + properties.Schema.STRING, + _('Key to compare. Relevant for HEADER and COOKIE types only.'), + update_allowed=True + ), + VALUE: properties.Schema( + properties.Schema.STRING, + _('Value to compare.'), + update_allowed=True, + required=True + ) + } + + def translation_rules(self, props): + return [ + translation.TranslationRule( + props, + translation.TranslationRule.RESOLVE, + [self.L7POLICY], + client_plugin=self.client_plugin(), + finder='get_l7policy', + ) + ] + + def validate(self): + super(L7Rule, self).validate() + if (self.properties[self.TYPE] in (self.HEADER, self.COOKIE) and + self.properties[self.KEY] is None): + msg = (_('Property %(key)s is missing. ' + 'This property should be specified for ' + 'rules of %(header)s and %(cookie)s types.') % + {'key': self.KEY, + 'header': self.HEADER, + 'cookie': self.COOKIE}) + raise exception.StackValidationFailed(message=msg) + + def _prepare_args(self, properties): + props = dict((k, v) for k, v in properties.items() + if v is not None) + props.pop(self.L7POLICY) + return props + + def _resource_create(self, properties): + return self.client().l7rule_create(self.properties[self.L7POLICY], + json={'rule': properties})['rule'] + + def _resource_update(self, prop_diff): + self.client().l7rule_set(self.resource_id, + self.properties[self.L7POLICY], + json={'rule': prop_diff}) + + def _resource_delete(self): + self.client().l7rule_delete(self.resource_id, + self.properties[self.L7POLICY]) + + def _show_resource(self): + return self.client().l7rule_show(self.resource_id, + self.properties[self.L7POLICY]) + + +def resource_mapping(): + return { + 'OS::Octavia::L7Rule': L7Rule + } diff --git a/heat/tests/openstack/octavia/inline_templates.py b/heat/tests/openstack/octavia/inline_templates.py index fb94775f29..9fc65b3431 100644 --- a/heat/tests/openstack/octavia/inline_templates.py +++ b/heat/tests/openstack/octavia/inline_templates.py @@ -115,3 +115,19 @@ resources: listener: 123 position: 1 ''' + +L7RULE_TEMPLATE = ''' +heat_template_version: 2016-04-08 +description: Template to test L7Rule Neutron resource +resources: + l7rule: + type: OS::Octavia::L7Rule + properties: + admin_state_up: True + l7policy: 123 + type: HEADER + compare_type: ENDS_WITH + key: test_key + value: test_value + invert: False +''' diff --git a/heat/tests/openstack/octavia/test_l7rule.py b/heat/tests/openstack/octavia/test_l7rule.py new file mode 100644 index 0000000000..2d46d6fca1 --- /dev/null +++ b/heat/tests/openstack/octavia/test_l7rule.py @@ -0,0 +1,178 @@ +# +# 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 +import yaml + +from osc_lib import exceptions + +from heat.common import exception +from heat.common.i18n import _ +from heat.common import template_format +from heat.engine.resources.openstack.octavia import l7rule +from heat.tests import common +from heat.tests.openstack.octavia import inline_templates +from heat.tests import utils + + +class L7RuleTest(common.HeatTestCase): + + def test_resource_mapping(self): + mapping = l7rule.resource_mapping() + self.assertEqual(mapping['OS::Octavia::L7Rule'], + l7rule.L7Rule) + + def _create_stack(self, tmpl=inline_templates.L7RULE_TEMPLATE): + self.t = template_format.parse(tmpl) + self.stack = utils.parse_stack(self.t) + self.l7rule = self.stack['l7rule'] + + self.octavia_client = mock.MagicMock() + self.l7rule.client = mock.MagicMock( + return_value=self.octavia_client) + self.l7rule.client_plugin().client = mock.MagicMock( + return_value=self.octavia_client) + + def test_validate_when_key_required(self): + tmpl = yaml.safe_load(inline_templates.L7RULE_TEMPLATE) + props = tmpl['resources']['l7rule']['properties'] + del props['key'] + self._create_stack(tmpl=yaml.safe_dump(tmpl)) + + msg = _('Property key is missing. This property should be ' + 'specified for rules of HEADER and COOKIE types.') + with mock.patch('heat.engine.clients.os.neutron.NeutronClientPlugin.' + 'has_extension', return_value=True): + self.assertRaisesRegex(exception.StackValidationFailed, + msg, self.l7rule.validate) + + def test_create(self): + self._create_stack() + self.octavia_client.l7rule_show.side_effect = [ + {'provisioning_status': 'PENDING_CREATE'}, + {'provisioning_status': 'PENDING_CREATE'}, + {'provisioning_status': 'ACTIVE'}, + ] + self.octavia_client.l7rule_create.side_effect = [ + exceptions.Conflict(409), + {'rule': {'id': '1234'}} + ] + expected = { + 'rule': { + 'admin_state_up': True, + 'invert': False, + 'type': u'HEADER', + 'compare_type': u'ENDS_WITH', + 'key': u'test_key', + 'value': u'test_value', + 'invert': False + } + } + + props = self.l7rule.handle_create() + self.assertFalse(self.l7rule.check_create_complete(props)) + self.octavia_client.l7rule_create.assert_called_with('123', + json=expected) + self.assertFalse(self.l7rule.check_create_complete(props)) + self.octavia_client.l7rule_create.assert_called_with('123', + json=expected) + self.assertFalse(self.l7rule.check_create_complete(props)) + self.assertTrue(self.l7rule.check_create_complete(props)) + + def test_create_missing_properties(self): + for prop in ('l7policy', 'type', 'compare_type', 'value'): + tmpl = yaml.safe_load(inline_templates.L7RULE_TEMPLATE) + del tmpl['resources']['l7rule']['properties'][prop] + self._create_stack(tmpl=yaml.safe_dump(tmpl)) + + self.assertRaises(exception.StackValidationFailed, + self.l7rule.validate) + + def test_show_resource(self): + self._create_stack() + self.l7rule.resource_id_set('1234') + self.octavia_client.l7rule_show.return_value = {'id': '1234'} + + self.assertEqual({'id': '1234'}, self.l7rule._show_resource()) + + self.octavia_client.l7rule_show.assert_called_with('1234', '123') + + def test_update(self): + self._create_stack() + self.l7rule.resource_id_set('1234') + self.octavia_client.l7rule_show.side_effect = [ + {'provisioning_status': 'PENDING_UPDATE'}, + {'provisioning_status': 'PENDING_UPDATE'}, + {'provisioning_status': 'ACTIVE'}, + ] + self.octavia_client.l7rule_set.side_effect = [ + exceptions.Conflict(409), None] + prop_diff = { + 'admin_state_up': False, + 'name': 'your_l7policy', + 'redirect_url': 'http://www.google.com' + } + + prop_diff = self.l7rule.handle_update(None, None, prop_diff) + + self.assertFalse(self.l7rule.check_update_complete(prop_diff)) + self.assertFalse(self.l7rule._update_called) + self.octavia_client.l7rule_set.assert_called_with( + '1234', '123', json={'rule': prop_diff}) + self.assertFalse(self.l7rule.check_update_complete(prop_diff)) + self.assertTrue(self.l7rule._update_called) + self.octavia_client.l7rule_set.assert_called_with( + '1234', '123', json={'rule': prop_diff}) + self.assertFalse(self.l7rule.check_update_complete(prop_diff)) + self.assertTrue(self.l7rule.check_update_complete(prop_diff)) + + def test_delete(self): + self._create_stack() + self.l7rule.resource_id_set('1234') + self.octavia_client.l7rule_show.side_effect = [ + {'provisioning_status': 'PENDING_DELETE'}, + {'provisioning_status': 'PENDING_DELETE'}, + {'provisioning_status': 'DELETED'}, + ] + self.octavia_client.l7rule_delete.side_effect = [ + exceptions.Conflict(409), + None] + + self.l7rule.handle_delete() + + self.assertFalse(self.l7rule.check_delete_complete(None)) + self.assertFalse(self.l7rule._delete_called) + self.assertFalse(self.l7rule.check_delete_complete(None)) + self.assertTrue(self.l7rule._delete_called) + self.octavia_client.l7rule_delete.assert_called_with( + '1234', '123') + self.assertTrue(self.l7rule.check_delete_complete(None)) + + def test_delete_already_gone(self): + self._create_stack() + self.l7rule.resource_id_set('1234') + self.octavia_client.l7rule_delete.side_effect = ( + exceptions.NotFound(404)) + + self.l7rule.handle_delete() + self.assertTrue(self.l7rule.check_delete_complete(None)) + + def test_delete_failed(self): + self._create_stack() + self.l7rule.resource_id_set('1234') + self.octavia_client.l7rule_delete.side_effect = ( + exceptions.Unauthorized(401)) + + self.l7rule.handle_delete() + self.assertRaises(exceptions.Unauthorized, + self.l7rule.check_delete_complete, None) diff --git a/releasenotes/notes/octavia-resources-0a25720e16dfe55d.yaml b/releasenotes/notes/octavia-resources-0a25720e16dfe55d.yaml new file mode 100644 index 0000000000..54cc17cc46 --- /dev/null +++ b/releasenotes/notes/octavia-resources-0a25720e16dfe55d.yaml @@ -0,0 +1,19 @@ +--- +features: + - Adds new resources for octavia lbaas service. + - New resource ``OS::Octavia::LoadBalancer`` is added to create and + manage Load Balancers which allow traffic to be directed between servers. + - New resource ``OS::Octavia::Listener`` is added to create and + manage Listeners which represent a listening endpoint for the Load + Balancer. + - New resource ``OS::Octavia::Pool`` is added to create and + manage Pools which represent a group of nodes. Pools define the subnet + where nodes reside, the balancing algorithm, and the nodes themselves. + - New resource ``OS::Octavia::PoolMember`` is added to create and + manage Pool members which represent a single backend node. + - New resource ``OS::Octavia::HealthMonitor`` is added to create and + manage Health Monitors which watch status of the Load Balanced servers. + - New resource ``OS::Octavia::L7Policy`` is added to create and + manage L7 Policies. + - New resource ``OS::Octavia::L7Rule`` is added to create and + manage L7 Rules. diff --git a/setup.cfg b/setup.cfg index 1e73bdec9d..935093d175 100644 --- a/setup.cfg +++ b/setup.cfg @@ -150,6 +150,7 @@ heat.constraints = nova.server = heat.engine.clients.os.nova:ServerConstraint octavia.listener = heat.engine.clients.os.octavia:ListenerConstraint octavia.loadbalancer = heat.engine.clients.os.octavia:LoadbalancerConstraint + octavia.l7policy = heat.engine.clients.os.octavia:L7PolicyConstraint octavia.pool = heat.engine.clients.os.octavia:PoolConstraint sahara.cluster = heat.engine.clients.os.sahara:ClusterConstraint sahara.cluster_template = heat.engine.clients.os.sahara:ClusterTemplateConstraint