From eb05cacdb651e7d526d5a0a9106ee55f3a9b49b7 Mon Sep 17 00:00:00 2001 From: rabi Date: Tue, 19 Dec 2017 14:26:46 +0530 Subject: [PATCH] Add octavia HealthMonitor Resource Change-Id: I1e779c4c257f0a57c2f6fdafa5f6fabb72a4b967 Partial-Bug: #1737567 --- .../openstack/octavia/health_monitor.py | 170 ++++++++++++++++++ .../openstack/octavia/inline_templates.py | 18 ++ .../openstack/octavia/test_health_monitor.py | 149 +++++++++++++++ 3 files changed, 337 insertions(+) create mode 100644 heat/engine/resources/openstack/octavia/health_monitor.py create mode 100644 heat/tests/openstack/octavia/test_health_monitor.py diff --git a/heat/engine/resources/openstack/octavia/health_monitor.py b/heat/engine/resources/openstack/octavia/health_monitor.py new file mode 100644 index 0000000000..626658a7bd --- /dev/null +++ b/heat/engine/resources/openstack/octavia/health_monitor.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. + +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 HealthMonitor(octavia_base.OctaviaBase): + """A resource to handle load balancer health monitors. + + This resource creates and manages octavia healthmonitors, + which watches status of the load balanced servers. + """ + + # Properties inputs for the resources create/update. + PROPERTIES = ( + ADMIN_STATE_UP, DELAY, EXPECTED_CODES, HTTP_METHOD, + MAX_RETRIES, POOL, TIMEOUT, TYPE, URL_PATH, TENANT_ID + ) = ( + 'admin_state_up', 'delay', 'expected_codes', 'http_method', + 'max_retries', 'pool', 'timeout', 'type', 'url_path', 'tenant_id' + ) + + # Supported HTTP methods + HTTP_METHODS = ( + GET, HEAT, POST, PUT, DELETE, TRACE, OPTIONS, + CONNECT, PATCH + ) = ( + 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'TRACE', 'OPTIONS', + 'CONNECT', 'PATCH' + ) + + # Supported output attributes of the resources. + ATTRIBUTES = (POOLS_ATTR) = ('pools') + + properties_schema = { + ADMIN_STATE_UP: properties.Schema( + properties.Schema.BOOLEAN, + _('The administrative state of the health monitor.'), + default=True, + update_allowed=True + ), + DELAY: properties.Schema( + properties.Schema.INTEGER, + _('The minimum time in milliseconds between regular connections ' + 'of the member.'), + required=True, + update_allowed=True, + constraints=[constraints.Range(min=0)] + ), + EXPECTED_CODES: properties.Schema( + properties.Schema.STRING, + _('The HTTP status codes expected in response from the ' + 'member to declare it healthy. Specify one of the following ' + 'values: a single value, such as 200. a list, such as 200, 202. ' + 'a range, such as 200-204.'), + update_allowed=True, + default='200' + ), + HTTP_METHOD: properties.Schema( + properties.Schema.STRING, + _('The HTTP method used for requests by the monitor of type ' + 'HTTP.'), + update_allowed=True, + default=GET, + constraints=[constraints.AllowedValues(HTTP_METHODS)] + ), + MAX_RETRIES: properties.Schema( + properties.Schema.INTEGER, + _('Number of permissible connection failures before changing the ' + 'member status to INACTIVE.'), + required=True, + update_allowed=True, + constraints=[constraints.Range(min=1, max=10)], + ), + POOL: properties.Schema( + properties.Schema.STRING, + _('ID or name of the load balancing pool.'), + required=True, + constraints=[ + constraints.CustomConstraint('octavia.pool') + ] + ), + TIMEOUT: properties.Schema( + properties.Schema.INTEGER, + _('Maximum number of milliseconds for a monitor to wait for a ' + 'connection to be established before it times out.'), + required=True, + update_allowed=True, + constraints=[constraints.Range(min=0)] + ), + TYPE: properties.Schema( + properties.Schema.STRING, + _('One of predefined health monitor types.'), + required=True, + constraints=[ + constraints.AllowedValues(['PING', 'TCP', 'HTTP', 'HTTPS']), + ] + ), + URL_PATH: properties.Schema( + properties.Schema.STRING, + _('The HTTP path used in the HTTP request used by the monitor to ' + 'test a member health. A valid value is a string the begins ' + 'with a forward slash (/).'), + update_allowed=True, + default='/' + ), + TENANT_ID: properties.Schema( + properties.Schema.STRING, + _('ID of the tenant who owns the health monitor.') + ) + } + + attributes_schema = { + POOLS_ATTR: attributes.Schema( + _('The list of Pools related to this monitor.'), + type=attributes.Schema.LIST + ) + } + + def translation_rules(self, props): + return [ + translation.TranslationRule( + props, + translation.TranslationRule.RESOLVE, + [self.POOL], + client_plugin=self.client_plugin(), + finder='get_pool', + ), + ] + + def _prepare_args(self, properties): + props = dict((k, v) for k, v in properties.items() if v is not None) + if self.POOL in props: + props['pool_id'] = props.pop(self.POOL) + return props + + def _resource_create(self, properties): + return self.client().health_monitor_create( + json={'healthmonitor': properties})['healthmonitor'] + + def _resource_update(self, prop_diff): + self.client().health_monitor_set( + self.resource_id, json={'healthmonitor': prop_diff}) + + def _resource_delete(self): + self.client().health_monitor_delete(self.resource_id) + + def _show_resource(self): + return self.client().health_monitor_show(self.resource_id) + + +def resource_mapping(): + return { + 'OS::Octavia::HealthMonitor': HealthMonitor, + } diff --git a/heat/tests/openstack/octavia/inline_templates.py b/heat/tests/openstack/octavia/inline_templates.py index 2dc40b7aa1..6031634979 100644 --- a/heat/tests/openstack/octavia/inline_templates.py +++ b/heat/tests/openstack/octavia/inline_templates.py @@ -81,3 +81,21 @@ resources: subnet: sub123 admin_state_up: True ''' + +MONITOR_TEMPLATE = ''' +heat_template_version: 2016-04-08 +description: Create a health monitor +resources: + monitor: + type: OS::Octavia::HealthMonitor + properties: + admin_state_up: True + delay: 3 + expected_codes: 200-202 + http_method: HEAD + max_retries: 5 + pool: 123 + timeout: 10 + type: HTTP + url_path: /health +''' diff --git a/heat/tests/openstack/octavia/test_health_monitor.py b/heat/tests/openstack/octavia/test_health_monitor.py new file mode 100644 index 0000000000..8804738815 --- /dev/null +++ b/heat/tests/openstack/octavia/test_health_monitor.py @@ -0,0 +1,149 @@ +# +# 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 osc_lib import exceptions + +from heat.common import template_format +from heat.engine.resources.openstack.octavia import health_monitor +from heat.tests import common +from heat.tests.openstack.octavia import inline_templates +from heat.tests import utils + + +class HealthMonitorTest(common.HeatTestCase): + + def _create_stack(self, tmpl=inline_templates.MONITOR_TEMPLATE): + self.t = template_format.parse(tmpl) + self.stack = utils.parse_stack(self.t) + self.healthmonitor = self.stack['monitor'] + + self.octavia_client = mock.MagicMock() + self.healthmonitor.client = mock.MagicMock( + return_value=self.octavia_client) + self.healthmonitor.client_plugin().client = mock.MagicMock( + return_value=self.octavia_client) + + def test_resource_mapping(self): + mapping = health_monitor.resource_mapping() + self.assertEqual(health_monitor.HealthMonitor, + mapping['OS::Octavia::HealthMonitor']) + + def test_create(self): + self._create_stack() + self.octavia_client.health_monitor_show.side_effect = [ + {'provisioning_status': 'PENDING_CREATE'}, + {'provisioning_status': 'PENDING_CREATE'}, + {'provisioning_status': 'ACTIVE'}, + ] + self.octavia_client.health_monitor_create.side_effect = [ + exceptions.Conflict(409), {'healthmonitor': {'id': '1234'}} + ] + expected = { + 'healthmonitor': { + 'admin_state_up': True, + 'delay': 3, + 'expected_codes': '200-202', + 'http_method': 'HEAD', + 'max_retries': 5, + 'pool_id': '123', + 'timeout': 10, + 'type': 'HTTP', + 'url_path': '/health' + } + } + + props = self.healthmonitor.handle_create() + + self.assertFalse(self.healthmonitor.check_create_complete(props)) + self.octavia_client.health_monitor_create.assert_called_with( + json=expected) + self.assertFalse(self.healthmonitor.check_create_complete(props)) + self.octavia_client.health_monitor_create.assert_called_with( + json=expected) + self.assertFalse(self.healthmonitor.check_create_complete(props)) + self.assertTrue(self.healthmonitor.check_create_complete(props)) + + def test_show_resource(self): + self._create_stack() + self.healthmonitor.resource_id_set('1234') + + self.assertTrue(self.healthmonitor._show_resource()) + + self.octavia_client.health_monitor_show.assert_called_with( + '1234') + + def test_update(self): + self._create_stack() + self.healthmonitor.resource_id_set('1234') + self.octavia_client.health_monitor_show.side_effect = [ + {'provisioning_status': 'PENDING_UPDATE'}, + {'provisioning_status': 'PENDING_UPDATE'}, + {'provisioning_status': 'ACTIVE'}, + ] + self.octavia_client.health_monitor_set.side_effect = [ + exceptions.Conflict(409), None] + prop_diff = { + 'admin_state_up': False, + } + + prop_diff = self.healthmonitor.handle_update(None, None, prop_diff) + + self.assertFalse(self.healthmonitor.check_update_complete(prop_diff)) + self.assertFalse(self.healthmonitor._update_called) + self.octavia_client.health_monitor_set.assert_called_with( + '1234', json={'healthmonitor': prop_diff}) + self.assertFalse(self.healthmonitor.check_update_complete(prop_diff)) + self.assertTrue(self.healthmonitor._update_called) + self.octavia_client.health_monitor_set.assert_called_with( + '1234', json={'healthmonitor': prop_diff}) + self.assertFalse(self.healthmonitor.check_update_complete(prop_diff)) + self.assertTrue(self.healthmonitor.check_update_complete(prop_diff)) + + def test_delete(self): + self._create_stack() + self.healthmonitor.resource_id_set('1234') + self.octavia_client.health_monitor_show.side_effect = [ + {'provisioning_status': 'PENDING_DELETE'}, + {'provisioning_status': 'PENDING_DELETE'}, + {'provisioning_status': 'DELETED'}, + ] + self.octavia_client.health_monitor_delete.side_effect = [ + exceptions.Conflict(409), + None] + + self.healthmonitor.handle_delete() + + self.assertFalse(self.healthmonitor.check_delete_complete(None)) + self.assertFalse(self.healthmonitor._delete_called) + self.octavia_client.health_monitor_delete.assert_called_with( + '1234') + self.assertFalse(self.healthmonitor.check_delete_complete(None)) + self.assertTrue(self.healthmonitor._delete_called) + self.octavia_client.health_monitor_delete.assert_called_with( + '1234') + self.assertTrue(self.healthmonitor.check_delete_complete(None)) + + def test_delete_failed(self): + self._create_stack() + self.healthmonitor.resource_id_set('1234') + self.octavia_client.health_monitor_delete.side_effect = ( + exceptions.Unauthorized(401)) + + self.healthmonitor.handle_delete() + self.assertRaises(exceptions.Unauthorized, + self.healthmonitor.check_delete_complete, None) + + self.octavia_client.health_monitor_delete.assert_called_with( + '1234')