Add ALPN support for TLS-enabled pools

ALPN is a TLS extension for application-layer protocol negotiation
within the TLS handshake [1].

This patch extends the Pool API to include a new 'alpn_protocols'
parameter. With this parameter, users can set an ALPN preference list
(descending order of preference) to be advertised by load balancer to
members.

This patch also adds HTTP/2 over TLS support to TLS-enabled pools to the
Amphora provider driver, although default the pool ALPN protocol list
configuration setting has HTTP/2 disabled similarly to the default
listener ALPN protocol list value added in Victoria release.

[1] https://tools.ietf.org/html/rfc7301

Change-Id: I91924486bab22601c15c538c8a5282ad8bc54700
This commit is contained in:
Carlos Goncalves 2020-09-15 18:21:41 +00:00
parent fbbc5f9024
commit d2d5fc80f8
37 changed files with 366 additions and 60 deletions

View File

@ -183,7 +183,7 @@ allowed_cidrs-optional:
min_version: 2.12
required: false
type: array
alpn_protocols:
alpn_protocols-listener:
description: |
A list of ALPN protocols.
Available protocols: http/1.0, http/1.1, h2
@ -191,7 +191,7 @@ alpn_protocols:
min_version: 2.20
required: true
type: array
alpn_protocols-optional:
alpn_protocols-listener-optional:
description: |
A list of ALPN protocols.
Available protocols: http/1.0, http/1.1, h2
@ -199,6 +199,22 @@ alpn_protocols-optional:
min_version: 2.20
required: false
type: array
alpn_protocols-pool:
description: |
A list of ALPN protocols.
Available protocols: http/1.0, http/1.1, h2
in: body
min_version: 2.24
required: true
type: array
alpn_protocols-pool-optional:
description: |
A list of ALPN protocols.
Available protocols: http/1.0, http/1.1, h2
in: body
min_version: 2.24
required: false
type: array
amphora-id:
description: |
The associated amphora ID.

View File

@ -1 +1 @@
curl -X POST -H "Content-Type: application/json" -H "X-Auth-Token: <token>" -d '{"pool":{"lb_algorithm":"ROUND_ROBIN","protocol":"HTTP","description":"Super Round Robin Pool","admin_state_up":true,"session_persistence":{"cookie_name":"ChocolateChip","type":"APP_COOKIE"},"listener_id":"023f2e34-7806-443b-bfae-16c324569a3d","name":"super-pool","tags":["test_tag"],"tls_container_ref":"http://198.51.100.10:9311/v1/containers/4073846f-1d5e-42e1-a4cf-a7046419d0e6","ca_tls_container_ref":"http://198.51.100.10:9311/v1/containers/5f0d5540-fae6-4646-85d6-8a84883807fb","crl_container_ref":"http://198.51.100.10:9311/v1/containers/6faf0a01-6892-454c-aaac-650282820c0b","tls_enabled":true,"tls_ciphers":"ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256", "tls_versions": ["TLSv1.2", "TLSv1.3"]}}' http://198.51.100.10:9876/v2/lbaas/pools
curl -X POST -H "Content-Type: application/json" -H "X-Auth-Token: <token>" -d '{"pool":{"lb_algorithm":"ROUND_ROBIN","protocol":"HTTP","description":"Super Round Robin Pool","admin_state_up":true,"session_persistence":{"cookie_name":"ChocolateChip","type":"APP_COOKIE"},"listener_id":"023f2e34-7806-443b-bfae-16c324569a3d","name":"super-pool","tags":["test_tag"],"tls_container_ref":"http://198.51.100.10:9311/v1/containers/4073846f-1d5e-42e1-a4cf-a7046419d0e6","ca_tls_container_ref":"http://198.51.100.10:9311/v1/containers/5f0d5540-fae6-4646-85d6-8a84883807fb","crl_container_ref":"http://198.51.100.10:9311/v1/containers/6faf0a01-6892-454c-aaac-650282820c0b","tls_enabled":true,"tls_ciphers":"ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256", "tls_versions": ["TLSv1.2", "TLSv1.3"], "alpn_protocols": ["http/1.1", "http/1.0"]}}' http://198.51.100.10:9876/v2/lbaas/pools

View File

@ -16,6 +16,7 @@
"crl_container_ref": "http://198.51.100.10:9311/v1/containers/6faf0a01-6892-454c-aaac-650282820c0b",
"tls_enabled": true,
"tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256",
"tls_versions": ["TLSv1.2", "TLSv1.3"]
"tls_versions": ["TLSv1.2", "TLSv1.3"],
"alpn_protocols": ["http/1.1", "http/1.0"]
}
}

View File

@ -33,6 +33,7 @@
"crl_container_ref": "http://198.51.100.10:9311/v1/containers/6faf0a01-6892-454c-aaac-650282820c0b",
"tls_enabled": true,
"tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256",
"tls_versions": ["TLSv1.2", "TLSv1.3"]
"tls_versions": ["TLSv1.2", "TLSv1.3"],
"alpn_protocols": ["http/1.1", "http/1.0"]
}
}

View File

@ -33,6 +33,7 @@
"crl_container_ref": "http://198.51.100.10:9311/v1/containers/6faf0a01-6892-454c-aaac-650282820c0b",
"tls_enabled": false,
"tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256",
"tls_versions": ["TLSv1.2", "TLSv1.3"]
"tls_versions": ["TLSv1.2", "TLSv1.3"],
"alpn_protocols": ["http/1.1", "http/1.0"]
}
}

View File

@ -1 +1 @@
curl -X PUT -H "Content-Type: application/json" -H "X-Auth-Token: <token>" -d '{"pool":{"lb_algorithm":"LEAST_CONNECTIONS","session_persistence":{"type":"SOURCE_IP"},"description":"second description","name":"second_name","tags":["updated_tag"],"tls_container_ref":"http://198.51.100.10:9311/v1/containers/c1cd501d-3cf9-4873-a11b-a74bebcde929","ca_tls_container_ref":null,"crl_container_ref":null,"tls_enabled":false,"tls_ciphers":"ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256", "tls_versions": ["TLSv1.2", "TLSv1.3"]}}' http://198.51.100.10:9876/v2/lbaas/pools/4029d267-3983-4224-a3d0-afb3fe16a2cd
curl -X PUT -H "Content-Type: application/json" -H "X-Auth-Token: <token>" -d '{"pool":{"lb_algorithm":"LEAST_CONNECTIONS","session_persistence":{"type":"SOURCE_IP"},"description":"second description","name":"second_name","tags":["updated_tag"],"tls_container_ref":"http://198.51.100.10:9311/v1/containers/c1cd501d-3cf9-4873-a11b-a74bebcde929","ca_tls_container_ref":null,"crl_container_ref":null,"tls_enabled":false,"tls_ciphers":"ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256", "tls_versions": ["TLSv1.2", "TLSv1.3"], "alpn_protocols": ["http/1.1", "http/1.0"]}}' http://198.51.100.10:9876/v2/lbaas/pools/4029d267-3983-4224-a3d0-afb3fe16a2cd

View File

@ -12,6 +12,7 @@
"crl_container_ref": null,
"tls_enabled": false,
"tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256",
"tls_versions": ["TLSv1.2", "TLSv1.3"]
"tls_versions": ["TLSv1.2", "TLSv1.3"],
"alpn_protocols": ["http/1.1", "http/1.0"]
}
}

View File

@ -33,6 +33,7 @@
"crl_container_ref": null,
"tls_enabled": false,
"tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256",
"tls_versions": ["TLSv1.2", "TLSv1.3"]
"tls_versions": ["TLSv1.2", "TLSv1.3"],
"alpn_protocols": ["http/1.1", "http/1.0"]
}
}

View File

@ -39,7 +39,8 @@
"crl_container_ref": "http://198.51.100.10:9311/v1/containers/6faf0a01-6892-454c-aaac-650282820c0b",
"tls_enabled": true,
"tls_ciphers": "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256",
"tls_versions": ["TLSv1.2", "TLSv1.3"]
"tls_versions": ["TLSv1.2", "TLSv1.3"],
"alpn_protocols": ["http/1.1", "http/1.0"]
}
]
}

View File

@ -47,7 +47,7 @@ Response Parameters
- admin_state_up: admin_state_up
- allowed_cidrs: allowed_cidrs
- alpn_protocols: alpn_protocols
- alpn_protocols: alpn_protocols-listener
- client_authentication: client_authentication
- client_ca_tls_container_ref: client_ca_tls_container_ref
- client_crl_container_ref: client_crl_container_ref
@ -144,7 +144,7 @@ Request
- admin_state_up: admin_state_up-default-optional
- allowed_cidrs: allowed_cidrs-optional
- alpn_protocols: alpn_protocols-optional
- alpn_protocols: alpn_protocols-listener-optional
- client_authentication: client_authentication-optional
- client_ca_tls_container_ref: client_ca_tls_container_ref-optional
- client_crl_container_ref: client_crl_container_ref-optional
@ -268,7 +268,7 @@ Response Parameters
- admin_state_up: admin_state_up
- allowed_cidrs: allowed_cidrs
- alpn_protocols: alpn_protocols
- alpn_protocols: alpn_protocols-listener
- client_authentication: client_authentication
- client_ca_tls_container_ref: client_ca_tls_container_ref
- client_crl_container_ref: client_crl_container_ref
@ -349,7 +349,7 @@ Response Parameters
- admin_state_up: admin_state_up
- allowed_cidrs: allowed_cidrs
- alpn_protocols: alpn_protocols
- alpn_protocols: alpn_protocols-listener
- client_authentication: client_authentication
- client_ca_tls_container_ref: client_ca_tls_container_ref
- client_crl_container_ref: client_crl_container_ref
@ -420,7 +420,7 @@ Request
- admin_state_up: admin_state_up-default-optional
- allowed_cidrs: allowed_cidrs-optional
- alpn_protocols: alpn_protocols-optional
- alpn_protocols: alpn_protocols-listener-optional
- client_authentication: client_authentication-optional
- client_ca_tls_container_ref: client_ca_tls_container_ref-optional
- client_crl_container_ref: client_crl_container_ref-optional
@ -459,7 +459,7 @@ Response Parameters
- admin_state_up: admin_state_up
- allowed_cidrs: allowed_cidrs
- alpn_protocols: alpn_protocols
- alpn_protocols: alpn_protocols-listener
- client_authentication: client_authentication
- client_ca_tls_container_ref: client_ca_tls_container_ref
- client_crl_container_ref: client_crl_container_ref

View File

@ -46,6 +46,7 @@ Response Parameters
.. rest_parameters:: ../parameters.yaml
- admin_state_up: admin_state_up
- alpn_protocols: alpn_protocols-pool
- ca_tls_container_ref: ca_tls_container_ref
- created_at: created_at
- crl_container_ref: crl_container_ref
@ -170,6 +171,7 @@ Request
.. rest_parameters:: ../parameters.yaml
- admin_state_up: admin_state_up-default-optional
- alpn_protocols: alpn_protocols-pool-optional
- ca_tls_container_ref: ca_tls_container_ref-optional
- crl_container_ref: crl_container_ref-optional
- description: description-optional
@ -247,6 +249,7 @@ Response Parameters
.. rest_parameters:: ../parameters.yaml
- admin_state_up: admin_state_up
- alpn_protocols: alpn_protocols-pool
- ca_tls_container_ref: ca_tls_container_ref
- created_at: created_at
- crl_container_ref: crl_container_ref
@ -320,6 +323,7 @@ Response Parameters
.. rest_parameters:: ../parameters.yaml
- admin_state_up: admin_state_up
- alpn_protocols: alpn_protocols-pool
- ca_tls_container_ref: ca_tls_container_ref
- created_at: created_at
- crl_container_ref: crl_container_ref
@ -383,6 +387,7 @@ Request
.. rest_parameters:: ../parameters.yaml
- admin_state_up: admin_state_up-default-optional
- alpn_protocols: alpn_protocols-pool-optional
- ca_tls_container_ref: ca_tls_container_ref-optional
- crl_container_ref: crl_container_ref-optional
- description: description-optional
@ -414,6 +419,7 @@ Response Parameters
.. rest_parameters:: ../parameters.yaml
- admin_state_up: admin_state_up
- alpn_protocols: alpn_protocols-pool
- ca_tls_container_ref: ca_tls_container_ref
- created_at: created_at
- crl_container_ref: crl_container_ref

View File

@ -30,6 +30,14 @@ cli=openstack loadbalancer pool create [--enable | --disable] --listener <listen
driver.amphora=complete
driver.ovn=complete
[operation.alpn_protocol]
title=alpn_protocol
status=optional
notes=List of accepted ALPN protocols (can be set multiple times).
cli=openstack loadbalancer pool create [--alpn-protocol <protocol>] --listener <listener>
driver.amphora=complete
driver.ovn=missing
[operation.ca_tls_container_ref]
title=ca_tls_container_ref
status=optional

View File

@ -88,6 +88,10 @@
# listeners. Available protocols: http/1.0, http/1.1, h2
# default_listener_alpn_protocols = http/1.1, http/1.0
# List of default ALPN protocols to be used on new TLS-terminated
# pools. Available protocols: http/1.0, http/1.1, h2
# default_pool_alpn_protocols = http/1.1, http/1.0
[database]
# This line MUST be changed to actually run the plugin.

View File

@ -72,7 +72,7 @@ munch==2.2.0
netaddr==0.7.19
netifaces==0.10.4
networkx==2.1.0
octavia-lib==2.2.0
octavia-lib==2.3.0
openstacksdk==0.12.0
os-client-config==1.29.0
os-service-types==1.2.0

View File

@ -83,11 +83,11 @@ class AmphoraProviderDriver(driver_base.ProviderDriver):
user_fault_string=msg,
operator_fault_string=msg)
def _validate_alpn_protocols(self, listener):
if not listener.alpn_protocols:
def _validate_alpn_protocols(self, obj):
if not obj.alpn_protocols:
return
supported = consts.AMPHORA_SUPPORTED_ALPN_PROTOCOLS
not_supported = set(listener.alpn_protocols) - set(supported)
not_supported = set(obj.alpn_protocols) - set(supported)
if not_supported:
msg = ('Amphora provider does not support %s ALPN protocol(s). '
'Supported: %s'
@ -189,6 +189,7 @@ class AmphoraProviderDriver(driver_base.ProviderDriver):
# Pool
def pool_create(self, pool):
self._validate_pool_algorithm(pool)
self._validate_alpn_protocols(pool)
payload = {consts.POOL_ID: pool.pool_id}
self.client.cast({}, 'create_pool', **payload)
@ -198,6 +199,7 @@ class AmphoraProviderDriver(driver_base.ProviderDriver):
self.client.cast({}, 'delete_pool', **payload)
def pool_update(self, old_pool, new_pool):
self._validate_alpn_protocols(new_pool)
if new_pool.lb_algorithm:
self._validate_pool_algorithm(new_pool)
pool_dict = new_pool.to_dict()

View File

@ -85,11 +85,11 @@ class AmphoraProviderDriver(driver_base.ProviderDriver):
user_fault_string=msg,
operator_fault_string=msg)
def _validate_alpn_protocols(self, listener):
if not listener.alpn_protocols:
def _validate_alpn_protocols(self, obj):
if not obj.alpn_protocols:
return
supported = consts.AMPHORA_SUPPORTED_ALPN_PROTOCOLS
not_supported = set(listener.alpn_protocols) - set(supported)
not_supported = set(obj.alpn_protocols) - set(supported)
if not_supported:
msg = ('Amphora provider does not support %s ALPN protocol(s). '
'Supported: %s'
@ -229,6 +229,7 @@ class AmphoraProviderDriver(driver_base.ProviderDriver):
def pool_create(self, pool):
self._validate_pool_algorithm(pool)
self._validate_alpn_protocols(pool)
payload = {consts.POOL: self._pool_convert_to_dict(pool)}
self.client.cast({}, 'create_pool', **payload)
@ -237,6 +238,7 @@ class AmphoraProviderDriver(driver_base.ProviderDriver):
self.client.cast({}, 'delete_pool', **payload)
def pool_update(self, old_pool, new_pool):
self._validate_alpn_protocols(new_pool)
if new_pool.lb_algorithm:
self._validate_pool_algorithm(new_pool)
pool_dict = self._pool_convert_to_dict(new_pool)

View File

@ -124,7 +124,7 @@ class RootController(object):
# Add quota support to octavia's l7policy and l7rule
self._add_a_version(versions, 'v2.19', 'v2', 'SUPPORTED',
'2020-05-12T00:00:00Z', host_url)
# ALPN protocols
# ALPN protocols (listener)
self._add_a_version(versions, 'v2.20', 'v2', 'SUPPORTED',
'2020-08-02T00:00:00Z', host_url)
# Amphora delete
@ -134,6 +134,9 @@ class RootController(object):
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',
self._add_a_version(versions, 'v2.23', 'v2', 'SUPPORTED',
'2020-09-07T00:00:00Z', host_url)
# ALPN protocols (pool)
self._add_a_version(versions, 'v2.24', 'v2', 'CURRENT',
'2020-10-15T00:00:00Z', host_url)
return {'versions': versions}

View File

@ -137,6 +137,8 @@ class PoolsController(base.BaseController):
validate.check_tls_version_list(pool_dict['tls_versions'])
# Validate TLS versions against minimum
validate.check_tls_version_min(pool_dict['tls_versions'])
# Validate ALPN protocol list
validate.check_alpn_protocols(pool_dict['alpn_protocols'])
try:
return self.repositories.create_pool_on_load_balancer(
@ -420,6 +422,10 @@ class PoolsController(base.BaseController):
# Validate TLS version against minimum
validate.check_tls_version_min(pool.tls_versions)
if pool.alpn_protocols is not wtypes.Unset:
# Validate ALPN protocol list
validate.check_alpn_protocols(pool.alpn_protocols)
@wsme_pecan.wsexpose(pool_types.PoolRootResponse, wtypes.text,
body=pool_types.PoolRootPut, status_code=200)
def put(self, id, pool_):

View File

@ -86,6 +86,7 @@ class PoolResponse(BasePoolType):
tls_enabled = wtypes.wsattr(bool)
tls_ciphers = wtypes.wsattr(wtypes.StringType())
tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType()))
alpn_protocols = wtypes.wsattr(wtypes.ArrayType(types.AlpnProtocolType()))
@classmethod
def from_data_model(cls, data_model, children=False):
@ -118,6 +119,7 @@ class PoolResponse(BasePoolType):
member_model.from_data_model(i) for i in data_model.members]
pool.tls_versions = data_model.tls_versions
pool.alpn_protocols = data_model.alpn_protocols
return pool
@ -167,6 +169,7 @@ class PoolPOST(BasePoolType):
tls_ciphers = wtypes.wsattr(wtypes.StringType(max_length=2048))
tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(
max_length=32)))
alpn_protocols = wtypes.wsattr(wtypes.ArrayType(types.AlpnProtocolType()))
class PoolRootPOST(types.BaseType):
@ -189,6 +192,7 @@ class PoolPUT(BasePoolType):
tls_ciphers = wtypes.wsattr(wtypes.StringType(max_length=2048))
tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(
max_length=32)))
alpn_protocols = wtypes.wsattr(wtypes.ArrayType(types.AlpnProtocolType()))
class PoolRootPut(types.BaseType):
@ -215,6 +219,7 @@ class PoolSingleCreate(BasePoolType):
tls_ciphers = wtypes.wsattr(wtypes.StringType(max_length=2048))
tls_versions = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(
max_length=32)))
alpn_protocols = wtypes.wsattr(wtypes.ArrayType(types.AlpnProtocolType()))
class PoolStatusResponse(BasePoolType):

View File

@ -134,6 +134,11 @@ api_opts = [
lib_consts.ALPN_PROTOCOL_HTTP_1_0],
help=_('List of ALPN protocols to use for new TLS-enabled '
'listeners.')),
cfg.ListOpt('default_pool_alpn_protocols',
default=[lib_consts.ALPN_PROTOCOL_HTTP_1_1,
lib_consts.ALPN_PROTOCOL_HTTP_1_0],
help=_('List of ALPN protocols to use for new TLS-enabled '
'pools.')),
]
# Options only used by the amphora agent

View File

@ -293,7 +293,8 @@ class Pool(BaseDataModel):
created_at=None, updated_at=None, provisioning_status=None,
tags=None, tls_certificate_id=None,
ca_tls_certificate_id=None, crl_container_id=None,
tls_enabled=None, tls_ciphers=None, tls_versions=None):
tls_enabled=None, tls_ciphers=None, tls_versions=None,
alpn_protocols=None):
self.id = id
self.project_id = project_id
self.name = name
@ -319,6 +320,7 @@ class Pool(BaseDataModel):
self.tls_enabled = tls_enabled
self.tls_ciphers = tls_ciphers
self.tls_versions = tls_versions
self.alpn_protocols = alpn_protocols
def update(self, update_dict):
for key, value in update_dict.items():

View File

@ -369,6 +369,8 @@ class JinjaTemplater(object):
ret_value['tls_ciphers'] = pool.tls_ciphers
if pool.tls_versions is not None:
ret_value['tls_versions'] = pool.tls_versions
if pool.alpn_protocols is not None:
ret_value['alpn_protocols'] = ",".join(pool.alpn_protocols)
if (pool.ca_tls_certificate_id and pool_tls_certs and
pool_tls_certs.get('ca_cert')):
ret_value['ca_cert'] = pool_tls_certs.get('ca_cert')

View File

@ -284,11 +284,17 @@ frontend {{ listener.id }}
{% set tls_versions_opt = tls_versions_opt + " no-tlsv13" %}
{% endif %}
{% endif %}
{{ "server %s %s:%d weight %s%s%s%s%s%s%s%s%s%s%s%s%s%s"|e|format(
{% if pool.alpn_protocols is defined %}
{% set alpn_opt = " alpn %s"|format(pool.alpn_protocols) %}
{% else %}
{% set alpn_opt = "" %}
{% endif %}
{{ "server %s %s:%d weight %s%s%s%s%s%s%s%s%s%s%s%s%s%s%s"|e|format(
member.id, member.address, member.protocol_port, member.weight,
hm_opt, persistence_opt, proxy_protocol_opt, member_backup_opt,
member_enabled_opt, def_opt_prefix, def_crt_opt, ca_opt, crl_opt,
def_verify_opt, def_sni_opt, ciphers_opt, tls_versions_opt)|trim() }}
def_verify_opt, def_sni_opt, ciphers_opt, tls_versions_opt,
alpn_opt)|trim() }}
{% endmacro %}

View File

@ -0,0 +1,35 @@
# Copyright 2020 Red Hat, Inc. All rights reserved.
#
# 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.
"""add pool alpn protocols column
Revision ID: be9fdc039b51
Revises: 8b47b2546312
Create Date: 2020-09-15 09:30:00.521760
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'be9fdc039b51'
down_revision = '8b47b2546312'
def upgrade():
op.add_column(
'pool',
sa.Column('alpn_protocols', sa.String(512), nullable=True)
)

View File

@ -353,6 +353,7 @@ class Pool(base_models.BASE, base_models.IdMixin, base_models.ProjectMixin,
tls_enabled = sa.Column(sa.Boolean, default=False, nullable=False)
tls_ciphers = sa.Column(sa.String(2048), nullable=True)
tls_versions = sa.Column(ScalarListType(), nullable=True)
alpn_protocols = sa.Column(ScalarListType(), nullable=True)
# This property should be a unique list of any listeners that reference
# this pool as its default_pool and any listeners referenced by enabled

View File

@ -184,6 +184,9 @@ def create_pool(pool_dict, lb_id=None):
if pool_dict['tls_versions'] is None:
pool_dict['tls_versions'] = (
CONF.api_settings.default_pool_tls_versions)
if pool_dict['alpn_protocols'] is None:
pool_dict['alpn_protocols'] = (
CONF.api_settings.default_pool_alpn_protocols)
pool_dict[constants.PROVISIONING_STATUS] = constants.PENDING_CREATE
pool_dict[constants.OPERATING_STATUS] = constants.OFFLINE
return pool_dict

View File

@ -249,7 +249,9 @@ class SampleDriverDataModels(object):
constants.CRL_CONTAINER_ID: self.pool_crl_container_ref,
lib_consts.TLS_ENABLED: True,
lib_consts.TLS_CIPHERS: None,
lib_consts.TLS_VERSIONS: None
lib_consts.TLS_VERSIONS: None,
lib_consts.ALPN_PROTOCOLS:
constants.AMPHORA_SUPPORTED_ALPN_PROTOCOLS
}
self.test_pool1_dict.update(self._common_test_dict)
@ -300,6 +302,8 @@ class SampleDriverDataModels(object):
lib_consts.TLS_ENABLED: True,
lib_consts.TLS_CIPHERS: None,
lib_consts.TLS_VERSIONS: None,
lib_consts.ALPN_PROTOCOLS:
constants.AMPHORA_SUPPORTED_ALPN_PROTOCOLS
}
self.provider_pool2_dict = copy.deepcopy(self.provider_pool1_dict)

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(24, len(version_ids))
self.assertEqual(25, len(version_ids))
self.assertIn('v2.0', version_ids)
self.assertIn('v2.1', version_ids)
self.assertIn('v2.2', version_ids)
@ -70,6 +70,7 @@ class TestRootController(base_db_test.OctaviaDBTestBase):
self.assertIn('v2.21', version_ids)
self.assertIn('v2.22', version_ids)
self.assertIn('v2.23', version_ids)
self.assertIn('v2.24', 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

@ -2623,3 +2623,73 @@ class TestPool(base.BaseAPITest):
self.assertEqual(expect_error_msg,
res.json['faultstring'])
self.assert_correct_status(lb_id=self.lb_id)
def test_create_with_alpn(self):
alpn_protocols = [lib_constants.ALPN_PROTOCOL_HTTP_2,
lib_constants.ALPN_PROTOCOL_HTTP_1_1]
api_pool = self.create_pool(
self.lb_id,
constants.PROTOCOL_HTTP,
constants.LB_ALGORITHM_ROUND_ROBIN,
listener_id=self.listener_id,
tls_enabled=True,
alpn_protocols=alpn_protocols).get(self.root_tag)
self.assertEqual(alpn_protocols, api_pool['alpn_protocols'])
def test_create_with_alpn_negative(self):
req_dict = {'protocol': constants.PROTOCOL_HTTP,
'lb_algorithm': constants.LB_ALGORITHM_ROUND_ROBIN,
'listener_id': self.listener_id,
'tls_enabled': True,
'alpn_protocols': [lib_constants.ALPN_PROTOCOL_HTTP_1_1,
'invalid-proto']}
res = self.post(self.POOLS_PATH, self._build_body(req_dict),
status=400)
fault = res.json['faultstring']
self.assertIn(
'Invalid input for field/attribute alpn_protocols', fault)
self.assertIn('Value should be a valid ALPN protocol ID', fault)
self.assert_correct_status(lb_id=self.lb_id)
def test_update_with_alpn(self):
alpn_protocols_orig = [lib_constants.ALPN_PROTOCOL_HTTP_1_0]
alpn_protocols = [lib_constants.ALPN_PROTOCOL_HTTP_2,
lib_constants.ALPN_PROTOCOL_HTTP_1_1]
pool = self.create_pool(
self.lb_id,
constants.PROTOCOL_HTTP,
constants.LB_ALGORITHM_ROUND_ROBIN,
listener_id=self.listener_id,
tls_enabled=True,
alpn_protocols=alpn_protocols_orig)
self.set_lb_status(self.lb_id)
pool_path = self.POOL_PATH.format(pool_id=pool['pool']['id'])
get_pool = self.get(pool_path).json['pool']
self.assertEqual(alpn_protocols_orig, get_pool.get('alpn_protocols'))
self.put(pool_path,
self._build_body({'alpn_protocols': alpn_protocols}))
get_pool = self.get(pool_path).json['pool']
self.assertEqual(alpn_protocols, get_pool.get('alpn_protocols'))
def test_update_with_alpn_negative(self):
alpn_protocols_orig = [lib_constants.ALPN_PROTOCOL_HTTP_1_0]
pool = self.create_pool(
self.lb_id,
constants.PROTOCOL_HTTP,
constants.LB_ALGORITHM_ROUND_ROBIN,
listener_id=self.listener_id,
tls_enabled=True,
alpn_protocols=alpn_protocols_orig)
self.set_lb_status(self.lb_id)
pool_path = self.POOL_PATH.format(pool_id=pool['pool']['id'])
get_pool = self.get(pool_path).json['pool']
self.assertEqual(alpn_protocols_orig, get_pool.get('alpn_protocols'))
req_dict = {'alpn_protocols': [
lib_constants.ALPN_PROTOCOL_HTTP_1_1, 'invalid-proto']}
res = self.put(self.POOLS_PATH, self._build_body(req_dict), status=400)
fault = res.json['faultstring']
self.assertIn(
'Invalid input for field/attribute alpn_protocols', fault)
self.assertIn('Value should be a valid ALPN protocol ID', fault)
self.assert_correct_status(lb_id=self.lb_id)

View File

@ -186,7 +186,8 @@ class AllRepositoriesTest(base.OctaviaDBTestBase):
'tags': ['test_tag'],
'tls_certificate_id': uuidutils.generate_uuid(),
'tls_enabled': False, 'tls_ciphers': None,
'tls_versions': None}
'tls_versions': None,
'alpn_protocols': None}
pool_dm = self.repos.create_pool_on_load_balancer(
self.session, pool, listener_id=self.listener.id)
pool_dm_dict = pool_dm.to_dict()
@ -220,7 +221,8 @@ class AllRepositoriesTest(base.OctaviaDBTestBase):
'tls_certificate_id': uuidutils.generate_uuid(),
'tls_enabled': False,
'tls_ciphers': None,
'tls_versions': None}
'tls_versions': None,
'alpn_protocols': None}
sp = {'type': constants.SESSION_PERSISTENCE_HTTP_COOKIE,
'cookie_name': 'cookie_monster',
'pool_id': pool['id'],
@ -265,7 +267,8 @@ class AllRepositoriesTest(base.OctaviaDBTestBase):
'provisioning_status': constants.ACTIVE,
'tags': ['test_tag'], 'tls_enabled': False,
'tls_ciphers': None,
'tls_versions': None}
'tls_versions': None,
'alpn_protocols': None}
pool_dm = self.repos.create_pool_on_load_balancer(
self.session, pool, listener_id=self.listener.id)
update_pool = {'protocol': constants.PROTOCOL_TCP, 'name': 'up_pool'}
@ -301,7 +304,8 @@ class AllRepositoriesTest(base.OctaviaDBTestBase):
'tags': ['test_tag'],
'tls_certificate_id': uuidutils.generate_uuid(),
'tls_enabled': False, 'tls_ciphers': None,
'tls_versions': None}
'tls_versions': None,
'alpn_protocols': None}
sp = {'type': constants.SESSION_PERSISTENCE_HTTP_COOKIE,
'cookie_name': 'cookie_monster',
'pool_id': pool['id'],
@ -406,7 +410,8 @@ class AllRepositoriesTest(base.OctaviaDBTestBase):
'id': uuidutils.generate_uuid(),
'provisioning_status': constants.ACTIVE,
'tls_enabled': False, 'tls_ciphers': None,
'tls_versions': None}
'tls_versions': None,
'alpn_protocols': None}
pool_dm = self.repos.create_pool_on_load_balancer(
self.session, pool, listener_id=self.listener.id)
update_pool = {'tls_certificate_id': uuidutils.generate_uuid()}

View File

@ -204,7 +204,8 @@ class TestAmphoraDriver(base.TestRpc):
def test_pool_create(self, mock_cast):
provider_pool = driver_dm.Pool(
pool_id=self.sample_data.pool1_id,
lb_algorithm=consts.LB_ALGORITHM_ROUND_ROBIN)
lb_algorithm=consts.LB_ALGORITHM_ROUND_ROBIN,
alpn_protocols=consts.AMPHORA_SUPPORTED_ALPN_PROTOCOLS)
self.amp_driver.pool_create(provider_pool)
payload = {consts.POOL_ID: self.sample_data.pool1_id}
mock_cast.assert_called_with({}, 'create_pool', **payload)
@ -220,6 +221,16 @@ class TestAmphoraDriver(base.TestRpc):
provider_pool)
mock_cast.assert_not_called()
@mock.patch('oslo_messaging.RPCClient.cast')
def test_pool_create_unsupported_alpn(self, mock_cast):
provider_pool = driver_dm.Pool(pool_id=self.sample_data.pool1_id)
provider_pool.alpn_protocols = ['http/1.1', 'eureka']
self.assertRaises(
exceptions.UnsupportedOptionError,
self.amp_driver.pool_create,
provider_pool)
mock_cast.assert_not_called()
@mock.patch('oslo_messaging.RPCClient.cast')
def test_pool_delete(self, mock_cast):
provider_pool = driver_dm.Pool(
@ -269,6 +280,18 @@ class TestAmphoraDriver(base.TestRpc):
provider_pool)
mock_cast.assert_not_called()
@mock.patch('oslo_messaging.RPCClient.cast')
def test_pool_update_unsupported_alpn(self, mock_cast):
old_provider_pool = driver_dm.Pool(pool_id=self.sample_data.pool1_id)
provider_pool = driver_dm.Pool(
listener_id=self.sample_data.pool1_id,
alpn_protocols=['http/1.1', 'eureka'])
self.assertRaises(
exceptions.UnsupportedOptionError,
self.amp_driver.pool_update,
old_provider_pool,
provider_pool)
# Member
@mock.patch('octavia.db.api.get_session')
@mock.patch('octavia.db.repositories.PoolRepository.get')

View File

@ -206,9 +206,10 @@ class TestAmphoraDriver(base.TestRpc):
def test_pool_create(self, mock_cast):
provider_pool = driver_dm.Pool(
pool_id=self.sample_data.pool1_id,
lb_algorithm=consts.LB_ALGORITHM_ROUND_ROBIN)
lb_algorithm=consts.LB_ALGORITHM_ROUND_ROBIN,
alpn_protocols=consts.AMPHORA_SUPPORTED_ALPN_PROTOCOLS)
self.amp_driver.pool_create(provider_pool)
payload = {consts.POOL: provider_pool.to_dict()}
payload = {consts.POOL: provider_pool.to_dict(recurse=True)}
mock_cast.assert_called_with({}, 'create_pool', **payload)
@mock.patch('oslo_messaging.RPCClient.cast')
@ -222,6 +223,16 @@ class TestAmphoraDriver(base.TestRpc):
provider_pool)
mock_cast.assert_not_called()
@mock.patch('oslo_messaging.RPCClient.cast')
def test_pool_create_unsupported_alpn(self, mock_cast):
provider_pool = driver_dm.Pool(pool_id=self.sample_data.pool1_id)
provider_pool.alpn_protocols = ['http/1.1', 'eureka']
self.assertRaises(
exceptions.UnsupportedOptionError,
self.amp_driver.pool_create,
provider_pool)
mock_cast.assert_not_called()
@mock.patch('oslo_messaging.RPCClient.cast')
def test_pool_delete(self, mock_cast):
provider_pool = driver_dm.Pool(
@ -271,6 +282,18 @@ class TestAmphoraDriver(base.TestRpc):
provider_pool)
mock_cast.assert_not_called()
@mock.patch('oslo_messaging.RPCClient.cast')
def test_pool_update_unsupported_alpn(self, mock_cast):
old_provider_pool = driver_dm.Pool(pool_id=self.sample_data.pool1_id)
provider_pool = driver_dm.Pool(
listener_id=self.sample_data.pool1_id,
alpn_protocols=['http/1.1', 'eureka'])
self.assertRaises(
exceptions.UnsupportedOptionError,
self.amp_driver.pool_update,
old_provider_pool,
provider_pool)
# Member
@mock.patch('octavia.db.api.get_session')
@mock.patch('octavia.db.repositories.PoolRepository.get')

View File

@ -145,6 +145,13 @@ class TestPoolPOST(base.BaseTypesTest):
self.assertRaises(exc.InvalidInput, wsme_json.fromjson, self._type,
body)
def test_invalid_alpn_protocols(self):
body = {"protocol": constants.PROTOCOL_HTTP,
"loadbalancer_id": uuidutils.generate_uuid(),
"alpn_protocols": ["bad", "boy"]}
self.assertRaises(exc.InvalidInput, wsme_json.fromjson, self._type,
body)
class TestPoolPUT(base.BaseTypesTest):
@ -188,6 +195,11 @@ class TestPoolPUT(base.BaseTypesTest):
self.assertRaises(exc.InvalidInput, wsme_json.fromjson, self._type,
body)
def test_invalid_alpn_protocols(self):
body = {"alpn_protocols": ["bad", "boy"]}
self.assertRaises(exc.InvalidInput, wsme_json.fromjson, self._type,
body)
class TestSessionPersistencePOST(base.BaseTypesTest, TestSessionPersistence):

View File

@ -1100,14 +1100,15 @@ class TestHaproxyCfg(base.TestCase):
" timeout server 50000\n"
" server sample_member_id_1 10.0.0.99:82 weight 13 "
"check inter 30s fall 3 rise 2 cookie sample_member_id_1 "
"{opts}\n"
"{opts} alpn {alpn}\n"
" server sample_member_id_2 10.0.0.98:82 weight 13 "
"check inter 30s fall 3 rise 2 cookie sample_member_id_2 "
"{opts}\n\n").format(
"{opts} alpn {alpn}\n\n").format(
maxconn=constants.HAPROXY_DEFAULT_MAXCONN,
opts="ssl crt %s verify none sni ssl_fc_sni" % cert_file_path +
" ciphers " + constants.CIPHERS_OWASP_SUITE_B +
" no-sslv3 no-tlsv10 no-tlsv11")
" no-sslv3 no-tlsv10 no-tlsv11",
alpn=",".join(constants.AMPHORA_SUPPORTED_ALPN_PROTOCOLS))
rendered_obj = self.jinja_cfg.render_loadbalancer_obj(
sample_configs_combined.sample_amphora_tuple(),
[sample_configs_combined.sample_listener_tuple(
@ -1137,13 +1138,14 @@ class TestHaproxyCfg(base.TestCase):
" timeout server 50000\n"
" server sample_member_id_1 10.0.0.99:82 weight 13 "
"check inter 30s fall 3 rise 2 cookie sample_member_id_1 "
"{opts}\n"
"{opts} alpn {alpn}\n"
" server sample_member_id_2 10.0.0.98:82 weight 13 "
"check inter 30s fall 3 rise 2 cookie sample_member_id_2 "
"{opts}\n\n").format(
"{opts} alpn {alpn}\n\n").format(
maxconn=constants.HAPROXY_DEFAULT_MAXCONN,
opts="ssl crt %s verify none sni ssl_fc_sni" % cert_file_path +
" ciphers " + constants.CIPHERS_OWASP_SUITE_B)
" ciphers " + constants.CIPHERS_OWASP_SUITE_B,
alpn=",".join(constants.AMPHORA_SUPPORTED_ALPN_PROTOCOLS))
rendered_obj = self.jinja_cfg.render_loadbalancer_obj(
sample_configs_combined.sample_amphora_tuple(),
[sample_configs_combined.sample_listener_tuple(
@ -1174,13 +1176,14 @@ class TestHaproxyCfg(base.TestCase):
" timeout server 50000\n"
" server sample_member_id_1 10.0.0.99:82 weight 13 "
"check inter 30s fall 3 rise 2 cookie sample_member_id_1 "
"{opts}\n"
"{opts} alpn {alpn}\n"
" server sample_member_id_2 10.0.0.98:82 weight 13 "
"check inter 30s fall 3 rise 2 cookie sample_member_id_2 "
"{opts}\n\n").format(
"{opts} alpn {alpn}\n\n").format(
maxconn=constants.HAPROXY_DEFAULT_MAXCONN,
opts="ssl crt %s verify none sni ssl_fc_sni" % cert_file_path +
" no-sslv3 no-tlsv10 no-tlsv11")
" no-sslv3 no-tlsv10 no-tlsv11",
alpn=",".join(constants.AMPHORA_SUPPORTED_ALPN_PROTOCOLS))
rendered_obj = self.jinja_cfg.render_loadbalancer_obj(
sample_configs_combined.sample_amphora_tuple(),
[sample_configs_combined.sample_listener_tuple(
@ -1193,7 +1196,7 @@ class TestHaproxyCfg(base.TestCase):
sample_configs_combined.sample_base_expected_config(backend=be),
rendered_obj)
def test_render_template_pool_cert_no_ciphers_or_versions(self):
def test_render_template_pool_cert_no_ciphers_or_versions_or_alpn(self):
cert_file_path = os.path.join(self.jinja_cfg.base_crt_dir,
'sample_listener_id_1', 'fake path')
be = ("backend sample_pool_id_1:sample_listener_id_1\n"
@ -1218,7 +1221,8 @@ class TestHaproxyCfg(base.TestCase):
rendered_obj = self.jinja_cfg.render_loadbalancer_obj(
sample_configs_combined.sample_amphora_tuple(),
[sample_configs_combined.sample_listener_tuple(
pool_cert=True, tls_enabled=True, backend_tls_versions=None)],
pool_cert=True, tls_enabled=True, backend_tls_versions=None,
backend_alpn_protocols=None)],
tls_certs={
'sample_pool_id_1':
{'client_cert': cert_file_path,
@ -1227,6 +1231,32 @@ class TestHaproxyCfg(base.TestCase):
sample_configs_combined.sample_base_expected_config(backend=be),
rendered_obj)
def test_render_template_pool_no_alpn(self):
be = ("backend sample_pool_id_1:sample_listener_id_1\n"
" mode http\n"
" balance roundrobin\n"
" cookie SRV insert indirect nocache\n"
" timeout check 31s\n"
" option httpchk GET /index.html HTTP/1.0\\r\\n\n"
" http-check expect rstatus 418\n"
" fullconn {maxconn}\n"
" option allbackups\n"
" timeout connect 5000\n"
" timeout server 50000\n"
" server sample_member_id_1 10.0.0.99:82 weight 13 "
"check inter 30s fall 3 rise 2 cookie sample_member_id_1\n"
" server sample_member_id_2 10.0.0.98:82 weight 13 "
"check inter 30s fall 3 rise 2 cookie sample_member_id_2"
"\n\n").format(
maxconn=constants.HAPROXY_DEFAULT_MAXCONN)
rendered_obj = self.jinja_cfg.render_loadbalancer_obj(
sample_configs_combined.sample_amphora_tuple(),
[sample_configs_combined.sample_listener_tuple(
backend_alpn_protocols=None)])
self.assertEqual(
sample_configs_combined.sample_base_expected_config(backend=be),
rendered_obj)
def test_render_template_with_full_pool_cert(self):
pool_client_cert = '/foo/cert.pem'
pool_ca_cert = '/foo/ca.pem'
@ -1244,16 +1274,17 @@ class TestHaproxyCfg(base.TestCase):
" timeout server 50000\n"
" server sample_member_id_1 10.0.0.99:82 weight 13 "
"check inter 30s fall 3 rise 2 cookie sample_member_id_1 "
"{opts}\n"
"{opts} alpn {alpn}\n"
" server sample_member_id_2 10.0.0.98:82 weight 13 "
"check inter 30s fall 3 rise 2 cookie sample_member_id_2 "
"{opts}\n\n").format(
"{opts} alpn {alpn}\n\n").format(
maxconn=constants.HAPROXY_DEFAULT_MAXCONN,
opts="%s %s %s %s %s %s" % (
"ssl", "crt", pool_client_cert,
"ca-file %s" % pool_ca_cert,
"crl-file %s" % pool_crl,
"verify required sni ssl_fc_sni no-sslv3 no-tlsv10 no-tlsv11"))
"verify required sni ssl_fc_sni no-sslv3 no-tlsv10 no-tlsv11"),
alpn=",".join(constants.AMPHORA_SUPPORTED_ALPN_PROTOCOLS))
rendered_obj = self.jinja_cfg.render_loadbalancer_obj(
sample_configs_combined.sample_amphora_tuple(),
[sample_configs_combined.sample_listener_tuple(

View File

@ -607,7 +607,9 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True,
alpn_protocols=constants.
AMPHORA_SUPPORTED_ALPN_PROTOCOLS,
sample_default_pool=1,
pool_enabled=True):
pool_enabled=True,
backend_alpn_protocols=constants.
AMPHORA_SUPPORTED_ALPN_PROTOCOLS):
proto = 'HTTP' if proto is None else proto
if be_proto is None:
be_proto = 'HTTP' if proto == 'TERMINATED_HTTPS' else proto
@ -645,7 +647,8 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True,
listener_id='sample_listener_id_1',
tls_ciphers=backend_tls_ciphers,
tls_versions=backend_tls_versions,
enabled=pool_enabled),
enabled=pool_enabled,
alpn_protocols=backend_alpn_protocols),
sample_pool_tuple(
proto=be_proto, monitor=monitor, persistence=persistence,
persistence_type=persistence_type,
@ -657,7 +660,8 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True,
listener_id='sample_listener_id_1',
tls_ciphers=backend_tls_ciphers,
tls_versions=None,
enabled=pool_enabled)]
enabled=pool_enabled,
alpn_protocols=backend_alpn_protocols)]
l7policies = [
sample_l7policy_tuple('sample_l7policy_id_1', sample_policy=1),
sample_l7policy_tuple('sample_l7policy_id_2', sample_policy=2),
@ -683,7 +687,8 @@ def sample_listener_tuple(proto=None, monitor=True, alloc_default_pool=True,
listener_id='sample_listener_id_1',
tls_ciphers=backend_tls_ciphers,
tls_versions=backend_tls_versions,
enabled=pool_enabled)]
enabled=pool_enabled,
alpn_protocols=backend_alpn_protocols)]
l7policies = []
listener = in_listener(
id=id,
@ -799,18 +804,21 @@ def sample_pool_tuple(listener_id=None, proto=None, monitor=True,
tls_ciphers=constants.CIPHERS_OWASP_SUITE_B,
tls_versions=constants.TLS_VERSIONS_OWASP_SUITE_B,
lb_algorithm=constants.LB_ALGORITHM_ROUND_ROBIN,
enabled=True):
enabled=True,
alpn_protocols=constants.
AMPHORA_SUPPORTED_ALPN_PROTOCOLS):
proto = 'HTTP' if proto is None else proto
if not tls_enabled:
tls_ciphers = None
tls_versions = None
alpn_protocols = None
monitor_proto = proto if monitor_proto is None else monitor_proto
in_pool = collections.namedtuple(
'pool', 'id, protocol, lb_algorithm, members, health_monitor, '
'session_persistence, enabled, operating_status, '
'tls_certificate_id, ca_tls_certificate_id, '
'crl_container_id, tls_enabled, tls_ciphers, '
'tls_versions, provisioning_status, ' +
'tls_versions, provisioning_status, alpn_protocols, ' +
constants.HTTP_REUSE)
if (proto == constants.PROTOCOL_UDP and
persistence_type == constants.SESSION_PERSISTENCE_SOURCE_IP):
@ -867,7 +875,8 @@ def sample_pool_tuple(listener_id=None, proto=None, monitor=True,
tls_enabled=tls_enabled,
tls_ciphers=tls_ciphers,
tls_versions=tls_versions,
provisioning_status=provisioning_status)
provisioning_status=provisioning_status,
alpn_protocols=alpn_protocols)
def sample_member_tuple(id, ip, enabled=True, operating_status='ACTIVE',

View File

@ -0,0 +1,16 @@
---
features:
- |
Added support for TLS extension Application Layer Protocol Negotiation
(ALPN) to TLS-enabled pools. A new parameter ``alpn_protocols`` was added
to the Pool API.
- |
Octavia provider drivers can now be extended to support HTTP/2 between
TLS-enabled pools and members.
- |
Added HTTP/2 over TLS support via ALPN protocol negotiation to the
amphora provider driver for TLS-enabled pools.
- |
The Octavia amphora driver now supports gRPC protocol when HTTP/2 is
enabled for TERMINATED_HTTPS listeners and TLS-enabled pools, and the
amphora image is using HAProxy 2.0 or newer.

View File

@ -41,7 +41,7 @@ castellan>=0.16.0 # Apache-2.0
tenacity>=5.0.4 # Apache-2.0
distro>=1.2.0 # Apache-2.0
jsonschema>=3.2.0 # MIT
octavia-lib>=2.2.0 # Apache-2.0
octavia-lib>=2.3.0 # Apache-2.0
netaddr>=0.7.19 # BSD
simplejson>=3.13.2 # MIT
setproctitle>=1.1.10 # BSD