Commit f6954f68 authored by Jérome Perrin's avatar Jérome Perrin Committed by Levin Zimmermann

stack/erp5: use slapos.recipe.build to manage haproxy parameters

and save the already allocated ports in a state file, so that requesting
new families does not change already allocated ports.
parent c9fa707f
...@@ -233,6 +233,13 @@ class TestBalancerPorts(ERP5InstanceTestCase): ...@@ -233,6 +233,13 @@ class TestBalancerPorts(ERP5InstanceTestCase):
param_dict['family-{family_name}'.format(family_name=family_name)]) param_dict['family-{family_name}'.format(family_name=family_name)])
self.checkValidHTTPSURL( self.checkValidHTTPSURL(
param_dict['family-{family_name}-v6'.format(family_name=family_name)]) param_dict['family-{family_name}-v6'.format(family_name=family_name)])
# ports are allocated in alphabetical order and are "stable", ie. is not supposed
# to change after updating software release, because there is typically a rapid-cdn
# frontend pointing to this port.
self.assertEqual(six.moves.urllib.parse.urlparse(param_dict['family-family1']).port, 2152)
self.assertEqual(usix.moves.rllib.parse.urlparse(param_dict['family-family1-v6']).port, 2152)
self.assertEqual(usix.moves.rllib.parse.urlparse(param_dict['family-family2']).port, 2154)
self.assertEqual(usix.moves.rllib.parse.urlparse(param_dict['family-family2-v6']).port, 2154)
def test_published_test_runner_url(self): def test_published_test_runner_url(self):
# each family's also a list of test test runner URLs, by default 3 per family # each family's also a list of test test runner URLs, by default 3 per family
...@@ -241,6 +248,7 @@ class TestBalancerPorts(ERP5InstanceTestCase): ...@@ -241,6 +248,7 @@ class TestBalancerPorts(ERP5InstanceTestCase):
family_test_runner_url_list = param_dict[ family_test_runner_url_list = param_dict[
'{family_name}-test-runner-url-list'.format(family_name=family_name)] '{family_name}-test-runner-url-list'.format(family_name=family_name)]
self.assertEqual(3, len(family_test_runner_url_list)) self.assertEqual(3, len(family_test_runner_url_list))
self.assertEqual(3, len(set(family_test_runner_url_list)))
for url in family_test_runner_url_list: for url in family_test_runner_url_list:
self.checkValidHTTPSURL(url) self.checkValidHTTPSURL(url)
...@@ -281,6 +289,61 @@ class TestBalancerPorts(ERP5InstanceTestCase): ...@@ -281,6 +289,61 @@ class TestBalancerPorts(ERP5InstanceTestCase):
]) ])
class TestBalancerPortsStable(ERP5InstanceTestCase):
"""Instantiate with two one families and a frontend, then
re-request with one more family and one more frontend, the ports
should not change
"""
__partition_reference__ = 'ap'
@classmethod
def getInstanceParameterDict(cls):
return {
'_':
json.dumps(
{
"frontend": {
"zzz": {
"zope-family": "zzz"
}
},
"zope-partition-dict": {
"zzz": {
"instance-count": 1,
"family": "zzz"
},
},
})
}
def test_same_balancer_ports_when_adding_zopes_or_frontends(self):
param_dict_before = self.getRootPartitionConnectionParameterDict()
# re-request with one more frontend and one more backend, that are before
# the existing ones when sorting alphabetically
instance_parameter_dict = json.loads(self.getInstanceParameterDict()['_'])
instance_parameter_dict['frontend']['aaa'] = {"zope-family": "aaa"}
instance_parameter_dict['zope-partition-dict']['aaa'] = {
"instance-count": 2,
"family": "aaa"
}
def rerequest():
return self.slap.request(
software_release=self.getSoftwareURL(),
software_type=self.getInstanceSoftwareType(),
partition_reference=self.default_partition_reference,
partition_parameter_kw={'_': json.dumps(instance_parameter_dict)},
state='started')
rerequest()
self.slap.waitForInstance(max_retry=10)
param_dict_after = json.loads(rerequest().getConnectionParameterDict()['_'])
self.assertEqual(param_dict_before['family-zzz-v6'], param_dict_after['family-zzz-v6'])
self.assertEqual(param_dict_before['url-frontend-zzz'], param_dict_after['url-frontend-zzz'])
self.assertNotEqual(param_dict_before['family-zzz-v6'], param_dict_after['family-aaa-v6'])
self.assertNotEqual(param_dict_before['url-frontend-zzz'], param_dict_after['url-frontend-aaa'])
class TestSeleniumTestRunner(ERP5InstanceTestCase, TestPublishedURLIsReachableMixin): class TestSeleniumTestRunner(ERP5InstanceTestCase, TestPublishedURLIsReachableMixin):
"""Test ERP5 can be instantiated with selenium server for test runner. """Test ERP5 can be instantiated with selenium server for test runner.
""" """
......
...@@ -70,7 +70,7 @@ md5sum = 274365ebbade26558ca4836837e781aa ...@@ -70,7 +70,7 @@ md5sum = 274365ebbade26558ca4836837e781aa
[template] [template]
filename = instance.cfg.in filename = instance.cfg.in
md5sum = 9a7f7888ba4183c9d900e862074f3baf md5sum = 33cf43b51d245f34fe7442eaac570f3f
[template-erp5] [template-erp5]
filename = instance-erp5.cfg.in filename = instance-erp5.cfg.in
...@@ -90,11 +90,11 @@ md5sum = c3e3f8cd985407931b705d15bdedc8d9 ...@@ -90,11 +90,11 @@ md5sum = c3e3f8cd985407931b705d15bdedc8d9
[template-balancer] [template-balancer]
filename = instance-balancer.cfg.in filename = instance-balancer.cfg.in
md5sum = d38f0716e3f9402be0d82a2748c7defb md5sum = 2901ccefe10dd6ebe944c5fc69fb1ff7
[template-haproxy-cfg] [template-haproxy-cfg]
filename = haproxy.cfg.in filename = haproxy.cfg.in
md5sum = 85a8c0dadf7b648ef9748b6199dcfeb6 md5sum = 9988a14c4108e3bce3f871e34673cdd5
[template-rsyslogd-cfg] [template-rsyslogd-cfg]
filename = rsyslogd.cfg.in filename = rsyslogd.cfg.in
......
{# This file configures haproxy to redirect requests from ports to specific urls. {# This file configures haproxy to redirect requests from ports to specific urls.
# It provides TLS support for server and optionnaly for client. # It provides TLS support for server and optionnaly for client.
# #
...@@ -45,8 +44,7 @@ ...@@ -45,8 +44,7 @@
# # The mapping of backends, keyed by family name # # The mapping of backends, keyed by family name
# "backend-dict": { # "backend-dict": {
# "family-secure": { # "family-secure": {
# ( 8000, # port int # ( 8000, # port int
# 'https', # proto str
# True, # ssl_required bool # True, # ssl_required bool
# None, # timeout (in seconds) int | None # None, # timeout (in seconds) int | None
# [ # backends # [ # backends
...@@ -57,8 +55,7 @@ ...@@ -57,8 +55,7 @@
# ), # ),
# }, # },
# "family-default": { # "family-default": {
# ( 8002, # port int # ( 8002, # port int
# 'https', # proto str
# False, # ssl_required bool # False, # ssl_required bool
# None, # timeout (in seconds) int | None # None, # timeout (in seconds) int | None
# [ # backends # [ # backends
...@@ -151,7 +148,7 @@ defaults ...@@ -151,7 +148,7 @@ defaults
{% set family_path_routing_dict = parameter_dict['family-path-routing-dict'] %} {% set family_path_routing_dict = parameter_dict['family-path-routing-dict'] %}
{% set path_routing_list = parameter_dict['path-routing-list'] %} {% set path_routing_list = parameter_dict['path-routing-list'] %}
{% for name, (port, _, certificate_authentication, timeout, backend_list) in sorted(six.iteritems(parameter_dict['backend-dict'])) -%} {% for name, (port, certificate_authentication, timeout, backend_list) in sorted(six.iteritems(parameter_dict['backend-dict'])) -%}
listen family_{{ name }} listen family_{{ name }}
{%- if parameter_dict.get('ca-cert') -%} {%- if parameter_dict.get('ca-cert') -%}
{%- set ssl_auth = ' ca-file ' ~ parameter_dict['ca-cert'] ~ ' verify' ~ ( ' required' if certificate_authentication else ' optional crt-ignore-err all' ) ~ ' crl-file ' ~ parameter_dict['crl'] %} {%- set ssl_auth = ' ca-file ' ~ parameter_dict['ca-cert'] ~ ' verify' ~ ( ' required' if certificate_authentication else ' optional crt-ignore-err all' ) ~ ' crl-file ' ~ parameter_dict['crl'] %}
......
...@@ -116,70 +116,8 @@ command = ${caucase-updater-housekeeper:output} ...@@ -116,70 +116,8 @@ command = ${caucase-updater-housekeeper:output}
update-command = ${:command} update-command = ${:command}
{% endif -%} {% endif -%}
{% set haproxy_dict = {} -%}
{% set zope_virtualhost_monster_backend_dict = {} %}
{% set test_runner_url_dict = {} %} {# family_name => list of URLs #}
{% set next_port = functools.partial(next, itertools.count(slapparameter_dict['tcpv4-port'])) -%}
{% for family_name, parameter_id_list in sorted(
six.iteritems(slapparameter_dict['zope-family-dict'])) -%}
{% set zope_family_address_list = [] -%}
{% set ssl_authentication = slapparameter_dict['ssl-authentication-dict'][family_name] -%}
{% set has_webdav = [] -%}
{% for parameter_id in parameter_id_list -%}
{% set zope_address_list = slapparameter_dict[parameter_id] -%}
{% for zope_address, maxconn, webdav in zope_address_list -%}
{% if webdav -%}
{% do has_webdav.append(None) %}
{% endif -%}
{% set zope_effective_address = zope_address -%}
{% do zope_family_address_list.append((zope_effective_address, maxconn, webdav)) -%}
{% endfor -%}
{# # Generate entries with rewrite rule for test runnners #}
{% set test_runner_address_list = slapparameter_dict.get(parameter_id ~ '-test-runner-address-list', []) %}
{% if test_runner_address_list -%}
{% set test_runner_backend_mapping = {} %}
{% set test_runner_balancer_url_list = [] %}
{% set test_runner_external_port = next_port() %}
{% for i, (test_runner_internal_ip, test_runner_internal_port) in enumerate(test_runner_address_list) %}
{% do test_runner_backend_mapping.__setitem__(
'unit_test_' ~ i,
'http://' ~ test_runner_internal_ip ~ ':' ~ test_runner_internal_port ) %}
{% do test_runner_balancer_url_list.append(
'https://' ~ ipv4 ~ ':' ~ test_runner_external_port ~ '/unit_test_' ~ i ~ '/' ) %}
{% endfor %}
{% do zope_virtualhost_monster_backend_dict.__setitem__(
(ipv4, test_runner_external_port),
( ssl_authentication, test_runner_backend_mapping ) ) -%}
{% do test_runner_url_dict.__setitem__(family_name, test_runner_balancer_url_list) -%}
{% endif -%}
{% endfor -%}
{# Make rendering fail artificially if any family has no known backend.
# This is useful as haproxy's hot-reconfiguration mechanism is
# supervisord-incompatible.
# As jinja2 postpones KeyError until place-holder value is actually used,
# do a no-op getitem.
-#}
{% do zope_family_address_list[0][0] -%}
{#
# We use to have haproxy then apache, now haproxy is playing apache's role
# To keep port stable, we consume one port so that haproxy use the same port
# that apache was using before.
-#}
{% set _ = next_port() -%}
{% set haproxy_port = next_port() -%}
{% if has_webdav -%}
{% set external_scheme = 'webdavs' -%}
{% else %}
{% set external_scheme = 'https' -%}
{% endif -%}
{% do haproxy_dict.__setitem__(family_name, (haproxy_port, external_scheme, slapparameter_dict['ssl-authentication-dict'][family_name], slapparameter_dict['timeout-dict'][family_name], zope_family_address_list)) -%}
{% endfor -%}
[haproxy-cfg-parameter-dict] [haproxy-cfg-parameter-dict]
recipe = slapos.recipe.build
ipv4 = {{ ipv4 }} ipv4 = {{ ipv4 }}
ipv6 = {{ ipv6 }} ipv6 = {{ ipv6 }}
cert = ${haproxy-conf-ssl:certificate} cert = ${haproxy-conf-ssl:certificate}
...@@ -193,9 +131,90 @@ family-path-routing-dict = {{ dumps(slapparameter_dict['family-path-routing-dict ...@@ -193,9 +131,90 @@ family-path-routing-dict = {{ dumps(slapparameter_dict['family-path-routing-dict
pidfile = ${directory:run}/haproxy.pid pidfile = ${directory:run}/haproxy.pid
log-socket = ${rsyslogd-cfg-parameter-dict:log-socket} log-socket = ${rsyslogd-cfg-parameter-dict:log-socket}
server-check-path = {{ dumps(slapparameter_dict['haproxy-server-check-path']) }} server-check-path = {{ dumps(slapparameter_dict['haproxy-server-check-path']) }}
backend-dict = {{ dumps(haproxy_dict) }} slapparameter-dict = {{ dumps(slapparameter_dict) }}
zope-virtualhost-monster-backend-dict = {{ dumps(zope_virtualhost_monster_backend_dict) }} ports-state-file = ${buildout:directory}/.${:_buildout_section_name_}-ports.json
init =
import functools
import itertools
import json
import os
import shutil
import six
from zc.buildout import UserError
slapparameter_dict = options['slapparameter-dict']
ipv4 = options['ipv4']
ipv6 = options['ipv6']
# read port state file
port_dict = {}
previous_port_dict = None
if os.path.exists(options['ports-state-file']):
with open(options['ports-state-file']) as f:
port_dict = json.load(f)
previous_port_dict = dict(port_dict)
_next_port = functools.partial(next, itertools.count(slapparameter_dict['tcpv4-port']))
def get_port(name):
if name in port_dict:
return port_dict[name]
port = _next_port()
while port in port_dict.values():
port = _next_port()
port_dict[name] = port
return port
haproxy_dict = {}
zope_virtualhost_monster_backend_dict = {}
for family_name, parameter_id_list in sorted(
six.iteritems(slapparameter_dict['zope-family-dict'])):
zope_family_address_list = []
ssl_authentication = slapparameter_dict['ssl-authentication-dict'][family_name]
for parameter_id in parameter_id_list:
zope_family_address_list.extend(slapparameter_dict[parameter_id])
# Generate entries with rewrite rule for test runnners
test_runner_address_list = slapparameter_dict.get(parameter_id + '-test-runner-address-list', [])
if test_runner_address_list:
test_runner_backend_mapping = {}
test_runner_balancer_url_list = []
for i, (test_runner_internal_ip, test_runner_internal_port) in enumerate(test_runner_address_list):
test_runner_backend_mapping['unit_test_%s' % i] = \
'http://%s:%s' % (test_runner_internal_ip, test_runner_internal_port)
test_runner_balancer_url_list.append(
'https://%s:%s/unit_test_%s/' % (ipv4, get_port('test-runner-' + family_name), i))
zope_virtualhost_monster_backend_dict[(ipv4, get_port('test-runner-' + family_name))] =\
( ssl_authentication, test_runner_backend_mapping )
self.buildout['publish'][family_name + '-test-runner-url-list'] = test_runner_balancer_url_list
if not zope_family_address_list:
raise UserError('No zope defined for family %s (maybe not ready)' % family_name)
# consume a port for compatibility when were using apache + haproxy
get_port('apache-compatibility-' + family_name)
legacy_port = get_port('legacy-' + family_name)
# a port for monitoring promise (which port is not important, the promise checks
# that haproxy is healthy enough to listen on a port)
options['haproxy-promise-port'] = legacy_port
haproxy_dict[family_name] = (
legacy_port,
ssl_authentication,
slapparameter_dict['timeout-dict'][family_name],
zope_family_address_list,
)
external_scheme = 'webdavs' if any(a[2] for a in zope_family_address_list) else 'https'
self.buildout['publish'][family_name] = "{external_scheme}://{ipv4}:{legacy_port}".format(**locals())
self.buildout['publish'][family_name + "-v6"] = "{external_scheme}://[{ipv6}]:{legacy_port}".format(**locals())
options['backend-dict'] = haproxy_dict
options['zope-virtualhost-monster-backend-dict'] = zope_virtualhost_monster_backend_dict
if port_dict != previous_port_dict:
with open(options['ports-state-file'] + '.tmp', 'w') as f:
json.dump(port_dict, f, indent=True)
shutil.move(options['ports-state-file'] + '.tmp', options['ports-state-file'])
[haproxy-cfg] [haproxy-cfg]
< = jinja2-template-base < = jinja2-template-base
...@@ -304,7 +323,7 @@ certificate-and-key = ${directory:etc}/certificate-and-key-generated.pem ...@@ -304,7 +323,7 @@ certificate-and-key = ${directory:etc}/certificate-and-key-generated.pem
promise = check_socket_listening promise = check_socket_listening
name = haproxy.py name = haproxy.py
config-host = {{ ipv4 }} config-host = {{ ipv4 }}
config-port = {{ next(six.itervalues(haproxy_dict))[0] }} config-port = ${haproxy-cfg-parameter-dict:haproxy-promise-port}
[{{ section('publish') }}] [{{ section('publish') }}]
recipe = slapos.cookbook:publish.serialised recipe = slapos.cookbook:publish.serialised
...@@ -392,7 +411,7 @@ config-command = "{{ parameter_dict["check-computer-memory-binary"] }}" -db ${mo ...@@ -392,7 +411,7 @@ config-command = "{{ parameter_dict["check-computer-memory-binary"] }}" -db ${mo
[monitor-instance-parameter] [monitor-instance-parameter]
monitor-httpd-ipv6 = {{ (ipv6_set | list)[0] }} monitor-httpd-ipv6 = {{ (ipv6_set | list)[0] }}
monitor-httpd-port = {{ next_port() }} monitor-httpd-port = 2197
monitor-title = {{ slapparameter_dict['name'] }} monitor-title = {{ slapparameter_dict['name'] }}
password = {{ slapparameter_dict['monitor-passwd'] }} password = {{ slapparameter_dict['monitor-passwd'] }}
......
...@@ -75,8 +75,9 @@ url = {{ template_balancer }} ...@@ -75,8 +75,9 @@ url = {{ template_balancer }}
filename = instance-balancer.cfg filename = instance-balancer.cfg
extra-context = extra-context =
section parameter_dict dynamic-template-balancer-parameters section parameter_dict dynamic-template-balancer-parameters
import itertools itertools
import hashlib hashlib import hashlib hashlib
# XXX: only used in software/slapos-master:
import itertools itertools
import functools functools import functools functools
import-list = import-list =
file caucase context:caucase-jinja2-library file caucase context:caucase-jinja2-library
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment