Jinja Haproxy templates

Adds Jinja templating for haproxy configuration
Adds tests to verify Jinja templating

Change-Id: I7dd71bec3d4993ffb732dcd316b801498329fd2a
Partially-Implements: bp/haproxy-amphora-driver
This commit is contained in:
ptoohill 2014-12-29 13:33:20 -06:00 committed by Susanne Balle
parent 10e3fb0f69
commit 847a25042e
16 changed files with 1064 additions and 0 deletions

View File

@ -0,0 +1,245 @@
# Copyright (c) 2015 Rackspace
#
# 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.
import os
import jinja2
import six
from octavia.common import constants
PROTOCOL_MAP = {
constants.PROTOCOL_TCP: 'tcp',
constants.PROTOCOL_HTTP: 'http',
constants.PROTOCOL_HTTPS: 'tcp',
constants.PROTOCOL_TERMINATED_HTTPS: 'http'
}
BALANCE_MAP = {
constants.LB_ALGORITHM_ROUND_ROBIN: 'roundrobin',
constants.LB_ALGORITHM_LEAST_CONNECTIONS: 'leastconn',
constants.LB_ALGORITHM_SOURCE_IP: 'source'
}
ACTIVE_PENDING_STATUSES = constants.SUPPORTED_PROVISIONING_STATUSES + (
constants.DEGRADED,)
BASE_PATH = '/var/lib/octavia'
BASE_CRT_DIR = '/listeners'
HAPROXY_TEMPLATE = os.path.abspath(
os.path.join(os.path.dirname(__file__),
'templates/haproxy_listener.template'))
JINJA_ENV = None
class JinjaTemplater(object):
def __init__(self, base_amp_path=None,
base_crt_dir=None,
haproxy_template=None):
""":param base_amp_path: Base path for amphora data
:param base_crt_dir: Base directory for certificate storage
:param haproxy_template: Absolute path to the Jinja template for
HaProxy configuration generation
"""
self.base_amp_path = base_amp_path if base_amp_path else BASE_PATH
self.base_crt_dir = base_crt_dir if base_crt_dir else BASE_CRT_DIR
self.haproxy_template = (haproxy_template if haproxy_template
else HAPROXY_TEMPLATE)
self.cert_store_path = '{0}{1}'.format(self.base_amp_path,
self.base_crt_dir)
def build_config(self, listener, tls_cert,
socket_path=None,
user_group='nogroup'):
"""Convert a logical configuration to the HAProxy version."""
return self.render_loadbalancer_obj(listener,
tls_cert=tls_cert,
user_group=user_group,
socket_path=socket_path)
def _get_template(self):
"""Returns the specified Jinja configuration template."""
global JINJA_ENV
if not JINJA_ENV:
template_loader = jinja2.FileSystemLoader(
searchpath=os.path.dirname(self.haproxy_template))
JINJA_ENV = jinja2.Environment(
loader=template_loader,
trim_blocks=True,
lstrip_blocks=True)
return JINJA_ENV.get_template(os.path.basename(self.haproxy_template))
def render_loadbalancer_obj(self, listener,
tls_cert=None,
user_group='nogroup',
socket_path=None):
"""Renders a templated configuration from a load balancer object."""
loadbalancer = self._transform_loadbalancer(
listener.loadbalancer,
listener,
tls_cert)
if not socket_path:
socket_path = '%s/%s.sock' % (self.base_amp_path, listener.id)
return self._get_template().render(
{'loadbalancer': loadbalancer,
'user_group': user_group,
'stats_sock': socket_path},
constants=constants)
def _transform_loadbalancer(self, loadbalancer, listener, tls_cert):
"""Transforms a load balanacer into an object that will
be processed by the templating system
"""
listener = self._transform_listener(listener, tls_cert)
return {
'name': loadbalancer.name,
'vip_address': loadbalancer.vip.ip_address,
'listener': listener
}
def _transform_listener(self, listener, tls_cert):
"""Transforms a listener into an object that will
be processed by the templating system
"""
ret_value = {
'id': listener.id,
'protocol_port': listener.protocol_port,
'protocol_mode': PROTOCOL_MAP[listener.protocol],
'protocol': listener.protocol
}
if listener.connection_limit and listener.connection_limit > -1:
ret_value['connection_limit'] = listener.connection_limit
if listener.tls_container_id:
ret_value['default_tls_path'] = '%s/%s/%s.pem' % (
self.cert_store_path, listener.id, tls_cert.primary_cn)
if listener.sni_containers:
ret_value['crt_dir'] = '%s/%s' % (
self.cert_store_path, listener.id)
if listener.default_pool:
ret_value['default_pool'] = self._transform_pool(
listener.default_pool)
return ret_value
def _transform_pool(self, pool):
"""Transforms a pool into an object that will
be processed by the templating system
"""
ret_value = {
'id': pool.id,
'protocol': PROTOCOL_MAP[pool.protocol],
'lb_algorithm': BALANCE_MAP.get(pool.lb_algorithm, 'roundrobin'),
'members': [],
'health_monitor': '',
'session_persistence': '',
'enabled': pool.enabled,
'operating_status': pool.operating_status
}
members = [self._transform_member(x)
for x in pool.members if self._include_member(x)]
ret_value['members'] = members
if pool.healthmonitor:
ret_value['health_monitor'] = self._transform_health_monitor(
pool.healthmonitor)
if pool.sessionpersistence:
ret_value[
'session_persistence'] = self._transform_session_persistence(
pool.sessionpersistence)
return ret_value
def _transform_session_persistence(self, persistence):
"""Transforms session persistence into an object that will
be processed by the templating system
"""
return {
'type': persistence.type,
'cookie_name': persistence.cookie_name
}
def _transform_member(self, member):
"""Transforms a member into an object that will
be processed by the templating system
"""
return {
'id': member.id,
'address': member.ip_address,
'protocol_port': member.protocol_port,
'weight': member.weight,
'enabled': member.enabled,
'subnet_id': member.subnet_id,
'operating_status': member.operating_status
}
def _transform_health_monitor(self, monitor):
"""Transforms a health monitor into an object that will
be processed by the templating system
"""
return {
'id': monitor.id,
'type': monitor.type,
'delay': monitor.delay,
'timeout': monitor.timeout,
'fall_threshold': monitor.fall_threshold,
'http_method': monitor.http_method,
'url_path': monitor.url_path,
'expected_codes': '|'.join(
self._expand_expected_codes(monitor.expected_codes)),
'enabled': monitor.enabled,
}
def _include_member(self, member):
"""Members that should be included
Return only those that meet the criteria for templating
"""
return (
member.operating_status in
ACTIVE_PENDING_STATUSES and
member.enabled
)
def _expand_expected_codes(self, codes):
"""Expand the expected code string in set of codes.
200-204 -> 200, 201, 202, 204
200, 203 -> 200, 203
"""
retval = set()
for code in codes.replace(',', ' ').split(' '):
code = code.strip()
if not code:
continue
elif '-' in code:
low, hi = code.split('-')[:2]
retval.update(
str(i) for i in six.moves.xrange(int(low), int(hi) + 1))
else:
retval.add(code)
return retval

View File

@ -0,0 +1,33 @@
{# # Copyright (c) 2015 Rackspace
#
# 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.
#
#}
# Configuration for {{ loadbalancer_name }}
global
daemon
user nobody
group {{ usergroup }}
log /dev/log local0
log /dev/log local1 notice
stats socket {{ sock_path }} mode 0666 level user
defaults
log global
retries 3
option redispatch
timeout connect 5000
timeout client 50000
timeout server 50000
{% block proxies %}{% endblock proxies %}

View File

@ -0,0 +1,27 @@
{# # Copyright (c) 2015 Rackspace
#
# 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.
#
#}
{% extends 'haproxy_proxies.template' %}
{% set loadbalancer_name = loadbalancer.name %}
{% set usergroup = user_group %}
{% set sock_path = stats_sock %}
{% block proxies %}
{% from 'haproxy_proxies.template' import frontend_macro as frontend_macro, backend_macro%}
{{ frontend_macro(constants, loadbalancer.listener, loadbalancer.vip_address) }}
{% if loadbalancer.listener.default_pool %}
{{ backend_macro(constants, loadbalancer.listener, loadbalancer.listener.default_pool) }}
{% endif %}
{% endblock proxies %}

View File

@ -0,0 +1,29 @@
{# # Copyright (c) 2015 Rackspace
#
# 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.
#
#}
{% extends 'haproxy_proxies.template' %}
{% set loadbalancer_name = loadbalancer.name %}
{% set usergroup = user_group %}
{% set sock_path = stats_sock %}
{% block proxies %}
{% from 'haproxy_proxies.template' import frontend_macro as frontend_macro, backend_macro%}
{% for listener in loadbalancer.listeners %}
{{ frontend_macro(constants, listener, loadbalancer.vip_address) }}
{% if listener.default_pool %}
{{ backend_macro(constants, listener, listener.default_pool) }}
{% endif %}
{% endfor %}
{% endblock proxies %}

View File

@ -0,0 +1,95 @@
{# # Copyright (c) 2015 Rackspace
#
# 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.
#
#}
{% extends 'haproxy_base.template' %}
{% macro bind_macro(constants, listener, lb_vip_address) %}
{% if listener.default_tls_path %}
{% set def_crt_opt = "ssl crt %s"|format(listener.default_tls_path)|trim() %}
{% else %}
{% set def_crt_opt = "" %}
{% endif %}
{% if listener.crt_dir %}
{% set crt_dir_opt = "crt %s"|format(listener.crt_dir)|trim() %}
{% else %}
{% set crt_dir_opt = "" %}
{% endif %}
bind {{ lb_vip_address }}:{{ listener.protocol_port }} {{ "%s %s"|format(def_crt_opt, crt_dir_opt)|trim() }}
{% endmacro %}
{% macro use_backend_macro(listener) %}
{% if listener.default_pool %}
default_backend {{ listener.default_pool.id }}
{% endif %}
{% endmacro %}
{% macro frontend_macro(constants, listener, lb_vip_address) %}
frontend {{ listener.id }}
option tcplog
{% if listener.connection_limit is defined %}
maxconn {{ listener.connection_limit }}
{% endif %}
{% if listener.protocol_mode == constants.PROTOCOL_HTTP.lower() %}
option forwardfor
{% endif %}
{{ bind_macro(constants, listener, lb_vip_address)|trim() }}
mode {{ listener.protocol_mode }}
{% if listener.default_pool %}
default_backend {{ listener.default_pool.id }}
{% endif %}
{% endmacro %}
{% macro backend_macro(constants, listener, pool) %}
backend {{ pool.id }}
mode {{ pool.protocol }}
balance {{ pool.lb_algorithm }}
{% if listener.protocol == constants.PROTOCOL_TERMINATED_HTTPS %}
redirect scheme https if !{ ssl_fc }
{% endif %}
{% if pool.session_persistence %}
{% if pool.session_persistence.type == constants.SESSION_PERSISTENCE_SOURCE_IP %}
stick-table type ip size 10k
stick on src
{% elif pool.session_persistence.type == constants.SESSION_PERSISTENCE_HTTP_COOKIE %}
cookie SRV insert indirect nocache
{% endif %}
{% endif %}
{% if pool.health_monitor %}
timeout check {{ pool.health_monitor.timeout }}
{% if pool.health_monitor.type == constants.HEALTH_MONITOR_HTTP or pool.health_monitor.type == constants.HEALTH_MONITOR_HTTPS %}
option httpchk {{ pool.health_monitor.http_method }} {{ pool.health_monitor.url_path }}
http-check expect rstatus {{ pool.health_monitor.expected_codes }}
{% endif %}
{% if pool.health_monitor.type == constants.HEALTH_MONITOR_HTTPS %}
option ssl-hello-chk
{% endif %}
{% endif %}
{% if listener.protocol_mode == constants.PROTOCOL_HTTP.lower() %}
option forwardfor
{% endif %}
{% for member in pool.members %}
{% if pool.health_monitor %}
{% set hm_opt = " check inter %ds fall %d"|format(pool.health_monitor.delay, pool.health_monitor.fall_threshold) %}
{% else %}
{% set hm_opt = "" %}
{% endif %}
{%if pool.session_persistence.type == constants.SESSION_PERSISTENCE_HTTP_COOKIE %}
{% set persistence_opt = " cookie %s"|format(member.id) %}
{% else %}
{% set persistence_opt = "" %}
{% endif %}
{{ "server %s %s:%d weight %s%s%s"|e|format(member.id, member.address, member.protocol_port, member.weight, hm_opt, persistence_opt)|trim() }}
{% endfor %}
{% endmacro %}

View File

@ -34,6 +34,7 @@ SUPPORTED_HEALTH_MONITOR_TYPES = (HEALTH_MONITOR_HTTP, HEALTH_MONITOR_HTTPS,
PROTOCOL_TCP = 'TCP'
PROTOCOL_HTTP = 'HTTP'
PROTOCOL_HTTPS = 'HTTPS'
PROTOCOL_TERMINATED_HTTPS = 'TERMINATED_HTTPS'
SUPPORTED_PROTOCOLS = (PROTOCOL_TCP, PROTOCOL_HTTPS, PROTOCOL_HTTP)
ACTIVE = 'ACTIVE'

View File

@ -190,6 +190,18 @@ class SNI(BaseDataModel):
self.tls_container_id = tls_container_id
class TLSContainer(BaseDataModel):
def __init__(self, id=None, primary_cn=None, certificate=None,
private_key=None, passphrase=None, intermediates=[]):
self.id = id
self.primary_cn = primary_cn
self.certificate = certificate
self.private_key = private_key
self.passphrase = passphrase
self.intermediates = intermediates
class Amphora(BaseDataModel):
def __init__(self, id=None, load_balancer_id=None, compute_id=None,

View File

@ -0,0 +1,312 @@
# Copyright 2014 OpenStack Foundation
# 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.
from octavia.amphorae.drivers.haproxy.jinja import jinja_cfg
from octavia.tests.unit import base as base
from octavia.tests.unit.common.sample_configs import sample_configs
class TestHaproxyCfg(base.TestCase):
def setUp(self):
super(TestHaproxyCfg, self).setUp()
self.jinja_cfg = jinja_cfg.JinjaTemplater(
base_amp_path='/var/lib/octavia',
base_crt_dir='/listeners')
def test_get_template(self):
template = self.jinja_cfg._get_template()
self.assertEqual('haproxy_listener.template', template.name)
def test_render_template_tls(self):
fe = ("frontend sample_listener_id_1\n"
" option tcplog\n"
" maxconn 98\n"
" option forwardfor\n"
" bind 10.0.0.2:443 "
"ssl crt /var/lib/octavia/listeners/"
"sample_listener_id_1/FakeCN.pem "
"crt /var/lib/octavia/listeners/sample_listener_id_1\n"
" mode http\n"
" default_backend sample_pool_id_1\n\n")
be = ("backend sample_pool_id_1\n"
" mode http\n"
" balance roundrobin\n"
" redirect scheme https if !{ ssl_fc }\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 cookie sample_member_id_1\n"
" server sample_member_id_2 10.0.0.98:82 "
"weight 13 check inter 30s fall 3 cookie sample_member_id_2\n\n")
tls_tupe = sample_configs.sample_tls_container_tuple(
certificate='imaCert1', private_key='imaPrivateKey1',
primary_cn='FakeCN')
rendered_obj = self.jinja_cfg.render_loadbalancer_obj(
sample_configs.sample_listener_tuple(proto='TERMINATED_HTTPS',
tls=True, sni=True),
tls_tupe)
self.assertEqual(
sample_configs.sample_base_expected_config(
frontend=fe, backend=be),
rendered_obj)
def test_render_template_tls_no_sni(self):
fe = ("frontend sample_listener_id_1\n"
" option tcplog\n"
" maxconn 98\n"
" option forwardfor\n"
" bind 10.0.0.2:443 "
"ssl crt /var/lib/octavia/listeners/"
"sample_listener_id_1/FakeCN.pem\n"
" mode http\n"
" default_backend sample_pool_id_1\n\n")
be = ("backend sample_pool_id_1\n"
" mode http\n"
" balance roundrobin\n"
" redirect scheme https if !{ ssl_fc }\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 cookie sample_member_id_1\n"
" server sample_member_id_2 10.0.0.98:82 "
"weight 13 check inter 30s fall 3 cookie sample_member_id_2\n\n")
rendered_obj = self.jinja_cfg.render_loadbalancer_obj(
sample_configs.sample_listener_tuple(
proto='TERMINATED_HTTPS', tls=True),
tls_cert=sample_configs.sample_tls_container_tuple(
certificate='ImAalsdkfjCert',
private_key='ImAsdlfksdjPrivateKey',
primary_cn="FakeCN"))
self.assertEqual(
sample_configs.sample_base_expected_config(
frontend=fe, backend=be),
rendered_obj)
def test_render_template_http(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 cookie sample_member_id_1\n"
" server sample_member_id_2 10.0.0.98:82 "
"weight 13 check inter 30s fall 3 cookie sample_member_id_2\n\n")
rendered_obj = self.jinja_cfg.render_loadbalancer_obj(
sample_configs.sample_listener_tuple())
self.assertEqual(
sample_configs.sample_base_expected_config(backend=be),
rendered_obj)
def test_render_template_https(self):
fe = ("frontend sample_listener_id_1\n"
" option tcplog\n"
" maxconn 98\n"
" bind 10.0.0.2:443\n"
" mode tcp\n"
" default_backend sample_pool_id_1\n\n")
be = ("backend sample_pool_id_1\n"
" mode tcp\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 ssl-hello-chk\n"
" server sample_member_id_1 10.0.0.99:82 "
"weight 13 check inter 30s fall 3 cookie sample_member_id_1\n"
" server sample_member_id_2 10.0.0.98:82 "
"weight 13 check inter 30s fall 3 cookie sample_member_id_2\n\n")
rendered_obj = self.jinja_cfg.render_loadbalancer_obj(
sample_configs.sample_listener_tuple(proto='HTTPS'))
self.assertEqual(sample_configs.sample_base_expected_config(
frontend=fe, backend=be), rendered_obj)
def test_render_template_no_monitor_http(self):
be = ("backend sample_pool_id_1\n"
" 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 "
"cookie sample_member_id_2\n\n")
rendered_obj = self.jinja_cfg.render_loadbalancer_obj(
sample_configs.sample_listener_tuple(proto='HTTP', monitor=False))
self.assertEqual(sample_configs.sample_base_expected_config(
backend=be), rendered_obj)
def test_render_template_no_monitor_https(self):
fe = ("frontend sample_listener_id_1\n"
" option tcplog\n"
" maxconn 98\n"
" bind 10.0.0.2:443\n"
" mode tcp\n"
" default_backend sample_pool_id_1\n\n")
be = ("backend sample_pool_id_1\n"
" mode tcp\n"
" balance roundrobin\n"
" cookie SRV insert indirect nocache\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 "
"cookie sample_member_id_2\n\n")
rendered_obj = self.jinja_cfg.render_loadbalancer_obj(
sample_configs.sample_listener_tuple(proto='HTTPS', monitor=False))
self.assertEqual(sample_configs.sample_base_expected_config(
frontend=fe, backend=be), rendered_obj)
def test_render_template_no_persistence_https(self):
fe = ("frontend sample_listener_id_1\n"
" option tcplog\n"
" maxconn 98\n"
" bind 10.0.0.2:443\n"
" mode tcp\n"
" default_backend sample_pool_id_1\n\n")
be = ("backend sample_pool_id_1\n"
" mode tcp\n"
" balance roundrobin\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(
sample_configs.sample_listener_tuple(proto='HTTPS', monitor=False,
persistence=False))
self.assertEqual(sample_configs.sample_base_expected_config(
frontend=fe, backend=be), rendered_obj)
def test_render_template_no_persistence_http(self):
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(
sample_configs.sample_listener_tuple(proto='HTTP', monitor=False,
persistence=False))
self.assertEqual(sample_configs.sample_base_expected_config(
backend=be), rendered_obj)
def test_render_template_sourceip_persistence(self):
be = ("backend sample_pool_id_1\n"
" mode http\n"
" balance roundrobin\n"
" stick-table type ip size 10k\n"
" stick on src\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\n"
" server sample_member_id_2 10.0.0.98:82 "
"weight 13 check inter 30s fall 3\n\n")
rendered_obj = self.jinja_cfg.render_loadbalancer_obj(
sample_configs.sample_listener_tuple(
persistence_type='SOURCE_IP'))
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)
self.assertEqual(sample_configs.RET_PERSISTENCE, ret)
def test_transform_health_monitor(self):
in_persistence = sample_configs.sample_health_monitor_tuple()
ret = self.jinja_cfg._transform_health_monitor(in_persistence)
self.assertEqual(sample_configs.RET_MONITOR, ret)
def test_transform_member(self):
in_member = sample_configs.sample_member_tuple('sample_member_id_1',
'10.0.0.99')
ret = self.jinja_cfg._transform_member(in_member)
self.assertEqual(sample_configs.RET_MEMBER_1, ret)
def test_transform_pool(self):
in_pool = sample_configs.sample_pool_tuple()
ret = self.jinja_cfg._transform_pool(in_pool)
self.assertEqual(sample_configs.RET_POOL, ret)
def test_transform_listener(self):
in_listener = sample_configs.sample_listener_tuple()
ret = self.jinja_cfg._transform_listener(in_listener, None)
self.assertEqual(sample_configs.RET_LISTENER, ret)
def test_transform_loadbalancer(self):
in_listener = sample_configs.sample_listener_tuple()
ret = self.jinja_cfg._transform_loadbalancer(
in_listener.loadbalancer, in_listener, None)
self.assertEqual(sample_configs.RET_LB, ret)
def test_include_member(self):
ret = self.jinja_cfg._include_member(
sample_configs.sample_member_tuple('sample_member_id_1',
'10.0.0.99'))
self.assertTrue(ret)
def test_include_member_invalid_status(self):
ret = self.jinja_cfg._include_member(
sample_configs.sample_member_tuple('sample_member_id_1',
'10.0.0.99',
operating_status='PENDING'))
self.assertFalse(ret)
def test_include_member_invalid_admin_state(self):
ret = self.jinja_cfg._include_member(
sample_configs.sample_member_tuple('sample_member_id_1',
'10.0.0.99',
enabled=False))
self.assertFalse(ret)
def test_expand_expected_codes(self):
exp_codes = ''
self.assertEqual(self.jinja_cfg._expand_expected_codes(exp_codes),
set([]))
exp_codes = '200'
self.assertEqual(
self.jinja_cfg._expand_expected_codes(exp_codes), set(['200']))
exp_codes = '200, 201'
self.assertEqual(self.jinja_cfg._expand_expected_codes(exp_codes),
set(['200', '201']))
exp_codes = '200, 201,202'
self.assertEqual(self.jinja_cfg._expand_expected_codes(exp_codes),
set(['200', '201', '202']))
exp_codes = '200-202'
self.assertEqual(self.jinja_cfg._expand_expected_codes(exp_codes),
set(['200', '201', '202']))
exp_codes = '200-202, 205'
self.assertEqual(self.jinja_cfg._expand_expected_codes(exp_codes),
set(['200', '201', '202', '205']))
exp_codes = '200, 201-203'
self.assertEqual(self.jinja_cfg._expand_expected_codes(exp_codes),
set(['200', '201', '202', '203']))
exp_codes = '200, 201-203, 205'
self.assertEqual(self.jinja_cfg._expand_expected_codes(exp_codes),
set(['200', '201', '202', '203', '205']))
exp_codes = '201-200, 205'
self.assertEqual(
self.jinja_cfg._expand_expected_codes(exp_codes), set(['205']))

View File

@ -0,0 +1,308 @@
# Copyright 2014 OpenStack Foundation
#
# 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.
#
import collections
RET_PERSISTENCE = {
'type': 'HTTP_COOKIE',
'cookie_name': 'HTTP_COOKIE'}
RET_MONITOR = {
'id': 'sample_monitor_id_1',
'type': 'HTTP',
'delay': 30,
'timeout': 31,
'fall_threshold': 3,
'http_method': 'GET',
'url_path': '/index.html',
'expected_codes': '418',
'enabled': True}
RET_MEMBER_1 = {
'id': 'sample_member_id_1',
'address': '10.0.0.99',
'protocol_port': 82,
'weight': 13,
'subnet_id': '10.0.0.1/24',
'enabled': True,
'operating_status': 'ACTIVE'}
RET_MEMBER_2 = {
'id': 'sample_member_id_2',
'address': '10.0.0.98',
'protocol_port': 82,
'weight': 13,
'subnet_id': '10.0.0.1/24',
'enabled': True,
'operating_status': 'ACTIVE'}
RET_POOL = {
'id': 'sample_pool_id_1',
'protocol': 'http',
'lb_algorithm': 'roundrobin',
'members': [RET_MEMBER_1, RET_MEMBER_2],
'health_monitor': RET_MONITOR,
'session_persistence': RET_PERSISTENCE,
'enabled': True,
'operating_status': 'ACTIVE'}
RET_DEF_TLS_CONT = {'id': 'cont_id_1', 'allencompassingpem': 'imapem',
'primary_cn': 'FakeCn'}
RET_SNI_CONT_1 = {'id': 'cont_id_2', 'allencompassingpem': 'imapem2',
'primary_cn': 'FakeCn'}
RET_SNI_CONT_2 = {'id': 'cont_id_3', 'allencompassingpem': 'imapem3',
'primary_cn': 'FakeCn2'}
RET_LISTENER = {
'id': 'sample_listener_id_1',
'protocol_port': '80',
'protocol': 'HTTP',
'protocol_mode': 'http',
'default_pool': RET_POOL,
'connection_limit': 98}
RET_LISTENER_TLS = {
'id': 'sample_listener_id_1',
'protocol_port': '443',
'protocol': 'TERMINATED_HTTPS',
'protocol_mode': 'http',
'default_pool': RET_POOL,
'connection_limit': 98,
'tls_container_id': 'cont_id_1',
'default_tls_path': '/etc/ssl/sample_loadbalancer_id_1/fakeCN.pem',
'default_tls_container': RET_DEF_TLS_CONT}
RET_LISTENER_TLS_SNI = {
'id': 'sample_listener_id_1',
'protocol_port': '443',
'protocol': 'http',
'protocol': 'TERMINATED_HTTPS',
'default_pool': RET_POOL,
'connection_limit': 98,
'tls_container_id': 'cont_id_1',
'default_tls_path': '/etc/ssl/sample_loadbalancer_id_1/fakeCN.pem',
'default_tls_container': RET_DEF_TLS_CONT,
'crt_dir': '/v2/sample_loadbalancer_id_1',
'sni_container_ids': ['cont_id_2', 'cont_id_3'],
'sni_containers': [RET_SNI_CONT_1, RET_SNI_CONT_2]}
RET_LB = {
'name': 'test-lb',
'vip_address': '10.0.0.2',
'listener': RET_LISTENER}
RET_LB_TLS = {
'name': 'test-lb',
'vip_address': '10.0.0.2',
'listener': RET_LISTENER_TLS}
RET_LB_TLS_SNI = {
'name': 'test-lb',
'vip_address': '10.0.0.2',
'listener': RET_LISTENER_TLS_SNI}
def sample_loadbalancer_tuple(proto=None, monitor=True, persistence=True,
persistence_type=None, tls=False, sni=False):
proto = 'HTTP' if proto is None else proto
in_lb = collections.namedtuple(
'loadbalancer', 'id, name, protocol, vip, '
'listeners')
return in_lb(
id='sample_loadbalancer_id_1',
name='test-lb',
protocol=proto,
vip=sample_vip_tuple(),
listeners=[sample_listener_tuple(proto=proto, monitor=monitor,
persistence=persistence,
persistence_type=persistence_type,
tls=tls,
sni=sni)]
)
def sample_listener_loadbalancer_tuple(proto=None):
proto = 'HTTP' if proto is None else proto
in_lb = collections.namedtuple(
'loadbalancer', 'id, name, protocol, vip')
return in_lb(
id='sample_loadbalancer_id_1',
name='test-lb',
protocol=proto,
vip=sample_vip_tuple()
)
def sample_vip_tuple():
vip = collections.namedtuple('vip', 'ip_address')
return vip(ip_address='10.0.0.2')
def sample_listener_tuple(proto=None, monitor=True, persistence=True,
persistence_type=None, tls=False, sni=False):
proto = 'HTTP' if proto is None else proto
port = '443' if proto is 'HTTPS' or proto is 'TERMINATED_HTTPS' else '80'
in_listener = collections.namedtuple(
'listener', 'id, protocol_port, protocol, default_pool, '
'connection_limit, tls_container_id, '
'sni_container_ids, default_tls_container, '
'sni_containers, loadbalancer')
return in_listener(
id='sample_listener_id_1',
protocol_port=port,
protocol=proto,
loadbalancer=sample_listener_loadbalancer_tuple(proto=proto),
default_pool=sample_pool_tuple(
proto=proto, monitor=monitor, persistence=persistence,
persistence_type=persistence_type),
connection_limit=98,
tls_container_id='cont_id_1' if tls else '',
sni_container_ids=['cont_id_2', 'cont_id_3'] if sni else [],
default_tls_container=sample_tls_container_tuple(
id='cont_id_1', certificate='--imapem1--\n',
private_key='--imakey1--\n', intermediates=[
'--imainter1--\n', '--imainter1too--\n'],
primary_cn='aFakeCN'
) if tls else '',
sni_containers=[
sample_tls_sni_container_tuple(
tls_container=sample_tls_container_tuple(
id='cont_id_2', certificate='--imapem2--\n',
private_key='--imakey2--\n', intermediates=[
'--imainter2--\n', '--imainter2too--\n'
], primary_cn='aFakeCN')),
sample_tls_sni_container_tuple(
tls_container=sample_tls_container_tuple(
id='cont_id_3', certificate='--imapem3--\n',
private_key='--imakey3--\n', intermediates=[
'--imainter3--\n', '--imainter3too--\n'
], primary_cn='aFakeCN'))]
if sni else []
)
def sample_tls_sni_container_tuple(tls_container=None):
sc = collections.namedtuple('sni_container', 'tls_container')
return sc(tls_container=tls_container)
def sample_tls_sni_containers_tuple(tls_container=None):
sc = collections.namedtuple('sni_containers', 'tls_container')
return [sc(tls_container=tls_container)]
def sample_tls_container_tuple(id='cont_id_1', certificate=None,
private_key=None, intermediates=[],
primary_cn=None):
sc = collections.namedtuple(
'tls_container',
'id, certificate, private_key, intermediates, primary_cn')
return sc(id=id, certificate=certificate, private_key=private_key,
intermediates=intermediates, primary_cn=primary_cn)
def sample_pool_tuple(proto=None, monitor=True, persistence=True,
persistence_type=None):
proto = 'HTTP' if proto is None else proto
in_pool = collections.namedtuple(
'pool', 'id, protocol, lb_algorithm, members, healthmonitor,'
'sessionpersistence, enabled, operating_status')
mon = sample_health_monitor_tuple(proto=proto) if monitor is True else None
persis = sample_session_persistence_tuple(
persistence_type=persistence_type) if persistence is True else None
return in_pool(
id='sample_pool_id_1',
protocol=proto,
lb_algorithm='ROUND_ROBIN',
members=[sample_member_tuple('sample_member_id_1', '10.0.0.99'),
sample_member_tuple('sample_member_id_2', '10.0.0.98')],
healthmonitor=mon,
sessionpersistence=persis,
enabled=True,
operating_status='ACTIVE')
def sample_member_tuple(id, ip, enabled=True, operating_status='ACTIVE'):
in_member = collections.namedtuple('member',
'id, ip_address, protocol_port, '
'weight, subnet_id, '
'enabled, operating_status')
return in_member(
id=id,
ip_address=ip,
protocol_port=82,
weight=13,
subnet_id='10.0.0.1/24',
enabled=enabled,
operating_status=operating_status)
def sample_session_persistence_tuple(persistence_type=None):
spersistence = collections.namedtuple('SessionPersistence',
'type, cookie_name')
pt = 'HTTP_COOKIE' if persistence_type is None else persistence_type
return spersistence(type=pt,
cookie_name=pt)
def sample_health_monitor_tuple(proto='HTTP'):
proto = 'HTTP' if proto is 'TERMINATED_HTTPS' else proto
monitor = collections.namedtuple(
'monitor', 'id, type, delay, timeout, fall_threshold, http_method, '
'url_path, expected_codes, enabled')
return monitor(id='sample_monitor_id_1', type=proto, delay=30,
timeout=31, fall_threshold=3, http_method='GET',
url_path='/index.html', expected_codes='418',
enabled=True)
def sample_base_expected_config(frontend=None, backend=None):
if frontend is None:
frontend = ("frontend sample_listener_id_1\n"
" option tcplog\n"
" maxconn 98\n"
" option forwardfor\n"
" bind 10.0.0.2:80\n"
" mode http\n"
" default_backend sample_pool_id_1\n\n")
if backend is None:
backend = ("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"
" server sample_member_id_1 10.0.0.99:82 weight 13 "
"check inter 30s fall 3 cookie sample_member_id_1\n"
" server sample_member_id_2 10.0.0.98:82 weight 13 "
"check inter 30s fall 3 cookie sample_member_id_2\n")
return ("# Configuration for test-lb\n"
"global\n"
" daemon\n"
" user nobody\n"
" group nogroup\n"
" log /dev/log local0\n"
" log /dev/log local1 notice\n"
" stats socket /var/lib/octavia/sample_listener_id_1.sock"
" mode 0666 level user\n\n"
"defaults\n"
" log global\n"
" retries 3\n"
" option redispatch\n"
" timeout connect 5000\n"
" timeout client 50000\n"
" timeout server 50000\n\n" + frontend + backend)

View File

@ -31,3 +31,5 @@ pyOpenSSL>=0.11
WSME>=0.6
pyasn1
pyasn1_modules
jinja2