Implement custom header support for Octavia
Implements support for custom header insertion in Octavia. A listener may be configured to insert custom headers which are supported by Octavia. Currently implemented support for X-Forwarded-For header, and X-Forwarded-Port Change-Id: I784f4939225c3acef362fcb5df57e77dbfb0f774
This commit is contained in:
parent
f629671974
commit
2194758bcf
@ -518,6 +518,9 @@ Listeners
|
||||
+---------------------+------------+-------------------------------------+
|
||||
| provisioning_status | String | Physical status of a listener |
|
||||
+---------------------+------------+-------------------------------------+
|
||||
| insert_headers | Dictionary | Dictionary of additional headers \ |
|
||||
| | | insertion into HTTP header |
|
||||
+---------------------+------------+-------------------------------------+
|
||||
|
||||
List Listeners
|
||||
**************
|
||||
@ -649,6 +652,8 @@ Create a listener.
|
||||
+------------------+----------+
|
||||
| enabled | no |
|
||||
+------------------+----------+
|
||||
| insert_headers | no |
|
||||
+------------------+----------+
|
||||
|
||||
**Request Example**::
|
||||
|
||||
@ -660,7 +665,8 @@ Create a listener.
|
||||
'name': 'listener_name',
|
||||
'description': 'listener_description',
|
||||
'default_pool_id': 'uuid',
|
||||
'enabled': true
|
||||
'enabled': true,
|
||||
'insert_headers': {'X-Forwarded-For': 'true', 'X-Forwarded-Port': 'true'}
|
||||
}
|
||||
|
||||
**Response Example**::
|
||||
|
@ -111,6 +111,14 @@ class ListenersController(base.BaseController):
|
||||
Update the load balancer db when provisioning status changes.
|
||||
"""
|
||||
lb_repo = self.repositories.load_balancer
|
||||
if (listener_dict
|
||||
and listener_dict.get('insert_headers')
|
||||
and list(set(listener_dict['insert_headers'].keys()) -
|
||||
set(constants.SUPPORTED_HTTP_HEADERS))):
|
||||
raise exceptions.InvalidOption(
|
||||
value=listener_dict.get('insert_headers'),
|
||||
option='insert_headers')
|
||||
|
||||
try:
|
||||
sni_containers = listener_dict.pop('sni_containers', [])
|
||||
db_listener = self.repositories.listener.create(
|
||||
|
@ -44,6 +44,7 @@ class ListenerResponse(base.BaseType):
|
||||
default_pool_id = wtypes.wsattr(wtypes.UuidType())
|
||||
default_pool = wtypes.wsattr(pool.PoolResponse)
|
||||
l7policies = wtypes.wsattr([l7policy.L7PolicyResponse])
|
||||
insert_headers = wtypes.wsattr(wtypes.DictType(str, str))
|
||||
|
||||
@classmethod
|
||||
def from_data_model(cls, data_model, children=False):
|
||||
@ -90,6 +91,7 @@ class ListenerPOST(base.BaseType):
|
||||
default_pool_id = wtypes.wsattr(wtypes.UuidType())
|
||||
default_pool = wtypes.wsattr(pool.PoolPOST)
|
||||
l7policies = wtypes.wsattr([l7policy.L7PolicyPOST], default=[])
|
||||
insert_headers = wtypes.wsattr(wtypes.DictType(str, str))
|
||||
|
||||
|
||||
class ListenerPUT(base.BaseType):
|
||||
@ -104,3 +106,4 @@ class ListenerPUT(base.BaseType):
|
||||
tls_termination = wtypes.wsattr(TLSTermination)
|
||||
sni_containers = [wtypes.StringType(max_length=255)]
|
||||
default_pool_id = wtypes.wsattr(wtypes.UuidType())
|
||||
insert_headers = wtypes.wsattr(wtypes.DictType(str, str))
|
||||
|
@ -308,5 +308,10 @@ API_VERSION = '0.5'
|
||||
HAPROXY_BASE_PEER_PORT = 1025
|
||||
KEEPALIVED_CONF = 'keepalived.conf.j2'
|
||||
CHECK_SCRIPT_CONF = 'keepalived_check_script.conf.j2'
|
||||
|
||||
PLUGGED_INTERFACES = '/var/lib/octavia/plugged_interfaces'
|
||||
AMPHORA_NAMESPACE = 'amphora-haproxy'
|
||||
|
||||
# List of HTTP headers which are supported for insertion
|
||||
SUPPORTED_HTTP_HEADERS = ['X-Forwarded-For',
|
||||
'X-Forwarded-Port']
|
||||
|
@ -263,7 +263,7 @@ class Listener(BaseDataModel):
|
||||
enabled=None, provisioning_status=None, operating_status=None,
|
||||
tls_certificate_id=None, stats=None, default_pool=None,
|
||||
load_balancer=None, sni_containers=None, peer_port=None,
|
||||
l7policies=None, pools=None):
|
||||
l7policies=None, pools=None, insert_headers=None):
|
||||
self.id = id
|
||||
self.project_id = project_id
|
||||
self.name = name
|
||||
@ -283,6 +283,7 @@ class Listener(BaseDataModel):
|
||||
self.sni_containers = sni_containers or []
|
||||
self.peer_port = peer_port
|
||||
self.l7policies = l7policies or []
|
||||
self.insert_headers = insert_headers or {}
|
||||
self.pools = pools or []
|
||||
|
||||
def update(self, update_dict):
|
||||
|
@ -167,6 +167,7 @@ class JinjaTemplater(object):
|
||||
'protocol_mode': PROTOCOL_MAP[listener.protocol],
|
||||
'protocol': listener.protocol,
|
||||
'peer_port': listener.peer_port,
|
||||
'insert_headers': listener.insert_headers,
|
||||
'topology': listener.load_balancer.topology,
|
||||
'amphorae': listener.load_balancer.amphorae,
|
||||
'enabled': listener.enabled
|
||||
|
@ -196,7 +196,14 @@ backend {{ pool.id }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if pool.protocol.lower() == constants.PROTOCOL_HTTP.lower() %}
|
||||
{% if listener.insert_headers.get('X-Forwarded-For',
|
||||
'False').lower() == 'true' %}
|
||||
option forwardfor
|
||||
{% endif %}
|
||||
{% if listener.insert_headers.get('X-Forwarded-Port',
|
||||
'False').lower() == 'true' %}
|
||||
http-request set-header X-Forwarded-Port %[dst_port]
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% for member in pool.members if member.enabled %}
|
||||
{{- member_macro(constants, pool, member) -}}
|
||||
|
@ -0,0 +1,32 @@
|
||||
# Copyright 2016 VMware
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Insert headers
|
||||
|
||||
Revision ID: 4d9cf7d32f2
|
||||
Revises: 9bf4d21caaea
|
||||
Create Date: 2016-02-21 17:16:22.316744
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '4d9cf7d32f2'
|
||||
down_revision = '9bf4d21caaea'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('listener', sa.Column('insert_headers', sa.PickleType()))
|
@ -378,7 +378,9 @@ class Listener(base_models.BASE, base_models.IdMixin,
|
||||
default_pool = orm.relationship("Pool", uselist=False,
|
||||
backref=orm.backref("_default_listeners",
|
||||
uselist=True))
|
||||
|
||||
peer_port = sa.Column(sa.Integer(), nullable=True)
|
||||
insert_headers = sa.Column(sa.PickleType())
|
||||
|
||||
# This property should be a unique list of the default_pool and anything
|
||||
# referenced by enabled L7Policies with at least one rule that also
|
||||
|
@ -87,6 +87,7 @@ class TestListener(base.BaseAPITest):
|
||||
'protocol_port': 80, 'connection_limit': 10,
|
||||
'tls_certificate_id': uuidutils.generate_uuid(),
|
||||
'sni_containers': [sni1, sni2],
|
||||
'insert_headers': {},
|
||||
'project_id': uuidutils.generate_uuid()}
|
||||
lb_listener.update(optionals)
|
||||
response = self.post(self.listeners_path, lb_listener)
|
||||
@ -169,7 +170,8 @@ class TestListener(base.BaseAPITest):
|
||||
defaults = {'name': None, 'default_pool_id': None,
|
||||
'description': None, 'enabled': True,
|
||||
'connection_limit': None, 'tls_certificate_id': None,
|
||||
'sni_containers': [], 'project_id': None}
|
||||
'sni_containers': [], 'project_id': None,
|
||||
'insert_headers': {}}
|
||||
lb_listener = {'protocol': constants.PROTOCOL_HTTP,
|
||||
'protocol_port': 80}
|
||||
response = self.post(self.listeners_path, lb_listener)
|
||||
@ -395,3 +397,16 @@ class TestListener(base.BaseAPITest):
|
||||
self.assertIsNone(listener.get('tls_termination'))
|
||||
get_listener = self.get(listener_path).json
|
||||
self.assertIsNone(get_listener.get('tls_termination'))
|
||||
|
||||
def test_create_with_valid_insert_headers(self):
|
||||
lb_listener = {'protocol': 'HTTP',
|
||||
'protocol_port': 80,
|
||||
'insert_headers': {'X-Forwarded-For': 'true'}}
|
||||
self.post(self.listeners_path, lb_listener, status=202)
|
||||
|
||||
def test_create_with_bad_insert_headers(self):
|
||||
lb_listener = {'protocol': 'HTTP',
|
||||
'protocol_port': 80,
|
||||
# 'insert_headers': {'x': 'x'}}
|
||||
'insert_headers': {'X-Forwarded-Four': 'true'}}
|
||||
self.post(self.listeners_path, lb_listener, status=400)
|
||||
|
@ -320,7 +320,8 @@ class TestLoadBalancerGraph(base.BaseAPITest):
|
||||
'connection_limit': None,
|
||||
'enabled': True,
|
||||
'provisioning_status': constants.PENDING_CREATE,
|
||||
'operating_status': constants.OFFLINE
|
||||
'operating_status': constants.OFFLINE,
|
||||
'insert_headers': {}
|
||||
}
|
||||
if create_sni_containers:
|
||||
create_listener['sni_containers'] = create_sni_containers
|
||||
|
@ -47,7 +47,6 @@ class TestHaproxyCfg(base.TestCase):
|
||||
" timeout check 31\n"
|
||||
" option httpchk GET /index.html\n"
|
||||
" http-check expect rstatus 418\n"
|
||||
" option forwardfor\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"
|
||||
@ -83,7 +82,6 @@ class TestHaproxyCfg(base.TestCase):
|
||||
" timeout check 31\n"
|
||||
" option httpchk GET /index.html\n"
|
||||
" http-check expect rstatus 418\n"
|
||||
" option forwardfor\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"
|
||||
@ -110,7 +108,6 @@ class TestHaproxyCfg(base.TestCase):
|
||||
" timeout check 31\n"
|
||||
" option httpchk GET /index.html\n"
|
||||
" http-check expect rstatus 418\n"
|
||||
" option forwardfor\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"
|
||||
@ -154,7 +151,6 @@ class TestHaproxyCfg(base.TestCase):
|
||||
" mode http\n"
|
||||
" balance roundrobin\n"
|
||||
" cookie SRV insert indirect nocache\n"
|
||||
" option forwardfor\n"
|
||||
" server sample_member_id_1 10.0.0.99:82 weight 13 "
|
||||
"cookie sample_member_id_1\n"
|
||||
" server sample_member_id_2 10.0.0.98:82 weight 13 "
|
||||
@ -206,7 +202,6 @@ class TestHaproxyCfg(base.TestCase):
|
||||
be = ("backend sample_pool_id_1\n"
|
||||
" mode http\n"
|
||||
" balance roundrobin\n"
|
||||
" option forwardfor\n"
|
||||
" server sample_member_id_1 10.0.0.99:82 weight 13\n"
|
||||
" server sample_member_id_2 10.0.0.98:82 weight 13\n\n")
|
||||
rendered_obj = self.jinja_cfg.render_loadbalancer_obj(
|
||||
@ -224,7 +219,6 @@ class TestHaproxyCfg(base.TestCase):
|
||||
" timeout check 31\n"
|
||||
" option httpchk GET /index.html\n"
|
||||
" http-check expect rstatus 418\n"
|
||||
" option forwardfor\n"
|
||||
" server sample_member_id_1 10.0.0.99:82 "
|
||||
"weight 13 check inter 30s fall 3 rise 2\n"
|
||||
" server sample_member_id_2 10.0.0.98:82 "
|
||||
@ -246,7 +240,6 @@ class TestHaproxyCfg(base.TestCase):
|
||||
" timeout check 31\n"
|
||||
" option httpchk GET /index.html\n"
|
||||
" http-check expect rstatus 418\n"
|
||||
" option forwardfor\n"
|
||||
" server sample_member_id_1 10.0.0.99:82 "
|
||||
"weight 13 check inter 30s fall 3 rise 2\n"
|
||||
" server sample_member_id_2 10.0.0.98:82 "
|
||||
@ -286,7 +279,6 @@ class TestHaproxyCfg(base.TestCase):
|
||||
" timeout check 31\n"
|
||||
" option httpchk GET /index.html\n"
|
||||
" http-check expect rstatus 418\n"
|
||||
" option forwardfor\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 "
|
||||
@ -299,7 +291,6 @@ class TestHaproxyCfg(base.TestCase):
|
||||
" timeout check 31\n"
|
||||
" option httpchk GET /healthmon.html\n"
|
||||
" http-check expect rstatus 418\n"
|
||||
" option forwardfor\n"
|
||||
" server sample_member_id_3 10.0.0.97:82 weight 13 check "
|
||||
"inter 30s fall 3 rise 2 cookie sample_member_id_3\n\n")
|
||||
rendered_obj = self.jinja_cfg.render_loadbalancer_obj(
|
||||
@ -307,6 +298,52 @@ class TestHaproxyCfg(base.TestCase):
|
||||
self.assertEqual(sample_configs.sample_base_expected_config(
|
||||
frontend=fe, backend=be), rendered_obj)
|
||||
|
||||
def test_render_template_http_xff(self):
|
||||
be = ("backend sample_pool_id_1\n"
|
||||
" mode http\n"
|
||||
" balance roundrobin\n"
|
||||
" cookie SRV insert indirect nocache\n"
|
||||
" timeout check 31\n"
|
||||
" option httpchk GET /index.html\n"
|
||||
" http-check expect rstatus 418\n"
|
||||
" option forwardfor\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")
|
||||
rendered_obj = self.jinja_cfg.render_loadbalancer_obj(
|
||||
sample_configs.sample_listener_tuple(
|
||||
insert_headers={'X-Forwarded-For': 'true'}))
|
||||
self.assertEqual(
|
||||
sample_configs.sample_base_expected_config(backend=be),
|
||||
rendered_obj)
|
||||
|
||||
def test_render_template_http_xff_xfport(self):
|
||||
be = ("backend sample_pool_id_1\n"
|
||||
" mode http\n"
|
||||
" balance roundrobin\n"
|
||||
" cookie SRV insert indirect nocache\n"
|
||||
" timeout check 31\n"
|
||||
" option httpchk GET /index.html\n"
|
||||
" http-check expect rstatus 418\n"
|
||||
" option forwardfor\n"
|
||||
" http-request set-header X-Forwarded-Port %[dst_port]\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")
|
||||
rendered_obj = self.jinja_cfg.render_loadbalancer_obj(
|
||||
sample_configs.sample_listener_tuple(
|
||||
insert_headers={'X-Forwarded-For': 'true',
|
||||
'X-Forwarded-Port': 'true'}))
|
||||
self.assertEqual(
|
||||
sample_configs.sample_base_expected_config(backend=be),
|
||||
rendered_obj)
|
||||
|
||||
def test_transform_session_persistence(self):
|
||||
in_persistence = sample_configs.sample_session_persistence_tuple()
|
||||
ret = self.jinja_cfg._transform_session_persistence(in_persistence)
|
||||
|
@ -204,7 +204,8 @@ RET_LISTENER = {
|
||||
'topology': 'SINGLE',
|
||||
'pools': [RET_POOL_1],
|
||||
'l7policies': [],
|
||||
'enabled': True}
|
||||
'enabled': True,
|
||||
'insert_headers': {}}
|
||||
|
||||
RET_LISTENER_L7 = {
|
||||
'id': 'sample_listener_id_1',
|
||||
@ -219,7 +220,8 @@ RET_LISTENER_L7 = {
|
||||
'pools': [RET_POOL_1, RET_POOL_2],
|
||||
'l7policies': [RET_L7POLICY_1, RET_L7POLICY_2, RET_L7POLICY_3,
|
||||
RET_L7POLICY_4, RET_L7POLICY_5],
|
||||
'enabled': True}
|
||||
'enabled': True,
|
||||
'insert_headers': {}}
|
||||
|
||||
RET_LISTENER_TLS = {
|
||||
'id': 'sample_listener_id_1',
|
||||
@ -233,7 +235,8 @@ RET_LISTENER_TLS = {
|
||||
'default_tls_container': RET_DEF_TLS_CONT,
|
||||
'pools': [RET_POOL_1],
|
||||
'l7policies': [],
|
||||
'enabled': True}
|
||||
'enabled': True,
|
||||
'insert_headers': {}}
|
||||
|
||||
RET_LISTENER_TLS_SNI = {
|
||||
'id': 'sample_listener_id_1',
|
||||
@ -250,7 +253,8 @@ RET_LISTENER_TLS_SNI = {
|
||||
'sni_containers': [RET_SNI_CONT_1, RET_SNI_CONT_2],
|
||||
'pools': [RET_POOL_1],
|
||||
'l7policies': [],
|
||||
'enabled': True}
|
||||
'enabled': True,
|
||||
'insert_headers': {}}
|
||||
|
||||
RET_LB = {
|
||||
'name': 'test-lb',
|
||||
@ -345,18 +349,19 @@ def sample_vip_tuple():
|
||||
def sample_listener_tuple(proto=None, monitor=True, persistence=True,
|
||||
persistence_type=None, persistence_cookie=None,
|
||||
tls=False, sni=False, peer_port=None, topology=None,
|
||||
l7=False, enabled=True):
|
||||
l7=False, enabled=True, insert_headers=None):
|
||||
proto = 'HTTP' if proto is None else proto
|
||||
be_proto = 'HTTP' if proto is 'TERMINATED_HTTPS' else proto
|
||||
topology = 'SINGLE' if topology is None else topology
|
||||
port = '443' if proto is 'HTTPS' or proto is 'TERMINATED_HTTPS' else '80'
|
||||
peer_port = 1024 if peer_port is None else peer_port
|
||||
insert_headers = insert_headers or {}
|
||||
in_listener = collections.namedtuple(
|
||||
'listener', 'id, project_id, protocol_port, protocol, default_pool, '
|
||||
'connection_limit, tls_certificate_id, '
|
||||
'sni_container_ids, default_tls_container, '
|
||||
'sni_containers, load_balancer, peer_port, pools, '
|
||||
'l7policies, enabled',)
|
||||
'l7policies, enabled, insert_headers',)
|
||||
if l7:
|
||||
pools = [
|
||||
sample_pool_tuple(
|
||||
@ -419,7 +424,8 @@ def sample_listener_tuple(proto=None, monitor=True, persistence=True,
|
||||
if sni else [],
|
||||
pools=pools,
|
||||
l7policies=l7policies,
|
||||
enabled=enabled
|
||||
enabled=enabled,
|
||||
insert_headers=insert_headers
|
||||
)
|
||||
|
||||
|
||||
@ -615,7 +621,6 @@ def sample_base_expected_config(frontend=None, backend=None, peers=None):
|
||||
" timeout check 31\n"
|
||||
" option httpchk GET /index.html\n"
|
||||
" http-check expect rstatus 418\n"
|
||||
" option forwardfor\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 "
|
||||
|
Loading…
Reference in New Issue
Block a user