From 6e61991833a64c9e9b8329e80a50609168fc4083 Mon Sep 17 00:00:00 2001 From: ZhaoBo Date: Mon, 6 Aug 2018 22:35:13 +0800 Subject: [PATCH] Support HTTP and TCP checks in UDP healthmonitor This patch introduces 2 macros in lvs. 1. Support HTTP GET, allow users create HTTP healthmonitor for udp pool. 2. Support TCP check, allow users create TCP healthmonitor for udp pool. Co-Authored-By: Adam Harwell Change-Id: I61c7d8d4df54710a92b8c055be84bba29bf3d7e6 Story: 2003200 Task: 23356 Story: 2003199 Task: 23355 --- api-ref/source/v2/general.inc | 49 +++++++- .../backends/utils/keepalivedlvs_query.py | 2 +- octavia/api/root_controller.py | 6 +- octavia/api/v2/controllers/health_monitor.py | 42 ++++--- octavia/api/v2/controllers/pool.py | 15 ++- .../haproxy/combined_listeners/jinja_cfg.py | 24 +--- .../haproxy/split_listeners/jinja_cfg.py | 24 +--- octavia/common/jinja/lvs/jinja_cfg.py | 16 ++- octavia/common/jinja/lvs/templates/macros.j2 | 36 +++++- octavia/common/utils.py | 21 ++++ .../functional/api/test_root_controller.py | 3 +- .../functional/api/v2/test_health_monitor.py | 83 +++++++++---- .../functional/api/v2/test_load_balancer.py | 109 +++++++++++++++--- .../combined_listeners/test_jinja_cfg.py | 29 ----- .../haproxy/split_listeners/test_jinja_cfg.py | 29 ----- .../common/jinja/lvs/test_lvs_jinja_cfg.py | 104 +++++++++++++++++ .../sample_configs/sample_configs_combined.py | 18 ++- .../sample_configs/sample_configs_split.py | 20 +++- octavia/tests/unit/common/test_utils.py | 29 +++++ ...dp-healthcheck-types-2414a5edee9f5110.yaml | 5 + 20 files changed, 470 insertions(+), 194 deletions(-) create mode 100644 releasenotes/notes/additional-udp-healthcheck-types-2414a5edee9f5110.yaml diff --git a/api-ref/source/v2/general.inc b/api-ref/source/v2/general.inc index 2b17751e69..88965a6d98 100644 --- a/api-ref/source/v2/general.inc +++ b/api-ref/source/v2/general.inc @@ -573,13 +573,13 @@ recreated. .. _valid_protocol: -Protocol Combinations -===================== +Protocol Combinations (Listener/Pool) +===================================== The listener and pool can be associated through the listener's ``default_pool_id`` or l7policy's ``redirect_pool_id``. Both listener and pool -must set the protocol parameter. But the association between the listener and -the pool isn't arbitrarily and has some constraints at the protocol aspect. +must set the protocol parameter, but the association between the listener and +the pool isn't arbitrary and has some constraints on the protocol aspect. Valid protocol combinations --------------------------- @@ -621,3 +621,44 @@ The pool protocol of PROXY will use the listener protocol as the pool protocol but will wrap that protocol in the proxy protocol. In the case of listener protocol TERMINATED_HTTPS, a pool protocol of PROXY will be HTTP wrapped in the proxy protocol. + +Protocol Combinations (Pool/Health Monitor) +=========================================== + +Pools and health monitors are also related with regard to protocol. Pools set +the protocol parameter for the real member connections, and the health monitor +sets a type for health checks. Health check types are limited based on the +protocol of the pool. + +Valid protocol combinations +--------------------------- + +.. |Health Monitor| replace:: |2| |2| Health Monitor +.. |UDPCONNECT| replace:: UDP-CONNECT +.. |4Y| replace:: |2| |2| Y +.. |4N| replace:: |2| |2| N +.. |5Y| replace:: |2| |2| |1| Y +.. |5N| replace:: |2| |2| |1| N + ++-------------------+-------+--------+-------+------+------------+---------------+ +|| |Health Monitor| || HTTP || HTTPS || PING || TCP || TLS-HELLO || |UDPCONNECT| | +|| Pool || || || || || || | ++===================+=======+========+=======+======+============+===============+ +| HTTP | |2Y| | |2Y| | |1Y| | |1Y| | |4Y| | |5N| | ++-------------------+-------+--------+-------+------+------------+---------------+ +| HTTPS | |2Y| | |2Y| | |1Y| | |1Y| | |4Y| | |5N| | ++-------------------+-------+--------+-------+------+------------+---------------+ +| PROXY | |2Y| | |2Y| | |1Y| | |1Y| | |4Y| | |5N| | ++-------------------+-------+--------+-------+------+------------+---------------+ +| TCP | |2Y| | |2Y| | |1Y| | |1Y| | |4Y| | |5N| | ++-------------------+-------+--------+-------+------+------------+---------------+ +| UDP | |2Y| | |2N| | |1N| | |1Y| | |4N| | |5Y| | ++-------------------+-------+--------+-------+------+------------+---------------+ + +"Y" means the combination is valid and "N" means invalid. + +These combinations are mostly as you'd expect for all non-UDP pool protocols: +non-UDP pools can have health monitors with any check type besides UDP-CONNECT. +For UDP pools however, things are a little more complicated. UDP Pools support +UDP-CONNECT but also HTTP and TCP checks. HTTPS checks are technically feasible +but have not yet been implemented. diff --git a/octavia/amphorae/backends/utils/keepalivedlvs_query.py b/octavia/amphorae/backends/utils/keepalivedlvs_query.py index 73a953aadc..390bb4eb93 100644 --- a/octavia/amphorae/backends/utils/keepalivedlvs_query.py +++ b/octavia/amphorae/backends/utils/keepalivedlvs_query.py @@ -37,7 +37,7 @@ CONFIG_COMMENT_REGEX = re.compile( DISABLED_MEMBER_COMMENT_REGEX = re.compile( r"#\sMember\s(\w{8}-\w{4}-\w{4}-\w{4}-\w{12}) is disabled") -CHECKER_REGEX = re.compile(r"MISC_CHECK") +CHECKER_REGEX = re.compile(r"(MISC_CHECK|HTTP_GET|TCP_CHECK)") def read_kernel_file(ns_name, file_path): diff --git a/octavia/api/root_controller.py b/octavia/api/root_controller.py index 2fb9384bd0..6ae62e7df3 100644 --- a/octavia/api/root_controller.py +++ b/octavia/api/root_controller.py @@ -109,8 +109,10 @@ class RootController(object): # Availability Zones self._add_a_version(versions, 'v2.14', 'v2', 'SUPPORTED', '2019-11-10T00:00:00Z', host_url) - # TLS version and cipher options - self._add_a_version(versions, 'v2.15', 'v2', 'CURRENT', + self._add_a_version(versions, 'v2.15', 'v2', 'SUPPORTED', '2020-03-10T00:00:00Z', host_url) + # Additional UDP Healthcheck Types (HTTP/TCP) + self._add_a_version(versions, 'v2.16', 'v2', 'CURRENT', + '2020-03-15T00:00:00Z', host_url) return {'versions': versions} diff --git a/octavia/api/v2/controllers/health_monitor.py b/octavia/api/v2/controllers/health_monitor.py index 8285a9422a..dae0002c27 100644 --- a/octavia/api/v2/controllers/health_monitor.py +++ b/octavia/api/v2/controllers/health_monitor.py @@ -165,31 +165,28 @@ class HealthMonitorController(base.BaseController): raise exceptions.InvalidOption(value='', option='') def _validate_healthmonitor_request_for_udp(self, request): - invalid_fields = (request.http_method or request.url_path or - request.expected_codes) - is_invalid = (hasattr(request, 'type') and - (request.type != consts.HEALTH_MONITOR_UDP_CONNECT or - invalid_fields)) - if is_invalid: + if request.type not in ( + consts.HEALTH_MONITOR_UDP_CONNECT, + consts.HEALTH_MONITOR_TCP, + consts.HEALTH_MONITOR_HTTP): raise exceptions.ValidationException(detail=_( "The associated pool protocol is %(pool_protocol)s, so only " - "a %(type)s health monitor is supported.") % { - 'pool_protocol': consts.PROTOCOL_UDP, - 'type': consts.HEALTH_MONITOR_UDP_CONNECT}) - # if the logic arrives here, that means the validation of request above - # is OK. type is UDP-CONNECT, then here we check the healthmonitor - # delay value is matched. - if request.delay: - conf_set = (CONF.api_settings. - udp_connect_min_interval_health_monitor) - if conf_set < 0: - return - if request.delay < conf_set: - raise exceptions.ValidationException(detail=_( - "The request delay value %(delay)s should be larger than " - "%(conf_set)s for %(type)s health monitor type.") % { + "a %(types)s health monitor is supported.") % { + 'pool_protocol': consts.PROTOCOL_UDP, + 'types': '/'.join((consts.HEALTH_MONITOR_UDP_CONNECT, + consts.HEALTH_MONITOR_TCP, + consts.HEALTH_MONITOR_HTTP))}) + # check the delay value if the HM type is UDP-CONNECT + hm_is_type_udp = ( + request.type == consts.HEALTH_MONITOR_UDP_CONNECT) + conf_min_delay = ( + CONF.api_settings.udp_connect_min_interval_health_monitor) + if hm_is_type_udp and request.delay < conf_min_delay: + raise exceptions.ValidationException(detail=_( + "The request delay value %(delay)s should be larger than " + "%(conf_min_delay)s for %(type)s health monitor type.") % { 'delay': request.delay, - 'conf_set': conf_set, + 'conf_min_delay': conf_min_delay, 'type': consts.HEALTH_MONITOR_UDP_CONNECT}) @wsme_pecan.wsexpose(hm_types.HealthMonitorRootResponse, @@ -348,6 +345,7 @@ class HealthMonitorController(base.BaseController): # Validate health monitor update options for UDP-CONNECT type. if (pool.protocol == consts.PROTOCOL_UDP and db_hm.type == consts.HEALTH_MONITOR_UDP_CONNECT): + health_monitor.type = db_hm.type self._validate_healthmonitor_request_for_udp(health_monitor) self._set_default_on_none(health_monitor) diff --git a/octavia/api/v2/controllers/pool.py b/octavia/api/v2/controllers/pool.py index 9528a2b8df..ce6e9f4f7c 100644 --- a/octavia/api/v2/controllers/pool.py +++ b/octavia/api/v2/controllers/pool.py @@ -287,12 +287,21 @@ class PoolsController(base.BaseController): resource=data_models.HealthMonitor._name()) # Now possibly create a healthmonitor - new_hm = None if hm: - hm['pool_id'] = db_pool.id - hm['project_id'] = db_pool.project_id + hm[constants.POOL_ID] = db_pool.id + hm[constants.PROJECT_ID] = db_pool.project_id new_hm = health_monitor.HealthMonitorController()._graph_create( lock_session, hm) + if db_pool.protocol == constants.PROTOCOL_UDP: + health_monitor.HealthMonitorController( + )._validate_healthmonitor_request_for_udp(new_hm) + else: + if new_hm.type == constants.HEALTH_MONITOR_UDP_CONNECT: + raise exceptions.ValidationException(detail=_( + "The %(type)s type is only supported for pools of " + "type %(protocol)s.") % { + 'type': new_hm.type, + 'protocol': constants.PROTOCOL_UDP}) db_pool.health_monitor = new_hm # Now check quotas for members diff --git a/octavia/common/jinja/haproxy/combined_listeners/jinja_cfg.py b/octavia/common/jinja/haproxy/combined_listeners/jinja_cfg.py index 39fcc7e3f5..9e59df0bbf 100644 --- a/octavia/common/jinja/haproxy/combined_listeners/jinja_cfg.py +++ b/octavia/common/jinja/haproxy/combined_listeners/jinja_cfg.py @@ -398,7 +398,7 @@ class JinjaTemplater(object): """ codes = None if monitor.expected_codes: - codes = '|'.join(self._expand_expected_codes( + codes = '|'.join(octavia_utils.expand_expected_codes( monitor.expected_codes)) return { 'id': monitor.id, @@ -479,25 +479,3 @@ class JinjaTemplater(object): # Spaces next value = re.sub(' ', '\\ ', value) return value - - @staticmethod - def _expand_expected_codes(codes): - """Expand the expected code string in set of codes. - - 200-204 -> 200, 201, 202, 204 - 200, 203 -> 200, 203 - """ - - retval = set() - for code in codes.replace(',', ' ').split(' '): - code = code.strip() - - if not code: - continue - if '-' in code: - low, hi = code.split('-')[:2] - retval.update( - str(i) for i in range(int(low), int(hi) + 1)) - else: - retval.add(code) - return sorted(retval) diff --git a/octavia/common/jinja/haproxy/split_listeners/jinja_cfg.py b/octavia/common/jinja/haproxy/split_listeners/jinja_cfg.py index 9d6287b7ab..732b1f4e30 100644 --- a/octavia/common/jinja/haproxy/split_listeners/jinja_cfg.py +++ b/octavia/common/jinja/haproxy/split_listeners/jinja_cfg.py @@ -382,7 +382,7 @@ class JinjaTemplater(object): """ codes = None if monitor.expected_codes: - codes = '|'.join(self._expand_expected_codes( + codes = '|'.join(octavia_utils.expand_expected_codes( monitor.expected_codes)) return { 'id': monitor.id, @@ -463,25 +463,3 @@ class JinjaTemplater(object): # Spaces next value = re.sub(' ', '\\ ', value) return value - - @staticmethod - def _expand_expected_codes(codes): - """Expand the expected code string in set of codes. - - 200-204 -> 200, 201, 202, 204 - 200, 203 -> 200, 203 - """ - - retval = set() - for code in codes.replace(',', ' ').split(' '): - code = code.strip() - - if not code: - continue - if '-' in code: - low, hi = code.split('-')[:2] - retval.update( - str(i) for i in range(int(low), int(hi) + 1)) - else: - retval.add(code) - return sorted(retval) diff --git a/octavia/common/jinja/lvs/jinja_cfg.py b/octavia/common/jinja/lvs/jinja_cfg.py index 67c427f7fe..59bba328d1 100644 --- a/octavia/common/jinja/lvs/jinja_cfg.py +++ b/octavia/common/jinja/lvs/jinja_cfg.py @@ -18,6 +18,7 @@ import jinja2 from octavia.common.config import cfg from octavia.common import constants +from octavia.common import utils as octavia_utils CONF = cfg.CONF @@ -194,7 +195,7 @@ class LvsJinjaTemplater(object): be processed by the templating system """ - return { + return_val = { 'id': monitor.id, 'type': monitor.type, 'delay': monitor.delay, @@ -206,3 +207,16 @@ class LvsJinjaTemplater(object): constants.HEALTH_MONITOR_UDP_CONNECT else None) } + if monitor.type == constants.HEALTH_MONITOR_HTTP: + return_val.update({ + 'rise_threshold': monitor.rise_threshold, + 'url_path': monitor.url_path, + 'http_method': (monitor.http_method + if monitor.http_method == + constants.HEALTH_MONITOR_HTTP_METHOD_GET else + None), + 'expected_codes': (sorted(list( + octavia_utils.expand_expected_codes( + monitor.expected_codes))) + if monitor.expected_codes else [])}) + return return_val diff --git a/octavia/common/jinja/lvs/templates/macros.j2 b/octavia/common/jinja/lvs/templates/macros.j2 index 2c719c3cd4..73d5a5cb7d 100644 --- a/octavia/common/jinja/lvs/templates/macros.j2 +++ b/octavia/common/jinja/lvs/templates/macros.j2 @@ -29,11 +29,41 @@ MISC_CHECK { } {%- endmacro -%} +{%- macro http_url_macro(health_monitor, health_monitor_status_code) %} +url { + path {{ health_monitor.url_path }} + status_code {{ health_monitor_status_code }} + } +{% endmacro -%} + +{%- macro http_get_macro(pool, member, health_monitor) -%} +HTTP_GET { + {% for status_code in health_monitor.expected_codes %} + {{ http_url_macro(health_monitor, status_code) -}} + {% endfor %} + connect_ip {{ member.monitor_address|default(member.address, true) }} + connect_port {{ member.monitor_port|default(member.protocol_port, true) }} + connect_timeout {{ health_monitor.timeout }} + } +{%- endmacro -%} + +{%- macro tcp_check_macro(pool, member, health_monitor) -%} +TCP_CHECK { + connect_ip {{ member.monitor_address|default(member.address, true) }} + connect_port {{ member.monitor_port|default(member.protocol_port, true) }} + connect_timeout {{ health_monitor.timeout }} + } +{%- endmacro -%} + {% macro health_monitor_rs_macro(constants, pool, member) %} {% if pool.health_monitor and pool.health_monitor.enabled %} - {% if pool.health_monitor.type == constants.HEALTH_MONITOR_UDP_CONNECT %} + {% if pool.health_monitor.type == constants.HEALTH_MONITOR_UDP_CONNECT %} {{ misc_check_macro(pool, member, pool.health_monitor) -}} - {% endif %} + {% elif pool.health_monitor.type == constants.HEALTH_MONITOR_HTTP and pool.health_monitor.http_method == constants.HEALTH_MONITOR_HTTP_METHOD_GET %} + {{ http_get_macro(pool, member, pool.health_monitor) -}} + {% elif pool.health_monitor.type == constants.HEALTH_MONITOR_TCP %} + {{ tcp_check_macro(pool, member, pool.health_monitor) -}} + {% endif %} {% endif %} {% endmacro %} @@ -54,10 +84,8 @@ MISC_CHECK { {% macro health_monitor_vs_macro(default_pool) %} {% if default_pool and default_pool.health_monitor and default_pool.health_monitor.enabled %} - {% if default_pool.health_monitor.delay %} delay_loop {{ default_pool.health_monitor.delay }} delay_before_retry {{ default_pool.health_monitor.delay }} - {% endif %} {% if default_pool.health_monitor.fall_threshold %} retry {{ default_pool.health_monitor.fall_threshold }} {% endif %} diff --git a/octavia/common/utils.py b/octavia/common/utils.py index 3082fd1301..fde334d09c 100644 --- a/octavia/common/utils.py +++ b/octavia/common/utils.py @@ -20,6 +20,7 @@ import base64 import hashlib +import re import socket import netaddr @@ -120,6 +121,26 @@ def b(s): return s.encode('utf-8') +def expand_expected_codes(codes): + """Expand the expected code string in set of codes. + + 200-204 -> 200, 201, 202, 204 + 200, 203 -> 200, 203 + """ + retval = set() + codes = re.split(', *', codes) + for code in codes: + if not code: + continue + if '-' in code: + low, hi = code.split('-')[:2] + retval.update( + str(i) for i in range(int(low), int(hi) + 1)) + else: + retval.add(code) + return retval + + class exception_logger(object): """Wrap a function and log raised exception diff --git a/octavia/tests/functional/api/test_root_controller.py b/octavia/tests/functional/api/test_root_controller.py index 7c60224ae3..7e1af13789 100644 --- a/octavia/tests/functional/api/test_root_controller.py +++ b/octavia/tests/functional/api/test_root_controller.py @@ -45,7 +45,7 @@ class TestRootController(base_db_test.OctaviaDBTestBase): def test_api_versions(self): versions = self._get_versions_with_config() version_ids = tuple(v.get('id') for v in versions) - self.assertEqual(16, len(version_ids)) + self.assertEqual(17, len(version_ids)) self.assertIn('v2.0', version_ids) self.assertIn('v2.1', version_ids) self.assertIn('v2.2', version_ids) @@ -62,6 +62,7 @@ class TestRootController(base_db_test.OctaviaDBTestBase): self.assertIn('v2.13', version_ids) self.assertIn('v2.14', version_ids) self.assertIn('v2.15', version_ids) + self.assertIn('v2.16', version_ids) # Each version should have a 'self' 'href' to the API version URL # [{u'rel': u'self', u'href': u'http://localhost/v2'}] diff --git a/octavia/tests/functional/api/v2/test_health_monitor.py b/octavia/tests/functional/api/v2/test_health_monitor.py index 11d7f60622..b547323cd1 100644 --- a/octavia/tests/functional/api/v2/test_health_monitor.py +++ b/octavia/tests/functional/api/v2/test_health_monitor.py @@ -801,7 +801,8 @@ class TestHealthMonitor(base.BaseAPITest): self.assertEqual('/', api_hm.get('url_path')) self.assertEqual('200', api_hm.get('expected_codes')) - def test_create_udp_case(self): + def test_create_udp_case_with_udp_connect_type(self): + # create with UDP-CONNECT type api_hm = self.create_health_monitor( self.udp_pool_with_listener_id, constants.HEALTH_MONITOR_UDP_CONNECT, @@ -826,6 +827,56 @@ class TestHealthMonitor(base.BaseAPITest): self.assertIsNone(api_hm.get('url_path')) self.assertIsNone(api_hm.get('expected_codes')) + def test_create_udp_case_with_tcp_type(self): + # create with TCP type + api_hm = self.create_health_monitor( + self.udp_pool_with_listener_id, constants.HEALTH_MONITOR_TCP, + 3, 1, 1, 1).get(self.root_tag) + self.assert_correct_status( + lb_id=self.udp_lb_id, listener_id=self.udp_listener_id, + pool_id=self.udp_pool_with_listener_id, hm_id=api_hm.get('id'), + lb_prov_status=constants.PENDING_UPDATE, + listener_prov_status=constants.PENDING_UPDATE, + pool_prov_status=constants.PENDING_UPDATE, + hm_prov_status=constants.PENDING_CREATE, + hm_op_status=constants.OFFLINE) + self.set_lb_status(self.udp_lb_id) + self.assertEqual(constants.HEALTH_MONITOR_TCP, api_hm.get('type')) + self.assertEqual(3, api_hm.get('delay')) + self.assertEqual(1, api_hm.get('timeout')) + self.assertEqual(1, api_hm.get('max_retries_down')) + self.assertEqual(1, api_hm.get('max_retries')) + self.assertIsNone(api_hm.get('http_method')) + self.assertIsNone(api_hm.get('url_path')) + self.assertIsNone(api_hm.get('expected_codes')) + + def test_create_udp_case_with_http_type(self): + # create with HTTP type + api_hm = self.create_health_monitor( + self.udp_pool_with_listener_id, constants.HEALTH_MONITOR_HTTP, + 3, 1, 1, 1, url_path='/test.html', + http_method=constants.HEALTH_MONITOR_HTTP_METHOD_GET, + expected_codes='200-201').get(self.root_tag) + self.assert_correct_status( + lb_id=self.udp_lb_id, listener_id=self.udp_listener_id, + pool_id=self.udp_pool_with_listener_id, hm_id=api_hm.get('id'), + lb_prov_status=constants.PENDING_UPDATE, + listener_prov_status=constants.PENDING_UPDATE, + pool_prov_status=constants.PENDING_UPDATE, + hm_prov_status=constants.PENDING_CREATE, + hm_op_status=constants.OFFLINE) + self.set_lb_status(self.udp_lb_id) + self.assertEqual(constants.HEALTH_MONITOR_HTTP, api_hm.get('type')) + self.assertEqual(3, api_hm.get('delay')) + self.assertEqual(1, api_hm.get('timeout')) + self.assertEqual(1, api_hm.get('max_retries_down')) + self.assertEqual(1, api_hm.get('max_retries')) + self.assertEqual(3, api_hm.get('delay')) + self.assertEqual(constants.HEALTH_MONITOR_HTTP_METHOD_GET, + api_hm.get('http_method')) + self.assertEqual('/test.html', api_hm.get('url_path')) + self.assertEqual('200-201', api_hm.get('expected_codes')) + def test_udp_case_when_udp_connect_min_interval_health_monitor_set(self): # negative case first req_dict = {'pool_id': self.udp_pool_with_listener_id, @@ -867,13 +918,15 @@ class TestHealthMonitor(base.BaseAPITest): 'max_retries_down': 1, 'max_retries': 1} expect_error_msg = ("Validation failure: The associated pool protocol " - "is %(pool_protocol)s, so only a %(type)s health " + "is %(pool_protocol)s, so only a %(types)s health " "monitor is supported.") % { 'pool_protocol': constants.PROTOCOL_UDP, - 'type': constants.HEALTH_MONITOR_UDP_CONNECT} + 'types': '/'.join([constants.HEALTH_MONITOR_UDP_CONNECT, + constants.HEALTH_MONITOR_TCP, + constants.HEALTH_MONITOR_HTTP])} - # Not allowed types, url_path, expected_codes specified. - update_req = {'type': constants.HEALTH_MONITOR_TCP} + # Not allowed types specified. + update_req = {'type': constants.HEALTH_MONITOR_TLS_HELLO} req_dict.update(update_req) res = self.post(self.HMS_PATH, self._build_body(req_dict), status=400, expect_errors=True) @@ -882,22 +935,6 @@ class TestHealthMonitor(base.BaseAPITest): lb_id=self.udp_lb_id, listener_id=self.udp_listener_id, pool_id=self.udp_pool_with_listener_id) - update_req = {'type': constants.HEALTH_MONITOR_UDP_CONNECT} - req_dict.update(update_req) - for req in [{'http_method': - constants.HEALTH_MONITOR_HTTP_METHOD_GET}, - {'url_path': constants.HEALTH_MONITOR_DEFAULT_URL_PATH}, - {'expected_codes': - constants.HEALTH_MONITOR_DEFAULT_EXPECTED_CODES}]: - req_dict.update(req) - res = self.post(self.HMS_PATH, self._build_body(req_dict), - status=400, - expect_errors=True) - self.assertEqual(expect_error_msg, res.json['faultstring']) - self.assert_correct_status( - lb_id=self.udp_lb_id, listener_id=self.udp_listener_id, - pool_id=self.udp_pool_with_listener_id) - # Hit error during create with a non-UDP pool req_dict = {'pool_id': self.pool_with_listener_id, 'delay': 1, @@ -1458,10 +1495,10 @@ class TestHealthMonitor(base.BaseAPITest): def test_update_udp_case(self): api_hm = self.create_health_monitor( self.udp_pool_with_listener_id, - constants.HEALTH_MONITOR_UDP_CONNECT, 3, 1, 1, 1).get( + constants.HEALTH_MONITOR_TCP, 3, 1, 1, 1).get( self.root_tag) self.set_lb_status(self.udp_lb_id) - new_hm = {'max_retries': 2} + new_hm = {'timeout': 2} self.put( self.HM_PATH.format(healthmonitor_id=api_hm.get('id')), self._build_body(new_hm)) diff --git a/octavia/tests/functional/api/v2/test_load_balancer.py b/octavia/tests/functional/api/v2/test_load_balancer.py index 30d2301a08..d8136ca39c 100644 --- a/octavia/tests/functional/api/v2/test_load_balancer.py +++ b/octavia/tests/functional/api/v2/test_load_balancer.py @@ -2724,24 +2724,56 @@ class TestLoadBalancerGraph(base.BaseAPITest): expected_member.update(create_member) return create_member, expected_member - def _get_hm_bodies(self): - create_hm = { - 'type': constants.HEALTH_MONITOR_PING, - 'delay': 1, - 'timeout': 1, - 'max_retries_down': 1, - 'max_retries': 1 - } - expected_hm = { - 'http_method': 'GET', - 'url_path': '/', - 'expected_codes': '200', - 'admin_state_up': True, - 'project_id': self._project_id, - 'provisioning_status': constants.PENDING_CREATE, - 'operating_status': constants.OFFLINE, - 'tags': [] - } + def _get_hm_bodies(self, hm_type=constants.HEALTH_MONITOR_PING, + delay=1): + if hm_type == constants.HEALTH_MONITOR_UDP_CONNECT: + create_hm = { + 'type': constants.HEALTH_MONITOR_UDP_CONNECT, + 'delay': delay, + 'timeout': 1, + 'max_retries_down': 1, + 'max_retries': 1 + } + expected_hm = { + 'admin_state_up': True, + 'project_id': self._project_id, + 'provisioning_status': constants.PENDING_CREATE, + 'operating_status': constants.OFFLINE, + 'tags': [] + } + elif hm_type == constants.HEALTH_MONITOR_HTTP: + create_hm = { + 'type': constants.HEALTH_MONITOR_HTTP, + 'delay': delay, + 'timeout': 1, + 'max_retries_down': 1, + 'max_retries': 1 + } + expected_hm = { + 'http_method': 'GET', + 'url_path': '/', + 'expected_codes': '200', + 'admin_state_up': True, + 'project_id': self._project_id, + 'provisioning_status': constants.PENDING_CREATE, + 'operating_status': constants.OFFLINE, + 'tags': [] + } + else: + create_hm = { + 'type': constants.HEALTH_MONITOR_PING, + 'delay': delay, + 'timeout': 1, + 'max_retries_down': 1, + 'max_retries': 1 + } + expected_hm = { + 'admin_state_up': True, + 'project_id': self._project_id, + 'provisioning_status': constants.PENDING_CREATE, + 'operating_status': constants.OFFLINE, + 'tags': [] + } expected_hm.update(create_hm) return create_hm, expected_hm @@ -2899,6 +2931,47 @@ class TestLoadBalancerGraph(base.BaseAPITest): api_lb = response.json.get(self.root_tag) self._assert_graphs_equal(expected_lb, api_lb) + def test_with_one_listener_one_hm_udp(self): + create_hm, expected_hm = self._get_hm_bodies( + hm_type=constants.HEALTH_MONITOR_UDP_CONNECT, + delay=3) + create_pool, expected_pool = self._get_pool_bodies( + create_hm=create_hm, + expected_hm=expected_hm, + protocol=constants.PROTOCOL_UDP) + create_listener, expected_listener = self._get_listener_bodies( + create_default_pool_name=create_pool['name'], + create_protocol=constants.PROTOCOL_UDP) + create_lb, expected_lb = self._get_lb_bodies( + create_listeners=[create_listener], + expected_listeners=[expected_listener], + create_pools=[create_pool]) + body = self._build_body(create_lb) + response = self.post(self.LBS_PATH, body) + api_lb = response.json.get(self.root_tag) + self._assert_graphs_equal(expected_lb, api_lb) + + def test_with_one_listener_one_hm_udp_validation_failure(self): + create_hm, expected_hm = self._get_hm_bodies( + hm_type=constants.HEALTH_MONITOR_UDP_CONNECT, + delay=1) + create_pool, expected_pool = self._get_pool_bodies( + create_hm=create_hm, + expected_hm=expected_hm, + protocol=constants.PROTOCOL_UDP) + create_listener, expected_listener = self._get_listener_bodies( + create_default_pool_name=create_pool['name'], + create_protocol=constants.PROTOCOL_UDP) + create_lb, expected_lb = self._get_lb_bodies( + create_listeners=[create_listener], + expected_listeners=[expected_listener], + create_pools=[create_pool]) + body = self._build_body(create_lb) + response = self.post(self.LBS_PATH, body, status=400, + expect_errors=True) + error_text = response.json.get('faultstring') + self.assertIn('request delay value 1 should be larger', error_text) + def test_with_one_listener_allowed_cidrs(self): allowed_cidrs = ['10.0.1.0/24', '172.16.0.0/16'] create_listener, expected_listener = self._get_listener_bodies( diff --git a/octavia/tests/unit/common/jinja/haproxy/combined_listeners/test_jinja_cfg.py b/octavia/tests/unit/common/jinja/haproxy/combined_listeners/test_jinja_cfg.py index 418d2e0f5b..6acad643b9 100644 --- a/octavia/tests/unit/common/jinja/haproxy/combined_listeners/test_jinja_cfg.py +++ b/octavia/tests/unit/common/jinja/haproxy/combined_listeners/test_jinja_cfg.py @@ -1086,35 +1086,6 @@ class TestHaproxyCfg(base.TestCase): self.assertEqual(self.jinja_cfg._escape_haproxy_config_string( 'string\\ with\\ all'), 'string\\\\\\ with\\\\\\ all') - def test_expand_expected_codes(self): - exp_codes = '' - self.assertEqual(self.jinja_cfg._expand_expected_codes(exp_codes), - []) - exp_codes = '200' - self.assertEqual( - self.jinja_cfg._expand_expected_codes(exp_codes), ['200']) - exp_codes = '200, 201' - self.assertEqual(self.jinja_cfg._expand_expected_codes(exp_codes), - ['200', '201']) - exp_codes = '200, 201,202' - self.assertEqual(self.jinja_cfg._expand_expected_codes(exp_codes), - ['200', '201', '202']) - exp_codes = '200-202' - self.assertEqual(self.jinja_cfg._expand_expected_codes(exp_codes), - ['200', '201', '202']) - exp_codes = '200-202, 205' - self.assertEqual(self.jinja_cfg._expand_expected_codes(exp_codes), - ['200', '201', '202', '205']) - exp_codes = '200, 201-203' - self.assertEqual(self.jinja_cfg._expand_expected_codes(exp_codes), - ['200', '201', '202', '203']) - exp_codes = '200, 201-203, 205' - self.assertEqual(self.jinja_cfg._expand_expected_codes(exp_codes), - ['200', '201', '202', '203', '205']) - exp_codes = '201-200, 205' - self.assertEqual( - self.jinja_cfg._expand_expected_codes(exp_codes), ['205']) - def test_render_template_no_log(self): j_cfg = jinja_cfg.JinjaTemplater( base_amp_path='/var/lib/octavia', diff --git a/octavia/tests/unit/common/jinja/haproxy/split_listeners/test_jinja_cfg.py b/octavia/tests/unit/common/jinja/haproxy/split_listeners/test_jinja_cfg.py index 8c198d88e9..a306b0e117 100644 --- a/octavia/tests/unit/common/jinja/haproxy/split_listeners/test_jinja_cfg.py +++ b/octavia/tests/unit/common/jinja/haproxy/split_listeners/test_jinja_cfg.py @@ -981,35 +981,6 @@ class TestHaproxyCfg(base.TestCase): self.assertEqual(self.jinja_cfg._escape_haproxy_config_string( 'string\\ with\\ all'), 'string\\\\\\ with\\\\\\ all') - def test_expand_expected_codes(self): - exp_codes = '' - self.assertEqual(self.jinja_cfg._expand_expected_codes(exp_codes), - []) - exp_codes = '200' - self.assertEqual( - self.jinja_cfg._expand_expected_codes(exp_codes), ['200']) - exp_codes = '200, 201' - self.assertEqual(self.jinja_cfg._expand_expected_codes(exp_codes), - ['200', '201']) - exp_codes = '200, 201,202' - self.assertEqual(self.jinja_cfg._expand_expected_codes(exp_codes), - ['200', '201', '202']) - exp_codes = '200-202' - self.assertEqual(self.jinja_cfg._expand_expected_codes(exp_codes), - ['200', '201', '202']) - exp_codes = '200-202, 205' - self.assertEqual(self.jinja_cfg._expand_expected_codes(exp_codes), - ['200', '201', '202', '205']) - exp_codes = '200, 201-203' - self.assertEqual(self.jinja_cfg._expand_expected_codes(exp_codes), - ['200', '201', '202', '203']) - exp_codes = '200, 201-203, 205' - self.assertEqual(self.jinja_cfg._expand_expected_codes(exp_codes), - ['200', '201', '202', '203', '205']) - exp_codes = '201-200, 205' - self.assertEqual( - self.jinja_cfg._expand_expected_codes(exp_codes), ['205']) - def test_render_template_no_log(self): j_cfg = jinja_cfg.JinjaTemplater( base_amp_path='/var/lib/octavia', diff --git a/octavia/tests/unit/common/jinja/lvs/test_lvs_jinja_cfg.py b/octavia/tests/unit/common/jinja/lvs/test_lvs_jinja_cfg.py index 83a0c3bd71..200777ba11 100644 --- a/octavia/tests/unit/common/jinja/lvs/test_lvs_jinja_cfg.py +++ b/octavia/tests/unit/common/jinja/lvs/test_lvs_jinja_cfg.py @@ -297,3 +297,107 @@ class TestLvsCfg(base.TestCase): ret = self.udp_jinja_cfg._transform_listener(in_listener) sample_configs_combined.RET_UDP_LISTENER.pop('connection_limit') self.assertEqual(sample_configs_combined.RET_UDP_LISTENER, ret) + + def test_render_template_udp_listener_with_http_health_monitor(self): + exp = ("# Configuration for Loadbalancer sample_loadbalancer_id_1\n" + "# Configuration for Listener sample_listener_id_1\n\n" + "net_namespace amphora-haproxy\n\n" + "virtual_server 10.0.0.2 80 {\n" + " lb_algo rr\n" + " lb_kind NAT\n" + " protocol UDP\n" + " delay_loop 30\n" + " delay_before_retry 30\n" + " retry 3\n\n\n" + " # Configuration for Pool sample_pool_id_1\n" + " # Configuration for HealthMonitor sample_monitor_id_1\n" + " # Configuration for Member sample_member_id_1\n" + " real_server 10.0.0.99 82 {\n" + " weight 13\n" + " uthreshold 98\n" + " HTTP_GET {\n" + " url {\n" + " path /index.html\n" + " status_code 200\n" + " }\n" + " url {\n" + " path /index.html\n" + " status_code 201\n" + " }\n" + " connect_ip 10.0.0.99\n" + " connect_port 82\n" + " connect_timeout 31\n" + " }\n" + " }\n\n" + " # Configuration for Member sample_member_id_2\n" + " real_server 10.0.0.98 82 {\n" + " weight 13\n" + " uthreshold 98\n" + " HTTP_GET {\n" + " url {\n" + " path /index.html\n" + " status_code 200\n" + " }\n" + " url {\n" + " path /index.html\n" + " status_code 201\n" + " }\n" + " connect_ip 10.0.0.98\n" + " connect_port 82\n" + " connect_timeout 31\n" + " }\n" + " }\n\n" + "}\n\n") + + listener = sample_configs_combined.sample_listener_tuple( + proto=constants.PROTOCOL_UDP, + monitor_proto=constants.HEALTH_MONITOR_HTTP, + connection_limit=98, + persistence=False, + monitor_expected_codes='200-201') + + rendered_obj = self.udp_jinja_cfg.render_loadbalancer_obj(listener) + self.assertEqual(exp, rendered_obj) + + def test_render_template_udp_listener_with_tcp_health_monitor(self): + exp = ("# Configuration for Loadbalancer sample_loadbalancer_id_1\n" + "# Configuration for Listener sample_listener_id_1\n\n" + "net_namespace amphora-haproxy\n\n" + "virtual_server 10.0.0.2 80 {\n" + " lb_algo rr\n" + " lb_kind NAT\n" + " protocol UDP\n" + " delay_loop 30\n" + " delay_before_retry 30\n" + " retry 3\n\n\n" + " # Configuration for Pool sample_pool_id_1\n" + " # Configuration for HealthMonitor sample_monitor_id_1\n" + " # Configuration for Member sample_member_id_1\n" + " real_server 10.0.0.99 82 {\n" + " weight 13\n" + " uthreshold 98\n" + " TCP_CHECK {\n" + " connect_ip 10.0.0.99\n" + " connect_port 82\n" + " connect_timeout 31\n" + " }\n" + " }\n\n" + " # Configuration for Member sample_member_id_2\n" + " real_server 10.0.0.98 82 {\n" + " weight 13\n" + " uthreshold 98\n" + " TCP_CHECK {\n" + " connect_ip 10.0.0.98\n" + " connect_port 82\n" + " connect_timeout 31\n" + " }\n" + " }\n\n" + "}\n\n") + listener = sample_configs_combined.sample_listener_tuple( + proto=constants.PROTOCOL_UDP, + monitor_proto=constants.HEALTH_MONITOR_TCP, + connection_limit=98, + persistence=False) + + rendered_obj = self.udp_jinja_cfg.render_loadbalancer_obj(listener) + self.assertEqual(exp, rendered_obj) diff --git a/octavia/tests/unit/common/sample_configs/sample_configs_combined.py b/octavia/tests/unit/common/sample_configs/sample_configs_combined.py index 8dcf3a9006..c0483894c3 100644 --- a/octavia/tests/unit/common/sample_configs/sample_configs_combined.py +++ b/octavia/tests/unit/common/sample_configs/sample_configs_combined.py @@ -588,8 +588,9 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True, tls=False, sni=False, peer_port=None, topology=None, l7=False, enabled=True, insert_headers=None, be_proto=None, monitor_ip_port=False, - monitor_proto=None, backup_member=False, - disabled_member=False, connection_limit=-1, + monitor_proto=None, monitor_expected_codes=None, + backup_member=False, disabled_member=False, + connection_limit=-1, timeout_client_data=50000, timeout_member_connect=5000, timeout_member_data=50000, @@ -686,6 +687,7 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True, persistence_granularity=persistence_granularity, monitor_ip_port=monitor_ip_port, monitor_proto=monitor_proto, + monitor_expected_codes=monitor_expected_codes, pool_cert=pool_cert, pool_ca_cert=pool_ca_cert, pool_crl=pool_crl, @@ -769,6 +771,7 @@ def sample_pool_tuple(listener_id=None, proto=None, monitor=True, persistence_cookie=None, persistence_timeout=None, persistence_granularity=None, sample_pool=1, monitor_ip_port=False, monitor_proto=None, + monitor_expected_codes=None, backup_member=False, disabled_member=False, has_http_reuse=True, pool_cert=False, pool_ca_cert=False, pool_crl=False, tls_enabled=False, @@ -806,7 +809,9 @@ def sample_pool_tuple(listener_id=None, proto=None, monitor=True, enabled=not disabled_member)] if monitor is True: mon = sample_health_monitor_tuple( - proto=monitor_proto, host_http_check=hm_host_http_check) + proto=monitor_proto, + host_http_check=hm_host_http_check, + expected_codes=monitor_expected_codes) elif sample_pool == 2: id = 'sample_pool_id_2' members = [sample_member_tuple('sample_member_id_3', '10.0.0.97', @@ -814,7 +819,8 @@ def sample_pool_tuple(listener_id=None, proto=None, monitor=True, if monitor is True: mon = sample_health_monitor_tuple( proto=monitor_proto, sample_hm=2, - host_http_check=hm_host_http_check) + host_http_check=hm_host_http_check, + expected_codes=monitor_expected_codes) return in_pool( id=id, protocol=proto, @@ -872,7 +878,7 @@ def sample_session_persistence_tuple(persistence_type=None, def sample_health_monitor_tuple(proto='HTTP', sample_hm=1, - host_http_check=False, + host_http_check=False, expected_codes=None, provisioning_status=constants.ACTIVE): proto = 'HTTP' if proto == 'TERMINATED_HTTPS' else proto monitor = collections.namedtuple( @@ -904,6 +910,8 @@ def sample_health_monitor_tuple(proto='HTTP', sample_hm=1, kwargs.update({'http_version': 1.1, 'domain_name': 'testlab.com'}) else: kwargs.update({'http_version': 1.0, 'domain_name': None}) + if expected_codes: + kwargs.update({'expected_codes': expected_codes}) if proto == constants.HEALTH_MONITOR_UDP_CONNECT: kwargs['check_script_path'] = (CONF.haproxy_amphora.base_path + 'lvs/check/' + 'udp_check.sh') diff --git a/octavia/tests/unit/common/sample_configs/sample_configs_split.py b/octavia/tests/unit/common/sample_configs/sample_configs_split.py index 228bfe45b9..f628bed949 100644 --- a/octavia/tests/unit/common/sample_configs/sample_configs_split.py +++ b/octavia/tests/unit/common/sample_configs/sample_configs_split.py @@ -611,8 +611,9 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True, tls=False, sni=False, peer_port=None, topology=None, l7=False, enabled=True, insert_headers=None, be_proto=None, monitor_ip_port=False, - monitor_proto=None, backup_member=False, - disabled_member=False, connection_limit=-1, + monitor_proto=None, monitor_expected_codes=None, + backup_member=False, disabled_member=False, + connection_limit=-1, timeout_client_data=50000, timeout_member_connect=5000, timeout_member_data=50000, @@ -697,6 +698,7 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True, persistence_granularity=persistence_granularity, monitor_ip_port=monitor_ip_port, monitor_proto=monitor_proto, + monitor_expected_codes=monitor_expected_codes, pool_cert=pool_cert, pool_ca_cert=pool_ca_cert, pool_crl=pool_crl, @@ -778,7 +780,8 @@ def sample_pool_tuple(proto=None, monitor=True, persistence=True, persistence_type=None, persistence_cookie=None, persistence_timeout=None, persistence_granularity=None, sample_pool=1, monitor_ip_port=False, - monitor_proto=None, backup_member=False, + monitor_proto=None, monitor_expected_codes=None, + backup_member=False, disabled_member=False, has_http_reuse=True, pool_cert=False, pool_ca_cert=False, pool_crl=False, tls_enabled=False, hm_host_http_check=False, @@ -811,7 +814,9 @@ def sample_pool_tuple(proto=None, monitor=True, persistence=True, enabled=not disabled_member)] if monitor is True: mon = sample_health_monitor_tuple( - proto=monitor_proto, host_http_check=hm_host_http_check) + proto=monitor_proto, + host_http_check=hm_host_http_check, + expected_codes=monitor_expected_codes) elif sample_pool == 2: id = 'sample_pool_id_2' members = [sample_member_tuple('sample_member_id_3', '10.0.0.97', @@ -819,7 +824,8 @@ def sample_pool_tuple(proto=None, monitor=True, persistence=True, if monitor is True: mon = sample_health_monitor_tuple( proto=monitor_proto, sample_hm=2, - host_http_check=hm_host_http_check) + host_http_check=hm_host_http_check, + expected_codes=monitor_expected_codes) return in_pool( id=id, @@ -877,7 +883,7 @@ def sample_session_persistence_tuple(persistence_type=None, def sample_health_monitor_tuple(proto='HTTP', sample_hm=1, - host_http_check=False, + host_http_check=False, expected_codes=None, provisioning_status=constants.ACTIVE): proto = 'HTTP' if proto == 'TERMINATED_HTTPS' else proto monitor = collections.namedtuple( @@ -909,6 +915,8 @@ def sample_health_monitor_tuple(proto='HTTP', sample_hm=1, kwargs.update({'http_version': 1.1, 'domain_name': 'testlab.com'}) else: kwargs.update({'http_version': 1.0, 'domain_name': None}) + if expected_codes: + kwargs.update({'expected_codes': expected_codes}) if proto == constants.HEALTH_MONITOR_UDP_CONNECT: kwargs['check_script_path'] = (CONF.haproxy_amphora.base_path + 'lvs/check/' + 'udp_check.sh') diff --git a/octavia/tests/unit/common/test_utils.py b/octavia/tests/unit/common/test_utils.py index 832f6ee6f8..512548314e 100644 --- a/octavia/tests/unit/common/test_utils.py +++ b/octavia/tests/unit/common/test_utils.py @@ -60,3 +60,32 @@ class TestConfig(base.TestCase): utils.ip_netmask_to_cidr('10.0.0.1', '255.255.240.0')) self.assertEqual('10.0.0.0/30', utils.ip_netmask_to_cidr( '10.0.0.1', '255.255.255.252')) + + def test_expand_expected_codes(self): + exp_codes = '' + self.assertEqual(utils.expand_expected_codes(exp_codes), + set()) + exp_codes = '200' + self.assertEqual(utils.expand_expected_codes(exp_codes), + {'200'}) + exp_codes = '200, 201' + self.assertEqual(utils.expand_expected_codes(exp_codes), + {'200', '201'}) + exp_codes = '200, 201,202' + self.assertEqual(utils.expand_expected_codes(exp_codes), + {'200', '201', '202'}) + exp_codes = '200-202' + self.assertEqual(utils.expand_expected_codes(exp_codes), + {'200', '201', '202'}) + exp_codes = '200-202, 205' + self.assertEqual(utils.expand_expected_codes(exp_codes), + {'200', '201', '202', '205'}) + exp_codes = '200, 201-203' + self.assertEqual(utils.expand_expected_codes(exp_codes), + {'200', '201', '202', '203'}) + exp_codes = '200, 201-203, 205' + self.assertEqual(utils.expand_expected_codes(exp_codes), + {'200', '201', '202', '203', '205'}) + exp_codes = '201-200, 205' + self.assertEqual(utils.expand_expected_codes(exp_codes), + {'205'}) diff --git a/releasenotes/notes/additional-udp-healthcheck-types-2414a5edee9f5110.yaml b/releasenotes/notes/additional-udp-healthcheck-types-2414a5edee9f5110.yaml new file mode 100644 index 0000000000..2f3a4bfa89 --- /dev/null +++ b/releasenotes/notes/additional-udp-healthcheck-types-2414a5edee9f5110.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Two new types of healthmonitoring are now valid for UDP listeners. Both + ``HTTP`` and ``TCP`` check types can now be used.