Add proxy v2 protocol support

This patch adds support for the proxy protocol v2 on pools.

Depends-On: https://review.opendev.org/747296
Change-Id: Ic112c5e71ee9b6433b307fdf27059f217ba4136e
Story: 2005611
Task: 30858
This commit is contained in:
Michael Johnson 2020-08-24 14:49:06 -07:00
parent 46de66b240
commit 7fe78c5943
17 changed files with 231 additions and 90 deletions

View File

@ -1236,7 +1236,7 @@ protocol:
protocol-pools:
description: |
The protocol for the resource. One of ``HTTP``, ``HTTPS``, ``PROXY``,
``TCP``, or ``UDP``.
``PROXYV2``, ``TCP``, or ``UDP``.
in: body
required: true
type: string

View File

@ -102,7 +102,8 @@ is ready for further configuration.
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``, ``TCP``, or ``UDP``.
listen. A valid value is ``HTTP``, ``HTTPS``, ``PROXY``, ``PROXYV2``,
``TCP``, or ``UDP``.
- ``lb_algorithm`` The load-balancer algorithm, such as
``ROUND_ROBIN``, ``LEAST_CONNECTIONS``, ``SOURCE_IP`` and ``SOURCE_IP_PORT``,

View File

@ -114,6 +114,14 @@ cli=openstack loadbalancer pool create --protocol PROXY --listener <listener>
driver.amphora=complete
driver.ovn=missing
[operation.protocol.PROXYV2]
title=protocol - PROXYV2
status=optional
notes=PROXY protocol version 2 support for the pool.
cli=openstack loadbalancer pool create --protocol PROXYV2 --listener <listener>
driver.amphora=complete
driver.ovn=missing
[operation.protocol.TCP]
title=protocol - TCP
status=optional

View File

@ -128,6 +128,9 @@ class RootController(object):
self._add_a_version(versions, 'v2.20', 'v2', 'SUPPORTED',
'2020-08-02T00:00:00Z', host_url)
# Amphora delete
self._add_a_version(versions, 'v2.21', 'v2', 'CURRENT',
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',
'2020-09-04T00:00:00Z', host_url)
return {'versions': versions}

View File

@ -12,6 +12,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from octavia_lib.common import constants as lib_constants
from wsme import types as wtypes
from octavia.api.common import types
@ -146,7 +147,8 @@ class PoolPOST(BasePoolType):
admin_state_up = wtypes.wsattr(bool, default=True)
listener_id = wtypes.wsattr(wtypes.UuidType())
loadbalancer_id = wtypes.wsattr(wtypes.UuidType())
protocol = wtypes.wsattr(wtypes.Enum(str, *constants.SUPPORTED_PROTOCOLS),
protocol = wtypes.wsattr(
wtypes.Enum(str, *lib_constants.POOL_SUPPORTED_PROTOCOLS),
mandatory=True)
lb_algorithm = wtypes.wsattr(
wtypes.Enum(str, *constants.SUPPORTED_LB_ALGORITHMS),
@ -198,7 +200,8 @@ class PoolSingleCreate(BasePoolType):
name = wtypes.wsattr(wtypes.StringType(max_length=255))
description = wtypes.wsattr(wtypes.StringType(max_length=255))
admin_state_up = wtypes.wsattr(bool, default=True)
protocol = wtypes.wsattr(wtypes.Enum(str, *constants.SUPPORTED_PROTOCOLS))
protocol = wtypes.wsattr(
wtypes.Enum(str, *lib_constants.POOL_SUPPORTED_PROTOCOLS))
lb_algorithm = wtypes.wsattr(
wtypes.Enum(str, *constants.SUPPORTED_LB_ALGORITHMS))
session_persistence = wtypes.wsattr(SessionPersistencePOST)

View File

@ -207,10 +207,13 @@ UPDATE_HEALTH = 'UPDATE_HEALTH'
VALID_LISTENER_POOL_PROTOCOL_MAP = {
PROTOCOL_TCP: [PROTOCOL_HTTP, PROTOCOL_HTTPS,
PROTOCOL_PROXY, PROTOCOL_TCP],
PROTOCOL_HTTP: [PROTOCOL_HTTP, PROTOCOL_PROXY],
PROTOCOL_HTTPS: [PROTOCOL_HTTPS, PROTOCOL_PROXY, PROTOCOL_TCP],
PROTOCOL_TERMINATED_HTTPS: [PROTOCOL_HTTP, PROTOCOL_PROXY],
PROTOCOL_PROXY, lib_consts.PROTOCOL_PROXYV2, PROTOCOL_TCP],
PROTOCOL_HTTP: [PROTOCOL_HTTP, PROTOCOL_PROXY,
lib_consts.PROTOCOL_PROXYV2],
PROTOCOL_HTTPS: [PROTOCOL_HTTPS, PROTOCOL_PROXY,
lib_consts.PROTOCOL_PROXYV2, PROTOCOL_TCP],
PROTOCOL_TERMINATED_HTTPS: [PROTOCOL_HTTP, PROTOCOL_PROXY,
lib_consts.PROTOCOL_PROXYV2],
PROTOCOL_UDP: [PROTOCOL_UDP]}
# API Integer Ranges
@ -810,6 +813,7 @@ L4_PROTOCOL_MAP = {
PROTOCOL_HTTPS: PROTOCOL_TCP,
PROTOCOL_TERMINATED_HTTPS: PROTOCOL_TCP,
PROTOCOL_PROXY: PROTOCOL_TCP,
lib_consts.PROTOCOL_PROXYV2: PROTOCOL_TCP,
PROTOCOL_UDP: PROTOCOL_UDP,
}

View File

@ -284,7 +284,9 @@ class JinjaTemplater(object):
os.path.join(self.base_crt_dir, loadbalancer.id,
tls_certs[listener.client_crl_container_id]))
tls_enabled = False
if listener.protocol == constants.PROTOCOL_TERMINATED_HTTPS:
tls_enabled = True
if listener.tls_ciphers is not None:
ret_value['tls_ciphers'] = listener.tls_ciphers
if listener.tls_versions is not None:
@ -300,7 +302,7 @@ class JinjaTemplater(object):
if tls_certs is not None and tls_certs.get(pool.id):
kwargs = {'pool_tls_certs': tls_certs.get(pool.id)}
pools.append(self._transform_pool(
pool, feature_compatibility, **kwargs))
pool, feature_compatibility, tls_enabled, **kwargs))
ret_value['pools'] = pools
policy_gen = (policy for policy in listener.l7policies if
policy.provisioning_status != constants.PENDING_DELETE)
@ -311,21 +313,27 @@ class JinjaTemplater(object):
break
l7policies = [self._transform_l7policy(
x, feature_compatibility, tls_certs)
x, feature_compatibility, tls_enabled, tls_certs)
for x in policy_gen]
ret_value['l7policies'] = l7policies
return ret_value
def _transform_pool(self, pool, feature_compatibility,
pool_tls_certs=None):
listener_tls_enabled, pool_tls_certs=None):
"""Transforms a pool into an object that will
be processed by the templating system
"""
proxy_protocol_version = None
if pool.protocol == constants.PROTOCOL_PROXY:
proxy_protocol_version = 1
if pool.protocol == lib_consts.PROTOCOL_PROXYV2:
proxy_protocol_version = 2
ret_value = {
'id': pool.id,
'protocol': PROTOCOL_MAP[pool.protocol],
'proxy_protocol': pool.protocol == constants.PROTOCOL_PROXY,
'proxy_protocol': proxy_protocol_version,
'listener_tls_enabled': listener_tls_enabled,
'lb_algorithm': BALANCE_MAP.get(pool.lb_algorithm, 'roundrobin'),
'members': [],
'health_monitor': '',
@ -425,7 +433,7 @@ class JinjaTemplater(object):
}
def _transform_l7policy(self, l7policy, feature_compatibility,
tls_certs=None):
listener_tls_enabled, tls_certs=None):
"""Transforms an L7 policy into an object that will
be processed by the templating system
@ -446,7 +454,8 @@ class JinjaTemplater(object):
kwargs = {'pool_tls_certs':
tls_certs.get(l7policy.redirect_pool.id)}
ret_value['redirect_pool'] = self._transform_pool(
l7policy.redirect_pool, feature_compatibility, **kwargs)
l7policy.redirect_pool, feature_compatibility,
listener_tls_enabled, **kwargs)
else:
ret_value['redirect_pool'] = None
if (l7policy.action in [constants.L7POLICY_ACTION_REDIRECT_TO_URL,

View File

@ -217,8 +217,14 @@ frontend {{ listener.id }}
{% else %}
{% set persistence_opt = "" %}
{% endif %}
{% if pool.proxy_protocol %}
{% if pool.proxy_protocol == 1 %}
{% set proxy_protocol_opt = " send-proxy" %}
{% elif pool.proxy_protocol == 2 %}
{% if pool.listener_tls_enabled %}
{% set proxy_protocol_opt = " send-proxy-v2-ssl-cn" %}
{% else %}
{% set proxy_protocol_opt = " send-proxy-v2" %}
{% endif %}
{% else %}
{% set proxy_protocol_opt = "" %}
{% endif %}
@ -288,7 +294,7 @@ frontend {{ listener.id }}
{% macro backend_macro(constants, lib_consts, listener, pool, loadbalancer) %}
backend {{ pool.id }}:{{ listener.id }}
{% if pool.protocol.lower() == constants.PROTOCOL_PROXY.lower() %}
{% if pool.proxy_protocol is not none %}
mode {{ listener.protocol_mode }}
{% else %}
mode {{ pool.protocol }}

View File

@ -0,0 +1,43 @@
# 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 PROXY v2 pool protocol
Revision ID: e6ee84f0abf3
Revises: 2ab994dd3ec2
Create Date: 2020-08-24 11:12:46.745185
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy import sql
# revision identifiers, used by Alembic.
revision = 'e6ee84f0abf3'
down_revision = '2ab994dd3ec2'
def upgrade():
insert_table = sql.table(
u'protocol',
sql.column(u'name', sa.String),
sql.column(u'description', sa.String)
)
op.bulk_insert(
insert_table,
[
{'name': 'PROXYV2'}
]
)

View File

@ -11,6 +11,7 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from octavia_lib.common import constants as lib_constants
from octavia.common import constants
@ -280,4 +281,5 @@ INVALID_LISTENER_POOL_PROTOCOL_MAP = {
constants.PROTOCOL_HTTP,
constants.PROTOCOL_HTTPS,
constants.PROTOCOL_TERMINATED_HTTPS,
constants.PROTOCOL_PROXY]}
constants.PROTOCOL_PROXY,
lib_constants.PROTOCOL_PROXYV2]}

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

@ -1303,6 +1303,12 @@ class TestL7Policy(base.BaseAPITest):
self.set_object_status(self.lb_repo, self.lb_id)
port = port + 1
for pool_proto in invalid_map[listener_proto]:
if pool_proto == constants.PROTOCOL_TERMINATED_HTTPS:
pool = self.create_pool(
self.lb_id, pool_proto,
constants.LB_ALGORITHM_ROUND_ROBIN, status=400)
self.assertIn("Invalid input", pool['faultstring'])
else:
pool = self.create_pool(
self.lb_id, pool_proto,
constants.LB_ALGORITHM_ROUND_ROBIN).get('pool')
@ -1310,10 +1316,10 @@ class TestL7Policy(base.BaseAPITest):
l7policy['listener_id'] = listener.get('id')
l7policy['redirect_pool_id'] = pool.get('id')
expect_error_msg = ("Validation failure: The pool protocol "
"'%s' is invalid while the listener "
"protocol is '%s'.") % (pool_proto,
listener_proto)
expect_error_msg = (
"Validation failure: The pool protocol '%s' is "
"invalid while the listener protocol is '%s'.") % (
pool_proto, listener_proto)
res = self.post(self.L7POLICIES_PATH,
self._build_body(l7policy), status=400)
self.assertEqual(expect_error_msg, res.json['faultstring'])
@ -1361,6 +1367,12 @@ class TestL7Policy(base.BaseAPITest):
self.set_object_status(self.lb_repo, self.lb_id)
port = port + 1
for pool_proto in invalid_map[listener_proto]:
if pool_proto == constants.PROTOCOL_TERMINATED_HTTPS:
pool = self.create_pool(
self.lb_id, pool_proto,
constants.LB_ALGORITHM_ROUND_ROBIN, status=400)
self.assertIn("Invalid input", pool['faultstring'])
else:
pool = self.create_pool(
self.lb_id, pool_proto,
constants.LB_ALGORITHM_ROUND_ROBIN).get('pool')
@ -1370,10 +1382,10 @@ class TestL7Policy(base.BaseAPITest):
constants.L7POLICY_ACTION_REJECT).get(self.root_tag)
self.set_object_status(self.lb_repo, self.lb_id)
new_l7policy['redirect_pool_id'] = pool.get('id')
expect_error_msg = ("Validation failure: The pool protocol "
"'%s' is invalid while the listener "
"protocol is '%s'.") % (pool_proto,
listener_proto)
expect_error_msg = (
"Validation failure: The pool protocol '%s' is "
"invalid while the listener protocol is '%s'.") % (
pool_proto, listener_proto)
res = self.put(self.L7POLICY_PATH.format(
l7policy_id=l7policy.get('id')),
self._build_body(new_l7policy), status=400)

View File

@ -2849,14 +2849,20 @@ class TestListener(base.BaseAPITest):
for listener_proto in invalid_map:
for pool_proto in invalid_map[listener_proto]:
port = port + 1
if pool_proto == constants.PROTOCOL_TERMINATED_HTTPS:
pool = self.create_pool(
self.lb_id, pool_proto,
constants.LB_ALGORITHM_ROUND_ROBIN, status=400)
self.assertIn("Invalid input", pool['faultstring'])
else:
pool = self.create_pool(
self.lb_id, pool_proto,
constants.LB_ALGORITHM_ROUND_ROBIN).get('pool')
self.set_object_status(self.lb_repo, self.lb_id)
expect_error_msg = ("Validation failure: The pool protocol "
"'%s' is invalid while the listener "
"protocol is '%s'.") % (pool_proto,
listener_proto)
expect_error_msg = (
"Validation failure: The pool protocol '%s' is "
"invalid while the listener protocol is '%s'.") % (
pool_proto, listener_proto)
listener = {'protocol': listener_proto,
'protocol_port': port,
'loadbalancer_id': self.lb_id,
@ -2907,13 +2913,20 @@ class TestListener(base.BaseAPITest):
"'%s' is invalid while the listener "
"protocol is '%s'.") % (pool_proto,
listener_proto)
if pool_proto == constants.PROTOCOL_TERMINATED_HTTPS:
pool = self.create_pool(
self.lb_id, pool_proto,
constants.LB_ALGORITHM_ROUND_ROBIN, status=400)
self.assertIn("Invalid input", pool['faultstring'])
else:
pool = self.create_pool(
self.lb_id, pool_proto,
constants.LB_ALGORITHM_ROUND_ROBIN).get('pool')
self.set_object_status(self.lb_repo, self.lb_id)
new_listener = {'default_pool_id': pool.get('id')}
res = self.put(
self.LISTENER_PATH.format(listener_id=listener.get('id')),
self.LISTENER_PATH.format(
listener_id=listener.get('id')),
self._build_body(new_listener), status=400)
self.assertEqual(expect_error_msg, res.json['faultstring'])
self.assert_correct_status(lb_id=self.lb_id)

View File

@ -14,6 +14,7 @@
from unittest import mock
from octavia_lib.common import constants as lib_constants
from oslo_config import cfg
from oslo_config import fixture as oslo_fixture
from oslo_utils import uuidutils
@ -765,6 +766,30 @@ class TestPool(base.BaseAPITest):
lb_id=self.lb_id, listener_id=self.listener_id,
pool_id=api_pool.get('id'))
def test_create_with_proxy_v2_protocol(self):
api_pool = self.create_pool(
self.lb_id,
lib_constants.PROTOCOL_PROXYV2,
constants.LB_ALGORITHM_ROUND_ROBIN,
listener_id=self.listener_id).get(self.root_tag)
self.assert_correct_status(
lb_id=self.lb_id, listener_id=self.listener_id,
pool_id=api_pool.get('id'),
lb_prov_status=constants.PENDING_UPDATE,
listener_prov_status=constants.PENDING_UPDATE,
pool_prov_status=constants.PENDING_CREATE,
pool_op_status=constants.OFFLINE)
self.set_lb_status(self.lb_id)
self.assertEqual(lib_constants.PROTOCOL_PROXYV2,
api_pool.get('protocol'))
self.assertEqual(constants.LB_ALGORITHM_ROUND_ROBIN,
api_pool.get('lb_algorithm'))
self.assertIsNotNone(api_pool.get('created_at'))
self.assertIsNone(api_pool.get('updated_at'))
self.assert_correct_status(
lb_id=self.lb_id, listener_id=self.listener_id,
pool_id=api_pool.get('id'))
def test_create_sans_listener(self):
api_pool = self.create_pool(
self.lb_id,
@ -2382,5 +2407,10 @@ class TestPool(base.BaseAPITest):
lb_pool['listener_id'] = listener.get('id')
res = self.post(self.POOLS_PATH, self._build_body(lb_pool),
status=400, expect_errors=True)
self.assertEqual(expect_error_msg, res.json['faultstring'])
if pool_proto == constants.PROTOCOL_TERMINATED_HTTPS:
self.assertIn('Invalid input',
res.json['faultstring'])
else:
self.assertEqual(expect_error_msg,
res.json['faultstring'])
self.assert_correct_status(lb_id=self.lb_id)

View File

@ -1288,18 +1288,18 @@ class TestHaproxyCfg(base.TestCase):
def test_transform_pool(self):
in_pool = sample_configs_combined.sample_pool_tuple()
ret = self.jinja_cfg._transform_pool(in_pool, {})
ret = self.jinja_cfg._transform_pool(in_pool, {}, False)
self.assertEqual(sample_configs_combined.RET_POOL_1, ret)
def test_transform_pool_2(self):
in_pool = sample_configs_combined.sample_pool_tuple(sample_pool=2)
ret = self.jinja_cfg._transform_pool(in_pool, {})
ret = self.jinja_cfg._transform_pool(in_pool, {}, False)
self.assertEqual(sample_configs_combined.RET_POOL_2, ret)
def test_transform_pool_http_reuse(self):
in_pool = sample_configs_combined.sample_pool_tuple(sample_pool=2)
ret = self.jinja_cfg._transform_pool(
in_pool, {constants.HTTP_REUSE: True})
in_pool, {constants.HTTP_REUSE: True}, False)
expected_config = copy.copy(sample_configs_combined.RET_POOL_2)
expected_config[constants.HTTP_REUSE] = True
self.assertEqual(expected_config, ret)
@ -1309,7 +1309,7 @@ class TestHaproxyCfg(base.TestCase):
cert_path = os.path.join(self.jinja_cfg.base_crt_dir,
'test_listener_id', 'pool_cert.pem')
ret = self.jinja_cfg._transform_pool(
in_pool, {}, pool_tls_certs={'client_cert': cert_path})
in_pool, {}, False, pool_tls_certs={'client_cert': cert_path})
expected_config = copy.copy(sample_configs_combined.RET_POOL_1)
expected_config['client_cert'] = cert_path
self.assertEqual(expected_config, ret)
@ -1383,25 +1383,25 @@ class TestHaproxyCfg(base.TestCase):
def test_transform_l7policy(self):
in_l7policy = sample_configs_combined.sample_l7policy_tuple(
'sample_l7policy_id_1')
ret = self.jinja_cfg._transform_l7policy(in_l7policy, {})
ret = self.jinja_cfg._transform_l7policy(in_l7policy, {}, False)
self.assertEqual(sample_configs_combined.RET_L7POLICY_1, ret)
def test_transform_l7policy_2_8(self):
in_l7policy = sample_configs_combined.sample_l7policy_tuple(
'sample_l7policy_id_2', sample_policy=2)
ret = self.jinja_cfg._transform_l7policy(in_l7policy, {})
ret = self.jinja_cfg._transform_l7policy(in_l7policy, {}, False)
self.assertEqual(sample_configs_combined.RET_L7POLICY_2, ret)
# test invalid action without redirect_http_code
in_l7policy = sample_configs_combined.sample_l7policy_tuple(
'sample_l7policy_id_8', sample_policy=2, redirect_http_code=None)
ret = self.jinja_cfg._transform_l7policy(in_l7policy, {})
ret = self.jinja_cfg._transform_l7policy(in_l7policy, {}, False)
self.assertEqual(sample_configs_combined.RET_L7POLICY_8, ret)
def test_transform_l7policy_disabled_rule(self):
in_l7policy = sample_configs_combined.sample_l7policy_tuple(
'sample_l7policy_id_6', sample_policy=6)
ret = self.jinja_cfg._transform_l7policy(in_l7policy, {})
ret = self.jinja_cfg._transform_l7policy(in_l7policy, {}, False)
self.assertEqual(sample_configs_combined.RET_L7POLICY_6, ret)
def test_escape_haproxy_config_string(self):

View File

@ -117,8 +117,9 @@ RET_MEMBER_3 = {
RET_POOL_1 = {
'id': 'sample_pool_id_1',
'protocol': 'http',
'proxy_protocol': False,
'proxy_protocol': None,
'lb_algorithm': 'roundrobin',
'listener_tls_enabled': False,
'members': [RET_MEMBER_1, RET_MEMBER_2],
'health_monitor': RET_MONITOR_1,
'session_persistence': RET_PERSISTENCE,
@ -134,8 +135,9 @@ RET_POOL_1 = {
RET_POOL_2 = {
'id': 'sample_pool_id_2',
'protocol': 'http',
'proxy_protocol': False,
'proxy_protocol': None,
'lb_algorithm': 'roundrobin',
'listener_tls_enabled': False,
'members': [RET_MEMBER_3],
'health_monitor': RET_MONITOR_2,
'session_persistence': RET_PERSISTENCE,

View File

@ -0,0 +1,4 @@
---
features:
- |
Added support for proxy protocol version 2.