Browse Source

Merge "Support HTTP and TCP checks in UDP healthmonitor"

changes/18/721318/1
Zuul 2 years ago committed by Gerrit Code Review
parent
commit
1b52ccd20f
  1. 49
      api-ref/source/v2/general.inc
  2. 2
      octavia/amphorae/backends/utils/keepalivedlvs_query.py
  3. 6
      octavia/api/root_controller.py
  4. 42
      octavia/api/v2/controllers/health_monitor.py
  5. 15
      octavia/api/v2/controllers/pool.py
  6. 24
      octavia/common/jinja/haproxy/combined_listeners/jinja_cfg.py
  7. 24
      octavia/common/jinja/haproxy/split_listeners/jinja_cfg.py
  8. 16
      octavia/common/jinja/lvs/jinja_cfg.py
  9. 36
      octavia/common/jinja/lvs/templates/macros.j2
  10. 21
      octavia/common/utils.py
  11. 3
      octavia/tests/functional/api/test_root_controller.py
  12. 83
      octavia/tests/functional/api/v2/test_health_monitor.py
  13. 109
      octavia/tests/functional/api/v2/test_load_balancer.py
  14. 29
      octavia/tests/unit/common/jinja/haproxy/combined_listeners/test_jinja_cfg.py
  15. 29
      octavia/tests/unit/common/jinja/haproxy/split_listeners/test_jinja_cfg.py
  16. 104
      octavia/tests/unit/common/jinja/lvs/test_lvs_jinja_cfg.py
  17. 18
      octavia/tests/unit/common/sample_configs/sample_configs_combined.py
  18. 20
      octavia/tests/unit/common/sample_configs/sample_configs_split.py
  19. 29
      octavia/tests/unit/common/test_utils.py
  20. 5
      releasenotes/notes/additional-udp-healthcheck-types-2414a5edee9f5110.yaml

49
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.

2
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):

6
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}

42
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)

15
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

24
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)

24
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)

16
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

36
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 %}

21
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

3
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'}]

83
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))

109
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(

29
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',

29
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',

104
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)

18
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')

20
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')

29
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'})

5
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.
Loading…
Cancel
Save