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:
Kobi Samoray 2015-12-15 15:17:13 +02:00
parent f629671974
commit 2194758bcf
13 changed files with 144 additions and 21 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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