From 639c11751e07eed48d79e7b81f12bac697eaea73 Mon Sep 17 00:00:00 2001 From: Gregory Thiemonge Date: Tue, 8 Sep 2020 19:08:34 +0200 Subject: [PATCH] Add SCTP support in API Add SCTP support in the API for listeners, pools, health-monitors resources. Story: 2007884 Task: 40255 Change-Id: I57a3c528a20943724bdcd36422c689f496068330 --- api-ref/source/parameters.yaml | 22 +-- api-ref/source/v2/general.inc | 71 ++++---- api-ref/source/v2/healthmonitor.inc | 2 +- api-ref/source/v2/l7policy.inc | 4 +- api-ref/source/v2/pool.inc | 2 +- doc/source/contributor/guides/providers.rst | 2 +- .../feature-matrix-healthmonitor.ini | 8 + .../feature-matrix-listener.ini | 8 + .../feature-matrix-pool.ini | 12 +- doc/source/user/guides/basic-cookbook.rst | 11 +- octavia/api/root_controller.py | 5 +- octavia/api/v2/controllers/health_monitor.py | 32 ++-- octavia/api/v2/controllers/listener.py | 19 ++- octavia/api/v2/controllers/pool.py | 35 ++-- octavia/common/constants.py | 4 +- .../versions/8b47b2546312_sctp_support.py | 46 ++++++ .../drivers/neutron/allowed_address_pairs.py | 6 +- .../functional/api/test_root_controller.py | 3 +- .../functional/api/v2/test_health_monitor.py | 153 +++++++++++++++++- octavia/tests/functional/api/v2/test_pool.py | 33 ++-- ...rt-for-sctp-protocol-152444b211ab2188.yaml | 5 + 21 files changed, 385 insertions(+), 98 deletions(-) create mode 100644 octavia/db/migration/alembic_migrations/versions/8b47b2546312_sctp_support.py create mode 100644 releasenotes/notes/add-support-for-sctp-protocol-152444b211ab2188.yaml diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 7aea6d0aff..705ee34b56 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -760,8 +760,8 @@ healthmonitor-timeout-optional: type: integer healthmonitor-type: description: | - The type of health monitor. One of ``HTTP``, ``HTTPS``, ``PING``, ``TCP``, - ``TLS-HELLO``, or ``UDP-CONNECT``. + The type of health monitor. One of ``HTTP``, ``HTTPS``, ``PING``, + ``SCTP``, ``TCP``, ``TLS-HELLO``, or ``UDP-CONNECT``. in: body required: true type: string @@ -1228,15 +1228,15 @@ project_id-optional-deprecated: type: string protocol: description: | - The protocol for the resource. One of ``HTTP``, ``HTTPS``, ``TCP``, - ``TERMINATED_HTTPS``, or ``UDP``. + The protocol for the resource. One of ``HTTP``, ``HTTPS``, ``SCTP``, + ``TCP``, ``TERMINATED_HTTPS``, or ``UDP``. in: body required: true type: string protocol-pools: description: | The protocol for the resource. One of ``HTTP``, ``HTTPS``, ``PROXY``, - ``PROXYV2``, ``TCP``, or ``UDP``. + ``PROXYV2``, ``SCTP``, ``TCP``, or ``UDP``. in: body required: true type: string @@ -1421,18 +1421,18 @@ session_persistence_cookie: type: string session_persistence_granularity: description: | - The netmask used to determine UDP session persistence. Currently only - valid for UDP pools with session persistence of SOURCE_IP. Default netmask - is 255.255.255.255, meaning per client full IP. + The netmask used to determine SCTP or UDP session persistence. Currently + only valid for SCTP or UDP pools with session persistence of SOURCE_IP. + Default netmask is 255.255.255.255, meaning per client full IP. in: body min_version: 2.2 required: false type: string session_persistence_timeout: description: | - The timeout, in seconds, after which a UDP flow may be rescheduled to a - different member. Currently only applies to UDP pools with session - persistence of SOURCE_IP. Default is 360. + The timeout, in seconds, after which a SCTP or UDP flow may be rescheduled + to a different member. Currently only applies to SCTP or UDP pools with + session persistence of SOURCE_IP. Default is 360. in: body min_version: 2.2 required: false diff --git a/api-ref/source/v2/general.inc b/api-ref/source/v2/general.inc index 5daeab4510..7781eb07e2 100644 --- a/api-ref/source/v2/general.inc +++ b/api-ref/source/v2/general.inc @@ -594,20 +594,22 @@ Valid protocol combinations .. |8Y| replace:: |2| |2| |2| |2| Y .. |8N| replace:: |2| |2| |2| |2| N -+-------------+-------+--------+------+-------------------+------+ -|| |listener| || HTTP || HTTPS || TCP || TERMINATED_HTTPS || UDP | -|| Pool || || || || || | -+=============+=======+========+======+===================+======+ -| HTTP | |2Y| | |2N| | |1Y| | |8Y| | |1N| | -+-------------+-------+--------+------+-------------------+------+ -| HTTPS | |2N| | |2Y| | |1Y| | |8N| | |1N| | -+-------------+-------+--------+------+-------------------+------+ -| PROXY | |2Y| | |2Y| | |1Y| | |8Y| | |1N| | -+-------------+-------+--------+------+-------------------+------+ -| TCP | |2N| | |2Y| | |1Y| | |8N| | |1N| | -+-------------+-------+--------+------+-------------------+------+ -| UDP | |2N| | |2N| | |1N| | |8N| | |1Y| | -+-------------+-------+--------+------+-------------------+------+ ++-------------+-------+--------+-------+------+-------------------+------+ +|| |listener| || HTTP || HTTPS || SCTP || TCP || TERMINATED_HTTPS || UDP | +|| Pool || || || || || || | ++=============+=======+========+=======+======+===================+======+ +| HTTP | |2Y| | |2N| | |2N| | |1Y| | |8Y| | |1N| | ++-------------+-------+--------+-------+------+-------------------+------+ +| HTTPS | |2N| | |2Y| | |2N| | |1Y| | |8N| | |1N| | ++-------------+-------+--------+-------+------+-------------------+------+ +| PROXY | |2Y| | |2Y| | |2N| | |1Y| | |8Y| | |1N| | ++-------------+-------+--------+-------+------+-------------------+------+ +| SCTP | |2N| | |2N| | |2Y| | |1N| | |8N| | |1N| | ++-------------+-------+--------+-------+------+-------------------+------+ +| TCP | |2N| | |2Y| | |2N| | |1Y| | |8N| | |1N| | ++-------------+-------+--------+-------+------+-------------------+------+ +| UDP | |2N| | |2N| | |2N| | |1N| | |8N| | |1Y| | ++-------------+-------+--------+-------+------+-------------------+------+ "Y" means the combination is valid and "N" means invalid. @@ -640,25 +642,28 @@ Valid protocol combinations .. |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| | -+-------------------+-------+--------+-------+------+------------+---------------+ ++-------------------+-------+--------+-------+-------+------+------------+---------------+ +|| |Health Monitor| || HTTP || HTTPS || PING || SCTP || TCP || TLS-HELLO || |UDPCONNECT| | +|| Pool || || || || || || || | ++===================+=======+========+=======+=======+======+============+===============+ +| HTTP | |2Y| | |2Y| | |1Y| | |1N| | |1Y| | |4Y| | |5N| | ++-------------------+-------+--------+-------+-------+------+------------+---------------+ +| HTTPS | |2Y| | |2Y| | |1Y| | |1N| | |1Y| | |4Y| | |5N| | ++-------------------+-------+--------+-------+-------+------+------------+---------------+ +| PROXY | |2Y| | |2Y| | |1Y| | |1N| | |1Y| | |4Y| | |5N| | ++-------------------+-------+--------+-------+-------+------+------------+---------------+ +| SCTP | |2Y| | |2N| | |1N| | |1Y| | |1Y| | |4N| | |5Y| | ++-------------------+-------+--------+-------+-------+------+------------+---------------+ +| TCP | |2Y| | |2Y| | |1Y| | |1N| | |1Y| | |4Y| | |5N| | ++-------------------+-------+--------+-------+-------+------+------------+---------------+ +| UDP | |2Y| | |2N| | |1N| | |1Y| | |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. +These combinations are mostly as you'd expect for all non-UDP/SCTP pool +protocols: non-UDP/SCTP pools can have health monitors with any check type +besides UDP-CONNECT and SCTP. +For UDP or SCTP pools however, things are a little more complicated. UDP and +SCTP Pools support UDP-CONNECT and SCTP but also HTTP and TCP checks. HTTPS +checks are technically feasible but have not yet been implemented. diff --git a/api-ref/source/v2/healthmonitor.inc b/api-ref/source/v2/healthmonitor.inc index c4941d0810..6d7090b66e 100644 --- a/api-ref/source/v2/healthmonitor.inc +++ b/api-ref/source/v2/healthmonitor.inc @@ -118,7 +118,7 @@ At a minimum, you must specify these health monitor attributes: times out. - ``type`` The type of health monitor. One of ``HTTP``, ``HTTPS``, ``PING``, - ``TCP``, ``TLS-HELLO``, or ``UDP-CONNECT``. + ``SCTP``, ``TCP``, ``TLS-HELLO``, or ``UDP-CONNECT``. Some attributes receive default values if you omit them from the request: diff --git a/api-ref/source/v2/l7policy.inc b/api-ref/source/v2/l7policy.inc index ec8f024b47..30bbd78251 100644 --- a/api-ref/source/v2/l7policy.inc +++ b/api-ref/source/v2/l7policy.inc @@ -115,7 +115,9 @@ L7 policies with ``action`` of ``REDIRECT_TO_URL`` will return the default HTTP L7 policies with ``action`` of ``REJECT`` will return a ``Forbidden (403)`` response code to the requester. -.. note:: Pools of type ``UDP`` cannot be used in L7 policies at this time. +.. note:: + Pools of type ``SCTP``, ``TCP`` or ``UDP`` cannot be used in L7 + policies at this time. .. rest_status_code:: success ../http-status.yaml diff --git a/api-ref/source/v2/pool.inc b/api-ref/source/v2/pool.inc index 96923ca9c6..3885db0299 100644 --- a/api-ref/source/v2/pool.inc +++ b/api-ref/source/v2/pool.inc @@ -103,7 +103,7 @@ At a minimum, you must specify these pool attributes: - ``protocol`` The protocol for which this pool and its members listen. A valid value is ``HTTP``, ``HTTPS``, ``PROXY``, ``PROXYV2``, - ``TCP``, or ``UDP``. + ``SCTP``, ``TCP``, or ``UDP``. - ``lb_algorithm`` The load-balancer algorithm, such as ``ROUND_ROBIN``, ``LEAST_CONNECTIONS``, ``SOURCE_IP`` and ``SOURCE_IP_PORT``, diff --git a/doc/source/contributor/guides/providers.rst b/doc/source/contributor/guides/providers.rst index b45fe326fa..d379245b17 100644 --- a/doc/source/contributor/guides/providers.rst +++ b/doc/source/contributor/guides/providers.rst @@ -1201,7 +1201,7 @@ and validated with the following exceptions: | | | be less than the delay value. | +-----------------------+--------+------------------------------------------+ | type | string | The type of health monitor. One of HTTP, | -| | | HTTPS, PING, TCP, TLS-HELLO or | +| | | HTTPS, PING, SCTP, TCP, TLS-HELLO or | | | | UDP-CONNECT. | +-----------------------+--------+------------------------------------------+ | url_path | string | The HTTP URL path of the request sent by | diff --git a/doc/source/user/feature-classification/feature-matrix-healthmonitor.ini b/doc/source/user/feature-classification/feature-matrix-healthmonitor.ini index 9936ad03c5..2c07660e5f 100644 --- a/doc/source/user/feature-classification/feature-matrix-healthmonitor.ini +++ b/doc/source/user/feature-classification/feature-matrix-healthmonitor.ini @@ -159,6 +159,14 @@ cli=openstack loadbalancer healthmonitor create --type UDP-CONNECT driver.amphora=complete driver.ovn=missing +[operation.type.SCTP] +title=type - SCTP +status=optional +notes=Use SCTP for the health monitor. +cli=openstack loadbalancer healthmonitor create --type SCTP +driver.amphora=missing +driver.ovn=missing + [operation.url_path] title=url_path status=optional diff --git a/doc/source/user/feature-classification/feature-matrix-listener.ini b/doc/source/user/feature-classification/feature-matrix-listener.ini index e6eecb2056..6ab960c628 100644 --- a/doc/source/user/feature-classification/feature-matrix-listener.ini +++ b/doc/source/user/feature-classification/feature-matrix-listener.ini @@ -230,6 +230,14 @@ cli=openstack loadbalancer listener create --protocol UDP driver.amphora=complete driver.ovn=complete +[operation.protocol.SCTP] +title=protocol - SCTP +status=optional +notes=SCTP protocol support for the listener. +cli=openstack loadbalancer listener create --protocol SCTP +driver.amphora=missing +driver.ovn=missing + [operation.protocol_port] title=protocol_port status=mandatory diff --git a/doc/source/user/feature-classification/feature-matrix-pool.ini b/doc/source/user/feature-classification/feature-matrix-pool.ini index 69e19c6646..057835e53e 100644 --- a/doc/source/user/feature-classification/feature-matrix-pool.ini +++ b/doc/source/user/feature-classification/feature-matrix-pool.ini @@ -138,6 +138,14 @@ cli=openstack loadbalancer pool create --protocol UDP --listener driver.amphora=complete driver.ovn=complete +[operation.protocol.SCTP] +title=protocol - SCTP +status=optional +notes=SCTP protocol support for the pool. +cli=openstack loadbalancer pool create --protocol SCTP --listener +driver.amphora=missing +driver.ovn=missing + [operation.session_persistence.APP_COOKIE] title=session_persistence - APP_COOKIE status=optional @@ -165,7 +173,7 @@ driver.ovn=missing [operation.session_persistence.persistence_timeout] title=session_persistence - persistence_timeout status=optional -notes=The timeout, in seconds, after which a UDP flow may be rescheduled to a different member. +notes=The timeout, in seconds, after which a SCTP or UDP flow may be rescheduled to a different member. cli=openstack loadbalancer pool create --session-persistence persistence_timeout=360 --listener driver.amphora=complete driver.ovn=missing @@ -173,7 +181,7 @@ driver.ovn=missing [operation.session_persistence.persistence_granularity] title=session_persistence - persistence_granularity status=optional -notes=The netmask used to determine UDP SOURCE_IP session persistence. +notes=The netmask used to determine SCTP or UDP SOURCE_IP session persistence. cli=openstack loadbalancer pool create --session-persistence persistence_granularity=255.255.255.255 --listener driver.amphora=complete driver.ovn=missing diff --git a/doc/source/user/guides/basic-cookbook.rst b/doc/source/user/guides/basic-cookbook.rst index 85220805ff..b0e8fae83d 100644 --- a/doc/source/user/guides/basic-cookbook.rst +++ b/doc/source/user/guides/basic-cookbook.rst @@ -856,8 +856,8 @@ generates the health check in your web application: Other health monitors --------------------- -Other health monitor types include ``PING``, ``TCP``, ``HTTPS``, ``TLS-HELLO``, -and ``UDP-CONNECT``. +Other health monitor types include ``PING``, ``TCP``, ``HTTPS``, ``SCTP``, +``TLS-HELLO``, and ``UDP-CONNECT``. ``PING`` health monitors send periodic ICMP PING requests to the back-end servers. Obviously, your back-end servers must be configured to allow PINGs in @@ -881,6 +881,13 @@ ssl back-end servers. Unfortunately, this causes problems if the servers are performing client certificate validation, as HAProxy won't have a valid cert. In this case, using ``TLS-HELLO`` type monitoring is an alternative. +``SCTP`` health monitors send an INIT packet to the back-end server's port. +If an application is listening on this port, the Operating System should reply +with an INIT ACK packet, but if the port is closed, it replies with an ABORT +packet. +If the health monitor receives an INIT ACK packet, it immediatly closes the +connection with an ABORT packet, and considers that the server is ONLINE. + ``TLS-HELLO`` health monitors simply ensure the back-end server responds to SSLv3 client hello messages. It will not check any other health metrics, like status code or body contents. diff --git a/octavia/api/root_controller.py b/octavia/api/root_controller.py index 82524589a4..5816fe8794 100644 --- a/octavia/api/root_controller.py +++ b/octavia/api/root_controller.py @@ -131,6 +131,9 @@ class RootController(object): self._add_a_version(versions, 'v2.21', 'v2', 'SUPPORTED', '2020-09-03T00:00:00Z', host_url) # Add PROXYV2 pool protocol - self._add_a_version(versions, 'v2.22', 'v2', 'CURRENT', + self._add_a_version(versions, 'v2.22', 'v2', 'SUPPORTED', '2020-09-04T00:00:00Z', host_url) + # SCTP protocol + self._add_a_version(versions, 'v2.23', 'v2', 'CURRENT', + '2020-09-07T00: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 29c0778707..4c8d275307 100644 --- a/octavia/api/v2/controllers/health_monitor.py +++ b/octavia/api/v2/controllers/health_monitor.py @@ -14,6 +14,7 @@ # under the License. from octavia_lib.api.drivers import data_models as driver_dm +from octavia_lib.common import constants as lib_consts from oslo_config import cfg from oslo_db import exception as odb_exceptions from oslo_log import log as logging @@ -164,16 +165,19 @@ class HealthMonitorController(base.BaseController): # do not give any information as to what constraint failed raise exceptions.InvalidOption(value='', option='') from e - def _validate_healthmonitor_request_for_udp(self, request): + def _validate_healthmonitor_request_for_udp_sctp(self, request, + pool_protocol): if request.type not in ( consts.HEALTH_MONITOR_UDP_CONNECT, + lib_consts.HEALTH_MONITOR_SCTP, consts.HEALTH_MONITOR_TCP, consts.HEALTH_MONITOR_HTTP): raise exceptions.ValidationException(detail=_( "The associated pool protocol is %(pool_protocol)s, so only " "a %(types)s health monitor is supported.") % { - 'pool_protocol': consts.PROTOCOL_UDP, + 'pool_protocol': pool_protocol, 'types': '/'.join((consts.HEALTH_MONITOR_UDP_CONNECT, + lib_consts.HEALTH_MONITOR_SCTP, consts.HEALTH_MONITOR_TCP, consts.HEALTH_MONITOR_HTTP))}) # check the delay value if the HM type is UDP-CONNECT @@ -209,14 +213,19 @@ class HealthMonitorController(base.BaseController): raise exceptions.DisabledOption( option='type', value=consts.HEALTH_MONITOR_PING) - if pool.protocol == consts.PROTOCOL_UDP: - self._validate_healthmonitor_request_for_udp(health_monitor) + if pool.protocol in (lib_consts.PROTOCOL_UDP, + lib_consts.PROTOCOL_SCTP): + self._validate_healthmonitor_request_for_udp_sctp(health_monitor, + pool.protocol) else: - if health_monitor.type == consts.HEALTH_MONITOR_UDP_CONNECT: + if health_monitor.type in (consts.HEALTH_MONITOR_UDP_CONNECT, + lib_consts.HEALTH_MONITOR_SCTP): raise exceptions.ValidationException(detail=_( "The %(type)s type is only supported for pools of type " - "%(protocol)s.") % {'type': health_monitor.type, - 'protocol': consts.PROTOCOL_UDP}) + "%(protocols)s.") % { + 'type': health_monitor.type, + 'protocols': '/'.join((consts.PROTOCOL_UDP, + lib_consts.PROTOCOL_SCTP))}) # Load the driver early as it also provides validation driver = driver_factory.get_driver(provider) @@ -342,11 +351,12 @@ class HealthMonitorController(base.BaseController): self._auth_validate_action(context, project_id, consts.RBAC_PUT) self._validate_update_hm(db_hm, health_monitor) - # Validate health monitor update options for UDP-CONNECT type. - if (pool.protocol == consts.PROTOCOL_UDP and - db_hm.type == consts.HEALTH_MONITOR_UDP_CONNECT): + # Validate health monitor update options for UDP/SCTP + if pool.protocol in (lib_consts.PROTOCOL_UDP, + lib_consts.PROTOCOL_SCTP): health_monitor.type = db_hm.type - self._validate_healthmonitor_request_for_udp(health_monitor) + self._validate_healthmonitor_request_for_udp_sctp(health_monitor, + pool.protocol) self._set_default_on_none(health_monitor) diff --git a/octavia/api/v2/controllers/listener.py b/octavia/api/v2/controllers/listener.py index 0d332b58c3..fe26e3fa35 100644 --- a/octavia/api/v2/controllers/listener.py +++ b/octavia/api/v2/controllers/listener.py @@ -14,6 +14,7 @@ # under the License. from octavia_lib.api.drivers import data_models as driver_dm +from octavia_lib.common import constants as lib_consts from oslo_config import cfg from oslo_db import exception as odb_exceptions from oslo_log import log as logging @@ -175,12 +176,13 @@ class ListenersController(base.BaseController): self._validate_insert_headers( listener_dict['insert_headers'].keys(), listener_protocol) - # Check for UDP compatibility - if (listener_protocol == constants.PROTOCOL_UDP and + # Check for UDP/SCTP compatibility + if (listener_protocol in (constants.PROTOCOL_UDP, + lib_consts.PROTOCOL_SCTP) and self._is_tls_or_insert_header(listener_dict)): raise exceptions.ValidationException( detail=_("%s protocol listener does not " - "support TLS.") % constants.PROTOCOL_UDP) + "support TLS.") % listener_protocol) # Check for TLS disabled if (not CONF.api_settings.allow_tls_terminated_listeners and @@ -251,8 +253,8 @@ class ListenersController(base.BaseController): listener_dict.get('client_ca_tls_certificate_id'), listener_dict.get('client_crl_container_id', None)) - # Validate that the L4 protocol (UDP or TCP) is not already used for - # the specified protocol_port in this load balancer + # Validate that the L4 protocol (UDP, TCP or SCTP) is not already used + # for the specified protocol_port in this load balancer pcontext = pecan_request.context query_filter = { 'project_id': listener_dict['project_id'], @@ -435,12 +437,13 @@ class ListenersController(base.BaseController): raise exceptions.ValidationException( detail='No listener object supplied.') - # Check for UDP compatibility - if (db_listener.protocol == constants.PROTOCOL_UDP and + # Check for UDP/SCTP compatibility + if (db_listener.protocol in (constants.PROTOCOL_UDP, + lib_consts.PROTOCOL_SCTP) and self._is_tls_or_insert_header(listener.to_dict())): raise exceptions.ValidationException(detail=_( "%s protocol listener does not support TLS or header " - "insertion.") % constants.PROTOCOL_UDP) + "insertion.") % db_listener.protocol) # Check for certs when not TERMINATED_HTTPS if (db_listener.protocol != constants.PROTOCOL_TERMINATED_HTTPS and diff --git a/octavia/api/v2/controllers/pool.py b/octavia/api/v2/controllers/pool.py index 73cc9062fb..dde59e2e9f 100644 --- a/octavia/api/v2/controllers/pool.py +++ b/octavia/api/v2/controllers/pool.py @@ -14,6 +14,7 @@ # under the License. from octavia_lib.api.drivers import data_models as driver_dm +from octavia_lib.common import constants as lib_consts from oslo_config import cfg from oslo_db import exception as odb_exceptions from oslo_log import log as logging @@ -165,7 +166,7 @@ class PoolsController(base.BaseController): return False return True - def _validate_pool_request_for_udp(self, request): + def _validate_pool_request_for_udp_sctp(self, request): if request.session_persistence: if (request.session_persistence.type == constants.SESSION_PERSISTENCE_SOURCE_IP and @@ -174,14 +175,15 @@ class PoolsController(base.BaseController): check_exist_attrs=['type', 'persistence_timeout', 'persistence_granularity'])): raise exceptions.ValidationException(detail=_( - "session_persistence %s type for UDP protocol " + "session_persistence %s type for UDP and SCTP protocols " "only accepts: type, persistence_timeout, " "persistence_granularity.") % ( constants.SESSION_PERSISTENCE_SOURCE_IP)) if request.session_persistence.cookie_name: raise exceptions.ValidationException(detail=_( "Cookie names are not supported for %s pools.") % - constants.PROTOCOL_UDP) + "/".join((constants.PROTOCOL_UDP, + lib_consts.PROTOCOL_SCTP))) if request.session_persistence.type in [ constants.SESSION_PERSISTENCE_HTTP_COOKIE, constants.SESSION_PERSISTENCE_APP_COOKIE]: @@ -189,7 +191,8 @@ class PoolsController(base.BaseController): "Session persistence of type %(type)s is not supported " "for %(protocol)s protocol pools.") % { 'type': request.session_persistence.type, - 'protocol': constants.PROTOCOL_UDP}) + 'protocol': "/".join((constants.PROTOCOL_UDP, + lib_consts.PROTOCOL_SCTP))}) @wsme_pecan.wsexpose(pool_types.PoolRootResponse, body=pool_types.PoolRootPOST, status_code=201) @@ -226,15 +229,16 @@ class PoolsController(base.BaseController): if pool.listener_id and listener: self._validate_protocol(listener.protocol, pool.protocol) - if pool.protocol == constants.PROTOCOL_UDP: - self._validate_pool_request_for_udp(pool) + if pool.protocol in (constants.PROTOCOL_UDP, + lib_consts.PROTOCOL_SCTP): + self._validate_pool_request_for_udp_sctp(pool) else: if (pool.session_persistence and ( pool.session_persistence.persistence_timeout or pool.session_persistence.persistence_granularity)): raise exceptions.ValidationException(detail=_( "persistence_timeout and persistence_granularity " - "is only for UDP protocol pools.")) + "is only for UDP and SCTP protocol pools.")) if pool.session_persistence: sp_dict = pool.session_persistence.to_dict(render_unsets=False) @@ -311,16 +315,20 @@ class PoolsController(base.BaseController): 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: + if db_pool.protocol in (constants.PROTOCOL_UDP, + lib_consts.PROTOCOL_SCTP): health_monitor.HealthMonitorController( - )._validate_healthmonitor_request_for_udp(new_hm) + )._validate_healthmonitor_request_for_udp_sctp(new_hm, + db_pool) else: - if new_hm.type == constants.HEALTH_MONITOR_UDP_CONNECT: + if new_hm.type in (constants.HEALTH_MONITOR_UDP_CONNECT, + lib_consts.HEALTH_MONITOR_SCTP): 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}) + 'protocol': '/'.join((constants.PROTOCOL_UDP, + lib_consts.PROTOCOL_SCTP))}) db_pool.health_monitor = new_hm # Now check quotas for members @@ -344,8 +352,9 @@ class PoolsController(base.BaseController): def _validate_pool_PUT(self, pool, db_pool): - if db_pool.protocol == constants.PROTOCOL_UDP: - self._validate_pool_request_for_udp(pool) + if db_pool.protocol in (constants.PROTOCOL_UDP, + lib_consts.PROTOCOL_SCTP): + self._validate_pool_request_for_udp_sctp(pool) else: if (pool.session_persistence and ( pool.session_persistence.persistence_timeout or diff --git a/octavia/common/constants.py b/octavia/common/constants.py index 0e38d3c088..c329723dfa 100644 --- a/octavia/common/constants.py +++ b/octavia/common/constants.py @@ -214,7 +214,8 @@ VALID_LISTENER_POOL_PROTOCOL_MAP = { lib_consts.PROTOCOL_PROXYV2, PROTOCOL_TCP], PROTOCOL_TERMINATED_HTTPS: [PROTOCOL_HTTP, PROTOCOL_PROXY, lib_consts.PROTOCOL_PROXYV2], - PROTOCOL_UDP: [PROTOCOL_UDP]} + PROTOCOL_UDP: [PROTOCOL_UDP], + lib_consts.PROTOCOL_SCTP: [lib_consts.PROTOCOL_SCTP]} # API Integer Ranges MIN_PORT_NUMBER = 1 @@ -815,6 +816,7 @@ L4_PROTOCOL_MAP = { PROTOCOL_PROXY: PROTOCOL_TCP, lib_consts.PROTOCOL_PROXYV2: PROTOCOL_TCP, PROTOCOL_UDP: PROTOCOL_UDP, + lib_consts.PROTOCOL_SCTP: lib_consts.PROTOCOL_SCTP, } # Image drivers diff --git a/octavia/db/migration/alembic_migrations/versions/8b47b2546312_sctp_support.py b/octavia/db/migration/alembic_migrations/versions/8b47b2546312_sctp_support.py new file mode 100644 index 0000000000..d6165050e8 --- /dev/null +++ b/octavia/db/migration/alembic_migrations/versions/8b47b2546312_sctp_support.py @@ -0,0 +1,46 @@ +# Copyright 2020 Red Hat, Inc. +# +# 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. + +"""sctp support + +Revision ID: 8b47b2546312 +Revises: e6ee84f0abf3 +Create Date: 2020-06-26 09:26:45.397873 + +""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy import sql + + +# revision identifiers, used by Alembic. +revision = '8b47b2546312' +down_revision = 'e6ee84f0abf3' + + +def upgrade(): + for table in ['protocol', 'health_monitor_type']: + insert_table = sql.table( + table, + sql.column(u'name', sa.String), + sql.column(u'description', sa.String) + ) + + op.bulk_insert( + insert_table, + [ + {'name': 'SCTP'} + ] + ) diff --git a/octavia/network/drivers/neutron/allowed_address_pairs.py b/octavia/network/drivers/neutron/allowed_address_pairs.py index 0199816348..d9409b70ef 100644 --- a/octavia/network/drivers/neutron/allowed_address_pairs.py +++ b/octavia/network/drivers/neutron/allowed_address_pairs.py @@ -16,6 +16,7 @@ import time from neutronclient.common import exceptions as neutron_client_exceptions from novaclient import exceptions as nova_client_exceptions +from octavia_lib.common import constants as lib_consts from oslo_config import cfg from oslo_log import log as logging from stevedore import driver as stevedore_driver @@ -159,6 +160,8 @@ class AllowedAddressPairsDriver(neutron_base.BaseNeutronDriver): protocol = constants.PROTOCOL_TCP.lower() if listener.protocol == constants.PROTOCOL_UDP: protocol = constants.PROTOCOL_UDP.lower() + elif listener.protocol == lib_consts.PROTOCOL_SCTP: + protocol = lib_consts.PROTOCOL_SCTP.lower() if listener.allowed_cidrs: for ac in listener.allowed_cidrs: @@ -183,7 +186,8 @@ class AllowedAddressPairsDriver(neutron_base.BaseNeutronDriver): # None ports with the egress rules. VRRP uses protocol 51 and 112 if (rule.get('direction') == 'egress' or rule.get('protocol').upper() not in - [constants.PROTOCOL_TCP, constants.PROTOCOL_UDP]): + [constants.PROTOCOL_TCP, constants.PROTOCOL_UDP, + lib_consts.PROTOCOL_SCTP]): continue old_ports.append((rule.get('port_range_max'), rule.get('protocol').lower(), diff --git a/octavia/tests/functional/api/test_root_controller.py b/octavia/tests/functional/api/test_root_controller.py index fe70b8e759..c120ce28c7 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(23, len(version_ids)) + self.assertEqual(24, len(version_ids)) self.assertIn('v2.0', version_ids) self.assertIn('v2.1', version_ids) self.assertIn('v2.2', version_ids) @@ -69,6 +69,7 @@ class TestRootController(base_db_test.OctaviaDBTestBase): self.assertIn('v2.20', version_ids) self.assertIn('v2.21', version_ids) self.assertIn('v2.22', version_ids) + self.assertIn('v2.23', 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 e90f8665c7..58c896ab59 100644 --- a/octavia/tests/functional/api/v2/test_health_monitor.py +++ b/octavia/tests/functional/api/v2/test_health_monitor.py @@ -24,6 +24,7 @@ from octavia.common import data_models from octavia.common import exceptions from octavia.db import repositories from octavia.tests.functional.api.v2 import base +from octavia_lib.common import constants as lib_consts class TestHealthMonitor(base.BaseAPITest): @@ -56,6 +57,7 @@ class TestHealthMonitor(base.BaseAPITest): self.set_lb_status(self.lb_id) self.pool_repo = repositories.PoolRepository() self._setup_udp_lb_resources() + self._setup_sctp_lb_resources() def _setup_udp_lb_resources(self): self.udp_lb = self.create_load_balancer(uuidutils.generate_uuid()).get( @@ -80,6 +82,25 @@ class TestHealthMonitor(base.BaseAPITest): group='api_settings', udp_connect_min_interval_health_monitor='3') + def _setup_sctp_lb_resources(self): + self.sctp_lb = self.create_load_balancer( + uuidutils.generate_uuid()).get('loadbalancer') + self.sctp_lb_id = self.sctp_lb.get('id') + self.set_lb_status(self.sctp_lb_id) + + self.sctp_listener = self.create_listener( + lib_consts.PROTOCOL_SCTP, 8888, + self.sctp_lb_id).get('listener') + self.sctp_listener_id = self.sctp_listener.get('id') + self.set_lb_status(self.sctp_lb_id) + + self.sctp_pool_with_listener = self.create_pool( + None, lib_consts.PROTOCOL_SCTP, constants.LB_ALGORITHM_ROUND_ROBIN, + listener_id=self.sctp_listener_id) + self.sctp_pool_with_listener_id = ( + self.sctp_pool_with_listener.get('pool').get('id')) + self.set_lb_status(self.sctp_lb_id) + def test_get(self): api_hm = self.create_health_monitor( self.pool_id, constants.HEALTH_MONITOR_HTTP, @@ -936,6 +957,32 @@ class TestHealthMonitor(base.BaseAPITest): self.assertEqual('/test.html', api_hm.get('url_path')) self.assertEqual('200-201', api_hm.get('expected_codes')) + def test_create_udp_case_with_sctp_type(self): + # create with SCTP type + api_hm = self.create_health_monitor( + self.udp_pool_with_listener_id, + lib_consts.HEALTH_MONITOR_SCTP, + 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(lib_consts.HEALTH_MONITOR_SCTP, + 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')) + # Verify the L7 fields is None + self.assertIsNone(api_hm.get('http_method')) + self.assertIsNone(api_hm.get('url_path')) + self.assertIsNone(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, @@ -981,6 +1028,7 @@ class TestHealthMonitor(base.BaseAPITest): "monitor is supported.") % { 'pool_protocol': constants.PROTOCOL_UDP, 'types': '/'.join([constants.HEALTH_MONITOR_UDP_CONNECT, + lib_consts.HEALTH_MONITOR_SCTP, constants.HEALTH_MONITOR_TCP, constants.HEALTH_MONITOR_HTTP])} @@ -1005,7 +1053,8 @@ class TestHealthMonitor(base.BaseAPITest): "supported for pools of type " "%(protocol)s.") % { 'type': constants.HEALTH_MONITOR_UDP_CONNECT, - 'protocol': constants.PROTOCOL_UDP} + 'protocol': '/'.join((constants.PROTOCOL_UDP, + lib_consts.PROTOCOL_SCTP))} res = self.post(self.HMS_PATH, self._build_body(req_dict), status=400, expect_errors=True) @@ -1014,6 +1063,108 @@ class TestHealthMonitor(base.BaseAPITest): lb_id=self.udp_lb_id, listener_id=self.udp_listener_id, pool_id=self.udp_pool_with_listener_id) + def test_create_sctp_case_with_udp_connect_type(self): + # create with UDP-CONNECT type + api_hm = self.create_health_monitor( + self.sctp_pool_with_listener_id, + constants.HEALTH_MONITOR_UDP_CONNECT, + 3, 1, 1, 1).get(self.root_tag) + self.assert_correct_status( + lb_id=self.sctp_lb_id, listener_id=self.sctp_listener_id, + pool_id=self.sctp_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.sctp_lb_id) + self.assertEqual(constants.HEALTH_MONITOR_UDP_CONNECT, + 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')) + # Verify the L7 fields is None + self.assertIsNone(api_hm.get('http_method')) + self.assertIsNone(api_hm.get('url_path')) + self.assertIsNone(api_hm.get('expected_codes')) + + def test_create_sctp_case_with_tcp_type(self): + # create with TCP type + api_hm = self.create_health_monitor( + self.sctp_pool_with_listener_id, constants.HEALTH_MONITOR_TCP, + 3, 1, 1, 1).get(self.root_tag) + self.assert_correct_status( + lb_id=self.sctp_lb_id, listener_id=self.sctp_listener_id, + pool_id=self.sctp_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.sctp_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_sctp_case_with_http_type(self): + # create with HTTP type + api_hm = self.create_health_monitor( + self.sctp_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.sctp_lb_id, listener_id=self.sctp_listener_id, + pool_id=self.sctp_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.sctp_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_create_sctp_case_with_sctp_type(self): + # create with SCTP type + api_hm = self.create_health_monitor( + self.sctp_pool_with_listener_id, + lib_consts.HEALTH_MONITOR_SCTP, + 3, 1, 1, 1).get(self.root_tag) + self.assert_correct_status( + lb_id=self.sctp_lb_id, listener_id=self.sctp_listener_id, + pool_id=self.sctp_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.sctp_lb_id) + self.assertEqual(lib_consts.HEALTH_MONITOR_SCTP, + 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')) + # Verify the L7 fields is None + self.assertIsNone(api_hm.get('http_method')) + self.assertIsNone(api_hm.get('url_path')) + self.assertIsNone(api_hm.get('expected_codes')) + def test_ensure_L7_fields_filled_during_create(self): # Create a health monitor with a load balancer pool api_hm = self.create_health_monitor( diff --git a/octavia/tests/functional/api/v2/test_pool.py b/octavia/tests/functional/api/v2/test_pool.py index b2c0b7b062..6f08a0acf1 100644 --- a/octavia/tests/functional/api/v2/test_pool.py +++ b/octavia/tests/functional/api/v2/test_pool.py @@ -27,6 +27,7 @@ from octavia.db import api as db_api from octavia.tests.common import constants as c_const from octavia.tests.common import sample_certs from octavia.tests.functional.api.v2 import base +from octavia_lib.common import constants as lib_consts class TestPool(base.BaseAPITest): @@ -1026,7 +1027,9 @@ class TestPool(base.BaseAPITest): 'lb_algorithm': constants.LB_ALGORITHM_ROUND_ROBIN, 'session_persistence': sp} expect_error_msg = ("Validation failure: Cookie names are not " - "supported for %s pools.") % constants.PROTOCOL_UDP + "supported for %s pools.") % ( + "/".join((constants.PROTOCOL_UDP, + lib_consts.PROTOCOL_SCTP))) res = self.post(self.POOLS_PATH, self._build_body(req_dict), status=400, expect_errors=True) self.assertEqual(expect_error_msg, res.json['faultstring']) @@ -1047,7 +1050,10 @@ class TestPool(base.BaseAPITest): constants.SESSION_PERSISTENCE_APP_COOKIE]: expect_error_msg = ("Validation failure: Session persistence of " "type %s is not supported for %s protocol " - "pools.") % (type, constants.PROTOCOL_UDP) + "pools.") % ( + type, + "/".join((constants.PROTOCOL_UDP, + lib_consts.PROTOCOL_SCTP))) sp.update({'type': type}) req_dict['session_persistence'] = sp res = self.post(self.POOLS_PATH, self._build_body(req_dict), @@ -1070,9 +1076,11 @@ class TestPool(base.BaseAPITest): 'session_persistence': sp} expect_error_msg = ( "Validation failure: session_persistence %s type for %s " - "protocol only accepts: type, persistence_timeout, " + "protocols only accepts: type, persistence_timeout, " "persistence_granularity.") % ( - constants.SESSION_PERSISTENCE_SOURCE_IP, constants.PROTOCOL_UDP) + constants.SESSION_PERSISTENCE_SOURCE_IP, + " and ".join((constants.PROTOCOL_UDP, + lib_consts.PROTOCOL_SCTP))) res = self.post(self.POOLS_PATH, self._build_body(req_dict), status=400, expect_errors=True) self.assertEqual(expect_error_msg, res.json['faultstring']) @@ -1092,7 +1100,9 @@ class TestPool(base.BaseAPITest): 'lb_algorithm': constants.LB_ALGORITHM_ROUND_ROBIN} expect_error_msg = ("Validation failure: persistence_timeout and " "persistence_granularity is only for %s protocol " - "pools.") % constants.PROTOCOL_UDP + "pools.") % ( + " and ".join((constants.PROTOCOL_UDP, + lib_consts.PROTOCOL_SCTP))) for s in sps: req_dict.update({'session_persistence': s}) res = self.post(self.POOLS_PATH, self._build_body(req_dict), @@ -1320,7 +1330,8 @@ class TestPool(base.BaseAPITest): # Error during update pool with non-UDP type and cookie_name. expect_error_msg = ( "Validation failure: Cookie names are not supported for %s" - " pools.") % constants.PROTOCOL_UDP + " pools.") % ("/".join((constants.PROTOCOL_UDP, + lib_consts.PROTOCOL_SCTP))) sess_p['type'] = constants.SESSION_PERSISTENCE_HTTP_COOKIE sess_p['cookie_name'] = 'test-cookie-name' new_pool = {'session_persistence': sess_p} @@ -1333,10 +1344,11 @@ class TestPool(base.BaseAPITest): # Error during update pool with source ip type and more options. expect_error_msg = ( - "Validation failure: session_persistence %s type for %s protocol " + "Validation failure: session_persistence %s type for %s protocols " "only accepts: type, persistence_timeout, " "persistence_granularity.") % ( - constants.SESSION_PERSISTENCE_SOURCE_IP, constants.PROTOCOL_UDP) + constants.SESSION_PERSISTENCE_SOURCE_IP, + " and ".join((constants.PROTOCOL_UDP, lib_consts.PROTOCOL_SCTP))) sess_p['type'] = constants.SESSION_PERSISTENCE_SOURCE_IP sess_p['cookie_name'] = 'test-cookie-name' sess_p['persistence_timeout'] = 4 @@ -1354,7 +1366,10 @@ class TestPool(base.BaseAPITest): constants.SESSION_PERSISTENCE_HTTP_COOKIE]: expect_error_msg = ("Validation failure: Session persistence of " "type %s is not supported for %s protocol " - "pools.") % (ty, constants.PROTOCOL_UDP) + "pools.") % ( + ty, + "/".join((constants.PROTOCOL_UDP, + lib_consts.PROTOCOL_SCTP))) sess_p['type'] = ty res = self.put(self.POOL_PATH.format(pool_id=api_pool.get('id')), self._build_body(new_pool), status=400, diff --git a/releasenotes/notes/add-support-for-sctp-protocol-152444b211ab2188.yaml b/releasenotes/notes/add-support-for-sctp-protocol-152444b211ab2188.yaml new file mode 100644 index 0000000000..c3a2e166f1 --- /dev/null +++ b/releasenotes/notes/add-support-for-sctp-protocol-152444b211ab2188.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add support for SCTP protocol. SCTP support has been added in the Octavia + API for listener, pool, and health-monitor resources.