Add SCTP support in API

Add SCTP support in the API for listeners, pools, health-monitors
resources.

Story: 2007884
Task: 40255

Change-Id: I57a3c528a20943724bdcd36422c689f496068330
This commit is contained in:
Gregory Thiemonge 2020-09-08 19:08:34 +02:00
parent 4260d8a74b
commit 639c11751e
21 changed files with 385 additions and 98 deletions

View File

@ -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

View File

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

View File

@ -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:

View File

@ -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

View File

@ -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``,

View File

@ -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 |

View File

@ -159,6 +159,14 @@ cli=openstack loadbalancer healthmonitor create --type UDP-CONNECT <pool>
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 <pool>
driver.amphora=missing
driver.ovn=missing
[operation.url_path]
title=url_path
status=optional

View File

@ -230,6 +230,14 @@ cli=openstack loadbalancer listener create --protocol UDP <loadbalancer>
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 <loadbalancer>
driver.amphora=missing
driver.ovn=missing
[operation.protocol_port]
title=protocol_port
status=mandatory

View File

@ -138,6 +138,14 @@ cli=openstack loadbalancer pool create --protocol UDP --listener <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 <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 <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 <listener>
driver.amphora=complete
driver.ovn=missing

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'}
]
)

View File

@ -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(),

View File

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

View File

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

View File

@ -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,

View File

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