Commit 6e735808 authored by Jérome Perrin's avatar Jérome Perrin

stack/erp5: implement Zope's rewrite rules in ERP5 balancer partition

The strategy for compatibility is that:
 - haproxy still listen on the same port as before, without rewrite rule.
   This is called "legacy" port.
 - for each frontend from request parameters, we introduce an haproxy
   frontend with a rewrite for the corresponding `internal-path`
   parameter.
 - the shared frontend instance is updated to use this new frontend
   entry from haproxy. This will cause a small downtime until the shared
   frontend is updated to the new URL on ERP5, but since this feature
   was not used, it's OK.

Technical details are that we:
 - split haproxy config to have frontends and backends.
 - introduce one frontend in haproxy for each frontend from request
   parameters.
 - routing-rule-list argument is still honored the same way, globally
   and after path from frontend.
 - change the shared frontend requests to use "" type, no longer "zope"
   type.
 - we don't do automatic detection of /VirtualHostRoot in URL but always
   add it, because it could be used to trick zope into thinking it
   serves requests for an arbitrary host and do open redirects
 - before using the request's host header in virtualhost path, we check
   that it does not contain /, to prevent injection of virutalhost path
   elements through the host header.
 - we don't use the "path" parameter from shared frontend, because we
   want the frontend to be simple, so we don't want it to rewrite the
   request path (which is also the reason why we deprecated "zope" type)
 - the tests have changed a lot, because they were using what's now the
   "legacy" URL types, so we updated it to use the new URL types with
   all the /VirtualHostRoot/../ in path and also because they use IPv6
   URL, no longer IPv4
parent 5b3fc1f2
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
"additionalProperties": false, "additionalProperties": false,
"definitions": { "definitions": {
"routing-rule-list": { "routing-rule-list": {
"description": "Maps the path received in requests to given zope path. Rules are applied in the order they are given. This requires the path received from the outside world (typically: frontend) to have its root correspond to Zope's root (for frontend: 'path' parameter must be empty), with the customary VirtualHostMonster construct (for frontend: 'type' must be 'zope').", "description": "Maps the path received in requests to given zope path. Rules are applied in the order they are given, after 'internal-path' from 'frontend' parameter. This also supports legacy frontends, using Rapid CDN with \"zope\" type.",
"type": "array", "type": "array",
"default": [ "default": [
[ [
......
...@@ -314,7 +314,7 @@ class CaucaseCertificate(ManagedResource): ...@@ -314,7 +314,7 @@ class CaucaseCertificate(ManagedResource):
) )
return os.path.join(software_release_root_path, 'bin', 'caucase') return os.path.join(software_release_root_path, 'bin', 'caucase')
def request(self, common_name: str, caucase: CaucaseService) -> None: def request(self, common_name: str, caucase: CaucaseService, san: x509.SubjectAlternativeName=None) -> None:
"""Generate certificate and request signature to the caucase service. """Generate certificate and request signature to the caucase service.
This overwrite any previously requested certificate for this instance. This overwrite any previously requested certificate for this instance.
...@@ -345,11 +345,10 @@ class CaucaseCertificate(ManagedResource): ...@@ -345,11 +345,10 @@ class CaucaseCertificate(ManagedResource):
NameOID.COMMON_NAME, NameOID.COMMON_NAME,
common_name, common_name,
), ),
])).sign( ]))
key, if san:
hashes.SHA256(), csr = csr.add_extension(san, critical=True)
default_backend(), csr = csr.sign(key, hashes.SHA256(), default_backend())
)
with open(self.csr_file, 'wb') as f: with open(self.csr_file, 'wb') as f:
f.write(csr.public_bytes(serialization.Encoding.PEM)) f.write(csr.public_bytes(serialization.Encoding.PEM))
......
...@@ -113,6 +113,12 @@ class BalancerTestCase(ERP5InstanceTestCase): ...@@ -113,6 +113,12 @@ class BalancerTestCase(ERP5InstanceTestCase):
'ssl-authentication-dict': {'default': False}, 'ssl-authentication-dict': {'default': False},
'ssl': {}, 'ssl': {},
'timeout-dict': {'default': None}, 'timeout-dict': {'default': None},
'frontend-parameter-dict': {
'default': {
'internal-path': '',
'zope-family': 'default',
},
},
'family-path-routing-dict': {}, 'family-path-routing-dict': {},
'path-routing-list': [], 'path-routing-list': [],
} }
...@@ -122,24 +128,72 @@ class BalancerTestCase(ERP5InstanceTestCase): ...@@ -122,24 +128,72 @@ class BalancerTestCase(ERP5InstanceTestCase):
return {'_': json.dumps(cls._getInstanceParameterDict())} return {'_': json.dumps(cls._getInstanceParameterDict())}
def setUp(self) -> None: def setUp(self) -> None:
self.default_balancer_url = json.loads( self.default_balancer_direct_url = json.loads(
self.computer_partition.getConnectionParameterDict()['_'])['default'] self.computer_partition.getConnectionParameterDict()['_'])['default']
self.default_balancer_zope_url = json.loads(
self.computer_partition.getConnectionParameterDict()['_'])['url-backend-default']
class TestURLRewrite(BalancerTestCase):
__partition_reference__ = 'ur'
def test_direct(self):
self.assertEqual(requests.get(self.default_balancer_direct_url, verify=False).json()['Path'], '/')
self.assertEqual(
requests.get(
urllib.parse.urljoin(
self.default_balancer_direct_url,
'/VirtualHostBase/https/example.com:443/VirtualHostRoot/path'),
verify=False
).json()['Path'],
'/VirtualHostBase/https/example.com:443/VirtualHostRoot/path')
def test_zope(self):
netloc = urllib.parse.urlparse(self.default_balancer_zope_url).netloc
self.assertEqual(
requests.get(self.default_balancer_zope_url, verify=False).json()['Path'],
f'/VirtualHostBase/https/{netloc}/VirtualHostRoot/')
self.assertEqual(
requests.get(urllib.parse.urljoin(
self.default_balancer_zope_url, 'path'), verify=False).json()['Path'],
f'/VirtualHostBase/https/{netloc}/VirtualHostRoot/path')
self.assertEqual(
requests.get(
urllib.parse.urljoin(
self.default_balancer_zope_url,
'/VirtualHostBase/https/example.com:443/VirtualHostRoot/path'),
verify=False
).json()['Path'],
f'/VirtualHostBase/https/{netloc}/VirtualHostRoot/VirtualHostBase/https/example.com:443/VirtualHostRoot/path')
def test_bad_host(self):
self.assertEqual(
requests.get(self.default_balancer_zope_url, headers={'Host': 'a/b'}, verify=False).status_code,
requests.codes.bad_request)
class SlowHTTPServer(ManagedHTTPServer): class SlowHTTPServer(ManagedHTTPServer):
"""An HTTP Server which reply after a timeout. """An HTTP Server which reply after a timeout.
Timeout is 2 seconds by default, and can be specified in the path of the URL Timeout is 2 seconds by default, and can be specified in the path of the URL:
GET /{timeout}
but because balancer rewrites the URL, the actual URL used by this server is:
GET /VirtualHostBase/https/{host}/VirtualHostRoot/{timeout}
""" """
class RequestHandler(BaseHTTPRequestHandler): class RequestHandler(BaseHTTPRequestHandler):
def do_GET(self) -> None: def do_GET(self) -> None:
self.send_response(200)
self.send_header("Content-Type", "text/plain")
timeout = 2 timeout = 2
if self.path == '/': # for health checks
timeout = 0
try: try:
timeout = int(self.path[1:]) timeout = int(self.path.split('/')[5])
except ValueError: except (ValueError, IndexError):
pass pass
self.send_response(200)
self.send_header("Content-Type", "text/plain")
time.sleep(timeout) time.sleep(timeout)
self.end_headers() self.end_headers()
self.wfile.write(b"OK\n") self.wfile.write(b"OK\n")
...@@ -161,12 +215,12 @@ class TestTimeout(BalancerTestCase, CrontabMixin): ...@@ -161,12 +215,12 @@ class TestTimeout(BalancerTestCase, CrontabMixin):
def test_timeout(self) -> None: def test_timeout(self) -> None:
self.assertEqual( self.assertEqual(
requests.get( requests.get(
urllib.parse.urljoin(self.default_balancer_url, '/1'), urllib.parse.urljoin(self.default_balancer_zope_url, '/1'),
verify=False).status_code, verify=False).status_code,
requests.codes.ok) requests.codes.ok)
self.assertEqual( self.assertEqual(
requests.get( requests.get(
urllib.parse.urljoin(self.default_balancer_url, '/5'), urllib.parse.urljoin(self.default_balancer_zope_url, '/5'),
verify=False).status_code, verify=False).status_code,
requests.codes.gateway_timeout) requests.codes.gateway_timeout)
...@@ -178,13 +232,13 @@ class TestLog(BalancerTestCase, CrontabMixin): ...@@ -178,13 +232,13 @@ class TestLog(BalancerTestCase, CrontabMixin):
@classmethod @classmethod
def _getInstanceParameterDict(cls) -> dict: def _getInstanceParameterDict(cls) -> dict:
parameter_dict = super()._getInstanceParameterDict() parameter_dict = super()._getInstanceParameterDict()
# use a slow server instead # use a slow server instead, so that we can test logs with slow requests
parameter_dict['dummy_http_server'] = [[cls.getManagedResource("slow_web_server", SlowHTTPServer).netloc, 1, False]] parameter_dict['dummy_http_server'] = [[cls.getManagedResource("slow_web_server", SlowHTTPServer).netloc, 1, False]]
return parameter_dict return parameter_dict
def test_access_log_format(self) -> None: def test_access_log_format(self) -> None:
requests.get( requests.get(
urllib.parse.urljoin(self.default_balancer_url, '/url_path'), urllib.parse.urljoin(self.default_balancer_zope_url, '/url_path'),
verify=False, verify=False,
) )
time.sleep(.5) # wait a bit more until access is logged time.sleep(.5) # wait a bit more until access is logged
...@@ -197,7 +251,7 @@ class TestLog(BalancerTestCase, CrontabMixin): ...@@ -197,7 +251,7 @@ class TestLog(BalancerTestCase, CrontabMixin):
# the request - but our test machines can be slow sometimes, so we tolerate # the request - but our test machines can be slow sometimes, so we tolerate
# it can take up to 20 seconds. # it can take up to 20 seconds.
match = re.match( match = re.match(
r'([(\d\.)]+) - - \[(.*?)\] "(.*?)" (\d+) (\d+) "(.*?)" "(.*?)" (\d+)', r'([(\da-fA-F:\.)]+) - - \[(.*?)\] "(.*?)" (\d+) (\d+) "(.*?)" "(.*?)" (\d+)',
access_line access_line
) )
self.assertTrue(match) self.assertTrue(match)
...@@ -208,7 +262,7 @@ class TestLog(BalancerTestCase, CrontabMixin): ...@@ -208,7 +262,7 @@ class TestLog(BalancerTestCase, CrontabMixin):
def test_access_log_apachedex_report(self) -> None: def test_access_log_apachedex_report(self) -> None:
# make a request so that we have something in the logs # make a request so that we have something in the logs
requests.get(self.default_balancer_url, verify=False) requests.get(self.default_balancer_zope_url, verify=False)
# crontab for apachedex is executed # crontab for apachedex is executed
self._executeCrontabAtDate('generate-apachedex-report', '23:59') self._executeCrontabAtDate('generate-apachedex-report', '23:59')
...@@ -233,7 +287,7 @@ class TestLog(BalancerTestCase, CrontabMixin): ...@@ -233,7 +287,7 @@ class TestLog(BalancerTestCase, CrontabMixin):
self._executeCrontabAtDate('logrotate', '2000-01-01') self._executeCrontabAtDate('logrotate', '2000-01-01')
# make a request so that we have something in the logs # make a request so that we have something in the logs
requests.get(self.default_balancer_url, verify=False).raise_for_status() requests.get(self.default_balancer_zope_url, verify=False).raise_for_status()
# slow query crontab depends on crontab for log rotation # slow query crontab depends on crontab for log rotation
# to be executed first. # to be executed first.
...@@ -248,7 +302,7 @@ class TestLog(BalancerTestCase, CrontabMixin): ...@@ -248,7 +302,7 @@ class TestLog(BalancerTestCase, CrontabMixin):
) )
self.assertTrue(os.path.exists(rotated_log_file)) self.assertTrue(os.path.exists(rotated_log_file))
requests.get(self.default_balancer_url, verify=False).raise_for_status() requests.get(self.default_balancer_zope_url, verify=False).raise_for_status()
# on next day execution of logrotate, log files are compressed # on next day execution of logrotate, log files are compressed
self._executeCrontabAtDate('logrotate', '2050-01-02') self._executeCrontabAtDate('logrotate', '2050-01-02')
self.assertTrue(os.path.exists(rotated_log_file + '.xz')) self.assertTrue(os.path.exists(rotated_log_file + '.xz'))
...@@ -262,11 +316,11 @@ class TestLog(BalancerTestCase, CrontabMixin): ...@@ -262,11 +316,11 @@ class TestLog(BalancerTestCase, CrontabMixin):
# after a while, balancer should detect and log this event in error log # after a while, balancer should detect and log this event in error log
time.sleep(5) time.sleep(5)
self.assertEqual( self.assertEqual(
requests.get(self.default_balancer_url, verify=False).status_code, requests.get(self.default_balancer_zope_url, verify=False).status_code,
requests.codes.service_unavailable) requests.codes.service_unavailable)
with open(os.path.join(self.computer_partition_root_path, 'var', 'log', 'apache-error.log')) as error_log_file: with open(os.path.join(self.computer_partition_root_path, 'var', 'log', 'apache-error.log')) as error_log_file:
error_line = error_log_file.read().splitlines()[-1] error_line = error_log_file.read().splitlines()[-1]
self.assertIn('proxy family_default has no server available!', error_line) self.assertIn('backend default has no server available!', error_line)
# this log also include a timestamp # this log also include a timestamp
self.assertRegex(error_line, r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}') self.assertRegex(error_line, r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}')
...@@ -274,7 +328,9 @@ class TestLog(BalancerTestCase, CrontabMixin): ...@@ -274,7 +328,9 @@ class TestLog(BalancerTestCase, CrontabMixin):
class BalancerCookieHTTPServer(ManagedHTTPServer): class BalancerCookieHTTPServer(ManagedHTTPServer):
"""An HTTP Server which can set balancer cookie. """An HTTP Server which can set balancer cookie.
This server set cookie when requested /set-cookie path. This server set cookie when requested /set-cookie path (actually
/VirtualHostBase/https/{host}/VirtualHostRoot/set-cookie , which is
added by balancer proxy)
The reply body is the name used when registering this resource The reply body is the name used when registering this resource
using getManagedResource. This way we can assert which using getManagedResource. This way we can assert which
...@@ -288,7 +344,8 @@ class BalancerCookieHTTPServer(ManagedHTTPServer): ...@@ -288,7 +344,8 @@ class BalancerCookieHTTPServer(ManagedHTTPServer):
def do_GET(self) -> None: def do_GET(self) -> None:
self.send_response(200) self.send_response(200)
self.send_header("Content-Type", "text/plain") self.send_header("Content-Type", "text/plain")
if self.path == '/set_cookie':
if self.path != '/' and self.path.split('/')[5] == 'set_cookie':
# the balancer tells the backend what's the name of the balancer cookie with # the balancer tells the backend what's the name of the balancer cookie with
# the X-Balancer-Current-Cookie header. # the X-Balancer-Current-Cookie header.
self.send_header('Set-Cookie', '%s=anything' % self.headers['X-Balancer-Current-Cookie']) self.send_header('Set-Cookie', '%s=anything' % self.headers['X-Balancer-Current-Cookie'])
...@@ -319,7 +376,7 @@ class TestBalancer(BalancerTestCase): ...@@ -319,7 +376,7 @@ class TestBalancer(BalancerTestCase):
def test_balancer_round_robin(self) -> None: def test_balancer_round_robin(self) -> None:
# requests are by default balanced to both servers # requests are by default balanced to both servers
self.assertEqual( self.assertEqual(
{requests.get(self.default_balancer_url, verify=False).text for _ in range(10)}, {requests.get(self.default_balancer_zope_url, verify=False).text for _ in range(10)},
{'backend_web_server1', 'backend_web_server2'} {'backend_web_server1', 'backend_web_server2'}
) )
...@@ -328,7 +385,7 @@ class TestBalancer(BalancerTestCase): ...@@ -328,7 +385,7 @@ class TestBalancer(BalancerTestCase):
self.getManagedResource("backend_web_server2", BalancerCookieHTTPServer).close() self.getManagedResource("backend_web_server2", BalancerCookieHTTPServer).close()
self.addCleanup(self.getManagedResource("backend_web_server2", BalancerCookieHTTPServer).open) self.addCleanup(self.getManagedResource("backend_web_server2", BalancerCookieHTTPServer).open)
self.assertEqual( self.assertEqual(
{requests.get(self.default_balancer_url, verify=False).text for _ in range(10)}, {requests.get(self.default_balancer_zope_url, verify=False).text for _ in range(10)},
{'backend_web_server1',} {'backend_web_server1',}
) )
...@@ -336,7 +393,7 @@ class TestBalancer(BalancerTestCase): ...@@ -336,7 +393,7 @@ class TestBalancer(BalancerTestCase):
# if backend provides a "SERVERID" cookie, balancer will overwrite it with the # if backend provides a "SERVERID" cookie, balancer will overwrite it with the
# backend selected by balancing algorithm # backend selected by balancing algorithm
self.assertIn( self.assertIn(
requests.get(urllib.parse.urljoin(self.default_balancer_url, '/set_cookie'), verify=False).cookies['SERVERID'], requests.get(urllib.parse.urljoin(self.default_balancer_zope_url, '/set_cookie'), verify=False).cookies['SERVERID'],
('default-0', 'default-1'), ('default-0', 'default-1'),
) )
...@@ -344,7 +401,7 @@ class TestBalancer(BalancerTestCase): ...@@ -344,7 +401,7 @@ class TestBalancer(BalancerTestCase):
# if request is made with the sticky cookie, the client stick on one balancer # if request is made with the sticky cookie, the client stick on one balancer
cookies = dict(SERVERID='default-1') cookies = dict(SERVERID='default-1')
self.assertEqual( self.assertEqual(
{requests.get(self.default_balancer_url, verify=False, cookies=cookies).text for _ in range(10)}, {requests.get(self.default_balancer_zope_url, verify=False, cookies=cookies).text for _ in range(10)},
{'backend_web_server2',} {'backend_web_server2',}
) )
...@@ -352,7 +409,7 @@ class TestBalancer(BalancerTestCase): ...@@ -352,7 +409,7 @@ class TestBalancer(BalancerTestCase):
self.getManagedResource("backend_web_server2", BalancerCookieHTTPServer).close() self.getManagedResource("backend_web_server2", BalancerCookieHTTPServer).close()
self.addCleanup(self.getManagedResource("backend_web_server2", BalancerCookieHTTPServer).open) self.addCleanup(self.getManagedResource("backend_web_server2", BalancerCookieHTTPServer).open)
self.assertEqual( self.assertEqual(
requests.get(self.default_balancer_url, verify=False, cookies=cookies).text, requests.get(self.default_balancer_zope_url, verify=False, cookies=cookies).text,
'backend_web_server1') 'backend_web_server1')
def test_balancer_stats_socket(self) -> None: def test_balancer_stats_socket(self) -> None:
...@@ -372,7 +429,7 @@ class TestBalancer(BalancerTestCase): ...@@ -372,7 +429,7 @@ class TestBalancer(BalancerTestCase):
raise raise
self.assertEqual(socat_process.poll(), 0) self.assertEqual(socat_process.poll(), 0)
# output is a csv # output is a csv
self.assertIn(b'family_default,FRONTEND,', output) self.assertIn(b'\ndefault,BACKEND,', output)
class TestTestRunnerEntryPoints(BalancerTestCase): class TestTestRunnerEntryPoints(BalancerTestCase):
...@@ -471,7 +528,7 @@ class TestHTTP(BalancerTestCase): ...@@ -471,7 +528,7 @@ class TestHTTP(BalancerTestCase):
'--insecure', '--insecure',
'--write-out', '--write-out',
'%{http_version}', '%{http_version}',
self.default_balancer_url, self.default_balancer_zope_url,
]), ]),
b'2', b'2',
) )
...@@ -482,16 +539,16 @@ class TestHTTP(BalancerTestCase): ...@@ -482,16 +539,16 @@ class TestHTTP(BalancerTestCase):
session.verify = False session.verify = False
# do a first request, which establish a first connection # do a first request, which establish a first connection
session.get(self.default_balancer_url).raise_for_status() session.get(self.default_balancer_zope_url).raise_for_status()
# "break" new connection method and check we can make another request # "break" new connection method and check we can make another request
with mock.patch( with mock.patch(
"requests.packages.urllib3.connectionpool.HTTPSConnectionPool._new_conn", "requests.packages.urllib3.connectionpool.HTTPSConnectionPool._new_conn",
) as new_conn: ) as new_conn:
session.get(self.default_balancer_url).raise_for_status() session.get(self.default_balancer_zope_url).raise_for_status()
new_conn.assert_not_called() new_conn.assert_not_called()
parsed_url = urllib.parse.urlparse(self.default_balancer_url) parsed_url = urllib.parse.urlparse(self.default_balancer_zope_url)
# check that we have an open file for the ip connection # check that we have an open file for the ip connection
self.assertTrue([ self.assertTrue([
...@@ -538,10 +595,10 @@ class TestServerTLSEmbeddedCaucase(BalancerTestCase): ...@@ -538,10 +595,10 @@ class TestServerTLSEmbeddedCaucase(BalancerTestCase):
return crypto_cert return crypto_cert
def test_certificate_validates_with_caucase_ca(self) -> None: def test_certificate_validates_with_caucase_ca(self) -> None:
requests.get(self.default_balancer_url, verify=self._getCaucaseCACertificatePath()) requests.get(self.default_balancer_zope_url, verify=self._getCaucaseCACertificatePath())
def test_certificate_renewal(self) -> None: def test_certificate_renewal(self) -> None:
balancer_parsed_url = urllib.parse.urlparse(self.default_balancer_url) balancer_parsed_url = urllib.parse.urlparse(self.default_balancer_zope_url)
certificate_before_renewal = self._getServerCertificate( certificate_before_renewal = self._getServerCertificate(
balancer_parsed_url.hostname, balancer_parsed_url.hostname,
balancer_parsed_url.port) balancer_parsed_url.port)
...@@ -640,7 +697,7 @@ class TestServerTLSCSRTemplateParameter(TestServerTLSExternalCaucase): ...@@ -640,7 +697,7 @@ class TestServerTLSCSRTemplateParameter(TestServerTLSExternalCaucase):
def test_certificate_validates_with_caucase_ca(self) -> None: def test_certificate_validates_with_caucase_ca(self) -> None:
super().test_certificate_validates_with_caucase_ca() super().test_certificate_validates_with_caucase_ca()
balancer_parsed_url = urllib.parse.urlparse(self.default_balancer_url) balancer_parsed_url = urllib.parse.urlparse(self.default_balancer_zope_url)
cert = self._getServerCertificate( cert = self._getServerCertificate(
balancer_parsed_url.hostname, balancer_parsed_url.hostname,
balancer_parsed_url.port, balancer_parsed_url.port,
...@@ -655,6 +712,8 @@ class ContentTypeHTTPServer(ManagedHTTPServer): ...@@ -655,6 +712,8 @@ class ContentTypeHTTPServer(ManagedHTTPServer):
For example when requested http://host/text/plain it will reply For example when requested http://host/text/plain it will reply
with Content-Type: text/plain header. with Content-Type: text/plain header.
This actually uses a URL like this to support zope style virtual host:
GET /VirtualHostBase/https/{host}/VirtualHostRoot/text/plain
The body is always "OK" The body is always "OK"
""" """
...@@ -665,7 +724,7 @@ class ContentTypeHTTPServer(ManagedHTTPServer): ...@@ -665,7 +724,7 @@ class ContentTypeHTTPServer(ManagedHTTPServer):
if self.path == '/': if self.path == '/':
self.send_header("Content-Length", '0') self.send_header("Content-Length", '0')
return self.end_headers() return self.end_headers()
content_type = self.path[1:] content_type = '/'.join(self.path.split('/')[5:])
body = b"OK" body = b"OK"
self.send_header("Content-Type", content_type) self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(len(body))) self.send_header("Content-Length", str(len(body)))
...@@ -707,7 +766,7 @@ class TestContentEncoding(BalancerTestCase): ...@@ -707,7 +766,7 @@ class TestContentEncoding(BalancerTestCase):
'application/font-woff2', 'application/font-woff2',
'application/x-font-opentype', 'application/x-font-opentype',
'application/wasm',): 'application/wasm',):
resp = requests.get(urllib.parse.urljoin(self.default_balancer_url, content_type), verify=False) resp = requests.get(urllib.parse.urljoin(self.default_balancer_zope_url, content_type), verify=False)
self.assertEqual(resp.headers['Content-Type'], content_type) self.assertEqual(resp.headers['Content-Type'], content_type)
self.assertEqual( self.assertEqual(
resp.headers.get('Content-Encoding'), resp.headers.get('Content-Encoding'),
...@@ -716,7 +775,7 @@ class TestContentEncoding(BalancerTestCase): ...@@ -716,7 +775,7 @@ class TestContentEncoding(BalancerTestCase):
self.assertEqual(resp.text, 'OK') self.assertEqual(resp.text, 'OK')
def test_no_gzip_encoding(self) -> None: def test_no_gzip_encoding(self) -> None:
resp = requests.get(urllib.parse.urljoin(self.default_balancer_url, '/image/png'), verify=False) resp = requests.get(urllib.parse.urljoin(self.default_balancer_zope_url, '/image/png'), verify=False)
self.assertNotIn('Content-Encoding', resp.headers) self.assertNotIn('Content-Encoding', resp.headers)
self.assertEqual(resp.text, 'OK') self.assertEqual(resp.text, 'OK')
...@@ -806,7 +865,19 @@ class TestServerTLSProvidedCertificate(BalancerTestCase): ...@@ -806,7 +865,19 @@ class TestServerTLSProvidedCertificate(BalancerTestCase):
def _getInstanceParameterDict(cls) -> dict: def _getInstanceParameterDict(cls) -> dict:
server_caucase = cls.getManagedResource('server_caucase', CaucaseService) server_caucase = cls.getManagedResource('server_caucase', CaucaseService)
server_certificate = cls.getManagedResource('server_certificate', CaucaseCertificate) server_certificate = cls.getManagedResource('server_certificate', CaucaseCertificate)
server_certificate.request(cls._ipv4_address, server_caucase) # Add all IPs of the computer in SubjectAlternativeName, we don't
# know what will be the IP of the balancer partition.
with sqlite3.connect(cls.slap._proxy_database) as db:
ip_address_list = [
x509.IPAddress(ipaddress.ip_address(r)) for (r, ) in db.execute(
f"SELECT address FROM partition_network{DB_VERSION}").fetchall()
]
assert ip_address_list
server_certificate.request(
cls.__name__,
server_caucase,
x509.SubjectAlternativeName(ip_address_list))
parameter_dict = super()._getInstanceParameterDict() parameter_dict = super()._getInstanceParameterDict()
with open(server_certificate.cert_file) as f: with open(server_certificate.cert_file) as f:
parameter_dict['ssl']['cert'] = f.read() parameter_dict['ssl']['cert'] = f.read()
...@@ -816,7 +887,7 @@ class TestServerTLSProvidedCertificate(BalancerTestCase): ...@@ -816,7 +887,7 @@ class TestServerTLSProvidedCertificate(BalancerTestCase):
def test_certificate_validates_with_provided_ca(self) -> None: def test_certificate_validates_with_provided_ca(self) -> None:
server_certificate = self.getManagedResource("server_certificate", CaucaseCertificate) server_certificate = self.getManagedResource("server_certificate", CaucaseCertificate)
requests.get(self.default_balancer_url, verify=server_certificate.ca_crt_file) requests.get(self.default_balancer_zope_url, verify=server_certificate.ca_crt_file)
class TestClientTLS(BalancerTestCase): class TestClientTLS(BalancerTestCase):
...@@ -862,7 +933,7 @@ class TestClientTLS(BalancerTestCase): ...@@ -862,7 +933,7 @@ class TestClientTLS(BalancerTestCase):
# the client certificate in "remote-user" header # the client certificate in "remote-user" header
def _make_request() -> dict: def _make_request() -> dict:
return requests.get( return requests.get(
self.default_balancer_url, self.default_balancer_zope_url,
cert=(client_certificate.cert_file, client_certificate.key_file), cert=(client_certificate.cert_file, client_certificate.key_file),
verify=False, verify=False,
).json() ).json()
......
...@@ -60,15 +60,8 @@ class TestPublishedURLIsReachableMixin: ...@@ -60,15 +60,8 @@ class TestPublishedURLIsReachableMixin:
"""Mixin that checks that default page of ERP5 is reachable. """Mixin that checks that default page of ERP5 is reachable.
""" """
def _checkERP5IsReachable(self, base_url, site_id, verify): @contextlib.contextmanager
# We access ERP5 trough a "virtual host", which should make def requestSession(self, base_url):
# ERP5 produce URLs using https://virtual-host-name:1234/virtual_host_root
# as base.
virtual_host_url = urllib.parse.urljoin(
base_url,
'/VirtualHostBase/https/virtual-host-name:1234/{}/VirtualHostRoot/_vh_virtual_host_root/'
.format(site_id))
# What happens is that instantiation just create the services, but does not # What happens is that instantiation just create the services, but does not
# wait for ERP5 to be initialized. When this test run ERP5 instance is # wait for ERP5 to be initialized. When this test run ERP5 instance is
# instantiated, but zope is still busy creating the site and haproxy replies # instantiated, but zope is still busy creating the site and haproxy replies
...@@ -84,7 +77,32 @@ class TestPublishedURLIsReachableMixin: ...@@ -84,7 +77,32 @@ class TestPublishedURLIsReachableMixin:
total=20, total=20,
backoff_factor=.5, backoff_factor=.5,
status_forcelist=(404, 500, 503)))) status_forcelist=(404, 500, 503))))
yield session
def _checkERP5IsReachableWithVirtualHost(self, url, verify):
with self.requestSession(urllib.parse.urljoin(url, '/')) as session:
r = session.get(url, verify=verify, allow_redirects=True)
# access on / are redirected to login form
self.assertTrue(r.url.endswith('/login_form'))
self.assertEqual(r.status_code, requests.codes.ok)
self.assertIn("ERP5", r.text)
# host header is used in redirected URL. The URL is always https
r = session.get(url, verify=verify, allow_redirects=False, headers={'Host': 'www.example.com'})
self.assertEqual(r.headers.get('Location'), 'https://www.example.com/login_form')
r = session.get(url, verify=verify, allow_redirects=False, headers={'Host': 'www.example.com:1234'})
self.assertEqual(r.headers.get('Location'), 'https://www.example.com:1234/login_form')
def _checkERP5IsReachableWithoutVirtualHost(self, base_url, site_id, verify):
# We access ERP5 trough a "virtual host", which should make
# ERP5 produce URLs using https://virtual-host-name:1234/virtual_host_root
# as base.
virtual_host_url = urllib.parse.urljoin(
base_url,
'/VirtualHostBase/https/virtual-host-name:1234/{}/VirtualHostRoot/_vh_virtual_host_root/'
.format(site_id))
with self.requestSession(base_url) as session:
r = session.get(virtual_host_url, verify=verify, allow_redirects=False) r = session.get(virtual_host_url, verify=verify, allow_redirects=False)
self.assertEqual(r.status_code, requests.codes.found) self.assertEqual(r.status_code, requests.codes.found)
# access on / are redirected to login form, with virtual host preserved # access on / are redirected to login form, with virtual host preserved
...@@ -119,7 +137,7 @@ class TestPublishedURLIsReachableMixin: ...@@ -119,7 +137,7 @@ class TestPublishedURLIsReachableMixin:
"""Tests the IPv6 URL published by the root partition is reachable. """Tests the IPv6 URL published by the root partition is reachable.
""" """
param_dict = self.getRootPartitionConnectionParameterDict() param_dict = self.getRootPartitionConnectionParameterDict()
self._checkERP5IsReachable( self._checkERP5IsReachableWithoutVirtualHost(
param_dict['family-default-v6'], param_dict['family-default-v6'],
param_dict['site-id'], param_dict['site-id'],
self._getCaucaseServiceCACertificate(), self._getCaucaseServiceCACertificate(),
...@@ -129,7 +147,7 @@ class TestPublishedURLIsReachableMixin: ...@@ -129,7 +147,7 @@ class TestPublishedURLIsReachableMixin:
"""Tests the IPv4 URL published by the root partition is reachable. """Tests the IPv4 URL published by the root partition is reachable.
""" """
param_dict = self.getRootPartitionConnectionParameterDict() param_dict = self.getRootPartitionConnectionParameterDict()
self._checkERP5IsReachable( self._checkERP5IsReachableWithoutVirtualHost(
param_dict['family-default'], param_dict['family-default'],
param_dict['site-id'], param_dict['site-id'],
self._getCaucaseServiceCACertificate(), self._getCaucaseServiceCACertificate(),
...@@ -139,9 +157,8 @@ class TestPublishedURLIsReachableMixin: ...@@ -139,9 +157,8 @@ class TestPublishedURLIsReachableMixin:
"""Tests the frontend URL published by the root partition is reachable. """Tests the frontend URL published by the root partition is reachable.
""" """
param_dict = self.getRootPartitionConnectionParameterDict() param_dict = self.getRootPartitionConnectionParameterDict()
self._checkERP5IsReachable( self._checkERP5IsReachableWithVirtualHost(
param_dict['url-frontend-default'], param_dict['url-frontend-default'],
param_dict['site-id'],
self._getCaucaseServiceCACertificate(), self._getCaucaseServiceCACertificate(),
) )
...@@ -157,9 +174,8 @@ class TestDefaultParameters(ERP5InstanceTestCase, TestPublishedURLIsReachableMix ...@@ -157,9 +174,8 @@ class TestDefaultParameters(ERP5InstanceTestCase, TestPublishedURLIsReachableMix
'.installed-switch-softwaretype.cfg')) as f: '.installed-switch-softwaretype.cfg')) as f:
installed = zc.buildout.configparser.parse(f, 'installed') installed = zc.buildout.configparser.parse(f, 'installed')
self.assertEqual( self.assertEqual(
installed['request-frontend-default']['config-type'], 'zope') installed['request-frontend-default']['config-type'], '')
self.assertEqual( self.assertNotIn('config-path', installed['request-frontend-default'])
installed['request-frontend-default']['config-path'], '/erp5')
self.assertEqual( self.assertEqual(
installed['request-frontend-default']['config-authenticate-to-backend'], 'true') installed['request-frontend-default']['config-authenticate-to-backend'], 'true')
self.assertEqual(installed['request-frontend-default']['shared'], 'true') self.assertEqual(installed['request-frontend-default']['shared'], 'true')
...@@ -310,8 +326,8 @@ class TestBalancerPorts(ERP5InstanceTestCase): ...@@ -310,8 +326,8 @@ class TestBalancerPorts(ERP5InstanceTestCase):
# frontend pointing to this port. # frontend pointing to this port.
self.assertEqual(urllib.parse.urlparse(param_dict['family-family1']).port, 2152) self.assertEqual(urllib.parse.urlparse(param_dict['family-family1']).port, 2152)
self.assertEqual(urllib.parse.urlparse(param_dict['family-family1-v6']).port, 2152) self.assertEqual(urllib.parse.urlparse(param_dict['family-family1-v6']).port, 2152)
self.assertEqual(urllib.parse.urlparse(param_dict['family-family2']).port, 2154) self.assertEqual(urllib.parse.urlparse(param_dict['family-family2']).port, 2155)
self.assertEqual(urllib.parse.urlparse(param_dict['family-family2-v6']).port, 2154) self.assertEqual(urllib.parse.urlparse(param_dict['family-family2-v6']).port, 2155)
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
...@@ -390,6 +406,8 @@ class TestBalancerPortsStable(ERP5InstanceTestCase): ...@@ -390,6 +406,8 @@ class TestBalancerPortsStable(ERP5InstanceTestCase):
def test_same_balancer_ports_when_adding_zopes_or_frontends(self): def test_same_balancer_ports_when_adding_zopes_or_frontends(self):
param_dict_before = self.getRootPartitionConnectionParameterDict() param_dict_before = self.getRootPartitionConnectionParameterDict()
balancer_param_dict_before = json.loads(
self.getComputerPartition('balancer').getConnectionParameter('_'))
# re-request with one more frontend and one more backend, that are before # re-request with one more frontend and one more backend, that are before
# the existing ones when sorting alphabetically # the existing ones when sorting alphabetically
...@@ -410,10 +428,15 @@ class TestBalancerPortsStable(ERP5InstanceTestCase): ...@@ -410,10 +428,15 @@ class TestBalancerPortsStable(ERP5InstanceTestCase):
rerequest() rerequest()
self.slap.waitForInstance(max_retry=10) self.slap.waitForInstance(max_retry=10)
param_dict_after = json.loads(rerequest().getConnectionParameterDict()['_']) param_dict_after = json.loads(rerequest().getConnectionParameterDict()['_'])
balancer_param_dict_after = json.loads(
self.getComputerPartition('balancer').getConnectionParameter('_'))
self.assertEqual(param_dict_before['family-zzz-v6'], param_dict_after['family-zzz-v6']) 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.assertEqual(param_dict_before['url-frontend-zzz'], param_dict_after['url-frontend-zzz'])
self.assertEqual(balancer_param_dict_before['url-backend-zzz'], balancer_param_dict_after['url-backend-zzz'])
self.assertNotEqual(param_dict_before['family-zzz-v6'], param_dict_after['family-aaa-v6']) 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']) self.assertNotEqual(param_dict_before['url-frontend-zzz'], param_dict_after['url-frontend-aaa'])
self.assertNotEqual(balancer_param_dict_before['url-backend-zzz'], balancer_param_dict_after['url-backend-aaa'])
class TestSeleniumTestRunner(ERP5InstanceTestCase, TestPublishedURLIsReachableMixin): class TestSeleniumTestRunner(ERP5InstanceTestCase, TestPublishedURLIsReachableMixin):
...@@ -476,14 +499,15 @@ class TestDisableTestRunner(ERP5InstanceTestCase, TestPublishedURLIsReachableMix ...@@ -476,14 +499,15 @@ class TestDisableTestRunner(ERP5InstanceTestCase, TestPublishedURLIsReachableMix
self.assertNotIn('runTestSuite', bin_programs) self.assertNotIn('runTestSuite', bin_programs)
def test_no_haproxy_testrunner_port(self): def test_no_haproxy_testrunner_port(self):
# Haproxy only listen on two ports, there is no haproxy ports allocated for test runner # Haproxy only listen on two ports for frontend, two ports for legacy entry points
# and there is no haproxy ports allocated for test runner
with self.slap.instance_supervisor_rpc as supervisor: with self.slap.instance_supervisor_rpc as supervisor:
all_process_info = supervisor.getAllProcessInfo() all_process_info = supervisor.getAllProcessInfo()
process_info, = (p for p in all_process_info if p['name'].startswith('haproxy')) process_info, = (p for p in all_process_info if p['name'].startswith('haproxy'))
haproxy_master_process = psutil.Process(process_info['pid']) haproxy_master_process = psutil.Process(process_info['pid'])
haproxy_worker_process, = haproxy_master_process.children() haproxy_worker_process, = haproxy_master_process.children()
self.assertEqual( self.assertEqual(
sorted([socket.AF_INET, socket.AF_INET6]), sorted([socket.AF_INET, socket.AF_INET6, socket.AF_INET, socket.AF_INET6]),
sorted( sorted(
c.family c.family
for c in haproxy_worker_process.connections() for c in haproxy_worker_process.connections()
...@@ -1356,10 +1380,13 @@ class TestFrontend(ERP5InstanceTestCase): ...@@ -1356,10 +1380,13 @@ class TestFrontend(ERP5InstanceTestCase):
}, },
"web": { "web": {
"family": "web", "family": "web",
"port-base": 2300,
}, },
"activities": { "activities": {
# this family will not have frontend # this family will not have frontend
"family": "activities" "family": "activities",
"port-base": 2400,
}, },
}, },
"frontend": { "frontend": {
...@@ -1368,7 +1395,7 @@ class TestFrontend(ERP5InstanceTestCase): ...@@ -1368,7 +1395,7 @@ class TestFrontend(ERP5InstanceTestCase):
}, },
"website": { "website": {
"zope-family": "web", "zope-family": "web",
"internal-path": "/%(site-id)s/web_site_module/my_website/", "internal-path": "/%(site-id)s/web_site_module/my_website",
"instance-parameters": { "instance-parameters": {
# some extra frontend parameters # some extra frontend parameters
"enable_cache": "true", "enable_cache": "true",
...@@ -1396,19 +1423,20 @@ class TestFrontend(ERP5InstanceTestCase): ...@@ -1396,19 +1423,20 @@ class TestFrontend(ERP5InstanceTestCase):
def test_request_parameters(self): def test_request_parameters(self):
param_dict = self.getRootPartitionConnectionParameterDict() param_dict = self.getRootPartitionConnectionParameterDict()
balancer_param_dict = json.loads(
self.getComputerPartition('balancer').getConnectionParameter('_'))
with open(os.path.join(self.computer_partition_root_path, with open(os.path.join(self.computer_partition_root_path,
'.installed-switch-softwaretype.cfg')) as f: '.installed-switch-softwaretype.cfg')) as f:
installed = zc.buildout.configparser.parse(f, 'installed') installed = zc.buildout.configparser.parse(f, 'installed')
self.assertEqual( self.assertEqual(
installed['request-frontend-backoffice']['config-type'], 'zope') installed['request-frontend-backoffice']['config-type'], '')
self.assertEqual( self.assertEqual(
installed['request-frontend-backoffice']['shared'], 'true') installed['request-frontend-backoffice']['shared'], 'true')
self.assertEqual( self.assertEqual(
installed['request-frontend-backoffice']['config-url'], installed['request-frontend-backoffice']['config-url'],
param_dict['family-default-v6']) balancer_param_dict['url-backend-backoffice'])
self.assertEqual( self.assertNotIn('config-path', installed['request-frontend-backoffice'])
installed['request-frontend-backoffice']['config-path'], '/erp5')
self.assertEqual( self.assertEqual(
installed['request-frontend-backoffice']['sla-computer_guid'], installed['request-frontend-backoffice']['sla-computer_guid'],
'COMP-1234') 'COMP-1234')
...@@ -1421,7 +1449,7 @@ class TestFrontend(ERP5InstanceTestCase): ...@@ -1421,7 +1449,7 @@ class TestFrontend(ERP5InstanceTestCase):
param_dict['url-frontend-backoffice']) param_dict['url-frontend-backoffice'])
self.assertEqual( self.assertEqual(
installed['request-frontend-website']['config-type'], 'zope') installed['request-frontend-website']['config-type'], '')
# no SLA by default # no SLA by default
self.assertFalse([k for k in installed['request-frontend-website'] if k.startswith('sla-')]) self.assertFalse([k for k in installed['request-frontend-website'] if k.startswith('sla-')])
# instance parameters are propagated # instance parameters are propagated
...@@ -1429,10 +1457,8 @@ class TestFrontend(ERP5InstanceTestCase): ...@@ -1429,10 +1457,8 @@ class TestFrontend(ERP5InstanceTestCase):
installed['request-frontend-website']['config-enable_cache'], 'true') installed['request-frontend-website']['config-enable_cache'], 'true')
self.assertEqual( self.assertEqual(
installed['request-frontend-website']['config-url'], installed['request-frontend-website']['config-url'],
param_dict['family-web-v6']) balancer_param_dict['url-backend-website'])
self.assertEqual( self.assertNotIn('config-path', installed['request-frontend-website'])
installed['request-frontend-website']['config-path'],
'/erp5/web_site_module/my_website/')
self.assertEqual( self.assertEqual(
installed['request-frontend-website']['connection-secure_access'], installed['request-frontend-website']['connection-secure_access'],
param_dict['url-frontend-website']) param_dict['url-frontend-website'])
...@@ -1440,6 +1466,32 @@ class TestFrontend(ERP5InstanceTestCase): ...@@ -1440,6 +1466,32 @@ class TestFrontend(ERP5InstanceTestCase):
# no frontend was requested for activities family # no frontend was requested for activities family
self.assertNotIn('request-frontend-activities', installed) self.assertNotIn('request-frontend-activities', installed)
self.assertNotIn('url-frontend-activities', param_dict) self.assertNotIn('url-frontend-activities', param_dict)
self.assertNotIn('url-backend-activities', balancer_param_dict)
def test_path_virtualhost(self):
balancer_param_dict = json.loads(
self.getComputerPartition('balancer').getConnectionParameter('_'))
found_line = False
retries = 10
while retries:
requests.get(balancer_param_dict['url-backend-website'], verify=False)
for logfile in glob.glob(os.path.join(self.getComputerPartitionPath('zope-web'), 'var/log/*Z2.log')):
with open(logfile) as f:
for line in f:
if 'GET /VirtualHost' in line:
found_line = True
break
if found_line:
break
time.sleep(1)
retries = retries - 1
self.assertTrue(found_line)
percent_encoded_netloc = urllib.parse.quote(
urllib.parse.urlparse(
balancer_param_dict['url-backend-website']).netloc)
self.assertIn(
f'/VirtualHostBase/https/{percent_encoded_netloc}/erp5/web_site_module/my_website/VirtualHostRoot/ HTTP', line)
class TestDefaultFrontendWithZopePartitionDict(ERP5InstanceTestCase): class TestDefaultFrontendWithZopePartitionDict(ERP5InstanceTestCase):
...@@ -1468,12 +1520,15 @@ class TestDefaultFrontendWithZopePartitionDict(ERP5InstanceTestCase): ...@@ -1468,12 +1520,15 @@ class TestDefaultFrontendWithZopePartitionDict(ERP5InstanceTestCase):
def test_frontend_requested(self): def test_frontend_requested(self):
param_dict = self.getRootPartitionConnectionParameterDict() param_dict = self.getRootPartitionConnectionParameterDict()
balancer_param_dict = json.loads(
self.getComputerPartition('balancer').getConnectionParameter('_'))
with open(os.path.join(self.computer_partition_root_path, with open(os.path.join(self.computer_partition_root_path,
'.installed-switch-softwaretype.cfg')) as f: '.installed-switch-softwaretype.cfg')) as f:
installed = zc.buildout.configparser.parse(f, 'installed') installed = zc.buildout.configparser.parse(f, 'installed')
self.assertEqual( self.assertEqual(
installed['request-frontend-default']['config-url'], installed['request-frontend-default']['config-url'],
param_dict['family-backoffice-v6']) balancer_param_dict['url-backend-default'])
requests.get( requests.get(
param_dict['url-frontend-default'], param_dict['url-frontend-default'],
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
# not need these here). # not need these here).
[template-erp5] [template-erp5]
filename = instance-erp5.cfg.in filename = instance-erp5.cfg.in
md5sum = ba46a66da1c834df14a80a20b21e4a96 md5sum = 6db19ee819a960a34012308e29c5bbfb
[template-balancer] [template-balancer]
filename = instance-balancer.cfg.in filename = instance-balancer.cfg.in
......
...@@ -435,6 +435,7 @@ return = ...@@ -435,6 +435,7 @@ return =
{% endfor -%} {% endfor -%}
{% do monitor_base_url_dict.__setitem__('request-balancer', '${' ~ 'request-balancer' ~ ':connection-monitor-base-url}') -%} {% do monitor_base_url_dict.__setitem__('request-balancer', '${' ~ 'request-balancer' ~ ':connection-monitor-base-url}') -%}
config-zope-family-dict = {{ dumps(zope_family_parameter_dict) }} config-zope-family-dict = {{ dumps(zope_family_parameter_dict) }}
config-frontend-parameter-dict = {{ dumps({}) }}
config-tcpv4-port = {{ dumps(balancer_dict.get('tcpv4-port', 2150)) }} config-tcpv4-port = {{ dumps(balancer_dict.get('tcpv4-port', 2150)) }}
{% for zope_section_id, name in zope_address_list_id_dict.items() -%} {% for zope_section_id, name in zope_address_list_id_dict.items() -%}
config-{{ name }} = {{ ' ${' ~ zope_section_id ~ ':connection-zope-address-list}' }} config-{{ name }} = {{ ' ${' ~ zope_section_id ~ ':connection-zope-address-list}' }}
......
...@@ -74,7 +74,7 @@ md5sum = 55463b0abdbe0118ef1c27e6b71c3324 ...@@ -74,7 +74,7 @@ md5sum = 55463b0abdbe0118ef1c27e6b71c3324
[template-erp5] [template-erp5]
filename = instance-erp5.cfg.in filename = instance-erp5.cfg.in
md5sum = 855fdc8ed0b2671f9e2c74d127cc6858 md5sum = ae9c380ae04dde4f20e139c66ef7c22a
[template-zeo] [template-zeo]
filename = instance-zeo.cfg.in filename = instance-zeo.cfg.in
...@@ -90,11 +90,11 @@ md5sum = 6178ba7b42848f9e2412ab898a7b026c ...@@ -90,11 +90,11 @@ md5sum = 6178ba7b42848f9e2412ab898a7b026c
[template-balancer] [template-balancer]
filename = instance-balancer.cfg.in filename = instance-balancer.cfg.in
md5sum = 42cb68905f92e7df38cc5c64b94be3de md5sum = 0fad9497da12ed0186dca5236c23f3a7
[template-haproxy-cfg] [template-haproxy-cfg]
filename = haproxy.cfg.in filename = haproxy.cfg.in
md5sum = 9988a14c4108e3bce3f871e34673cdd5 md5sum = 2cd76971b64b0bf7771978ad07bfc2e5
[template-rsyslogd-cfg] [template-rsyslogd-cfg]
filename = rsyslogd.cfg.in filename = rsyslogd.cfg.in
......
...@@ -18,11 +18,11 @@ ...@@ -18,11 +18,11 @@
# "stats-socket": "<file_path>", # "stats-socket": "<file_path>",
# #
# # IPv4 to listen on # # IPv4 to listen on
# # All backends from `backend-dict` will listen on this IP. # # All frontends from `frontend-dict` will listen on this IP.
# "ipv4": "0.0.0.0", # "ipv4": "0.0.0.0",
# #
# # IPv6 to listen on # # IPv6 to listen on
# # All backends from `backend-dict` will listen on this IP. # # All frontends from `frontend-dict` will listen on this IP.
# "ipv6": "::1", # "ipv6": "::1",
# #
# # Certificate and key in PEM format. All ports will serve TLS using # # Certificate and key in PEM format. All ports will serve TLS using
...@@ -41,34 +41,59 @@ ...@@ -41,34 +41,59 @@
# # Path to use for HTTP health check on backends from `backend-dict`. # # Path to use for HTTP health check on backends from `backend-dict`.
# "server-check-path": "/", # "server-check-path": "/",
# #
# # The mapping of frontend, keyed by frontend name
# "frontend-dict": {
# "frontend-default": {
# "port": 8080,
# "client-cert-required": False,
# "backend-name": "family-default",
# "request-path-prepend": "/erp5",
# }
# "legacy-frontend-family-secure": {
# "port": 8000,
# "client-cert-required": False,
# "backend-name": "family-secure",
# "request-path-prepend": None, # None means do not rewrite the request path
# }
# "legacy-frontend-family-default": {
# "port": 8002,
# "client-cert-required": False,
# "backend-name": "family-default",
# "request-path-prepend": None, # None means do not rewrite the request path
# }
# }
# # 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 # "timeout": None, # in seconds
# True, # ssl_required bool # "backend-list": [
# None, # timeout (in seconds) int | None # [
# [ # backends
# '10.0.0.10:8001', # netloc str # '10.0.0.10:8001', # netloc str
# 1, # max_connection_count int # 1, # max_connection_count int
# False, # is_web_dav bool # False, # is_web_dav bool
# ], # ]
# ), # ]
# }, # },
# "family-default": { # "family-default": {
# ( 8002, # port int # "timeout": None, # in seconds
# False, # ssl_required bool # "backend-list": [
# None, # timeout (in seconds) int | None # [
# [ # backends
# '10.0.0.10:8003', # netloc str # '10.0.0.10:8003', # netloc str
# 1, # max_connection_count int # 1, # max_connection_count int
# False, # is_web_dav bool # False, # is_web_dav bool
# ], # ],
# ), # [
# '10.0.0.10:8004', # netloc str
# 1, # max_connection_count int
# False, # is_web_dav bool
# ],
# ]
# }, # },
# #
# # The mapping of zope paths. # # The mapping of zope paths.
# # This is a Zope specific feature. # # This is a Zope specific feature used only to provide https while running
# # `enable_authentication` has same meaning as for `backend-list`. # # ERP5 "unit test" suite.
# # `enable_authentication` has same meaning as for `backend-dict`.
# "zope-virtualhost-monster-backend-dict": { # "zope-virtualhost-monster-backend-dict": {
# # {(ip, port): ( enable_authentication, {frontend_path: ( internal_url ) }, ) } # # {(ip, port): ( enable_authentication, {frontend_path: ( internal_url ) }, ) }
# ('[::1]', 8004): ( # ('[::1]', 8004): (
...@@ -81,15 +106,20 @@ ...@@ -81,15 +106,20 @@
# } # }
# #
# This sample of `parameter_dict` will make haproxy listening to : # This sample of `parameter_dict` will make haproxy listening to :
# From to `backend-list`: # For "frontend-default":
# For "family-secure": # - 0.0.0.0:8080 redirecting internaly to http://10.0.0.10:8003 or http://10.0.0.10:8004
# - [::1]:8080 redirecting internaly to http://10.0.0.10:8003 or http://10.0.0.10:8004
# accepting requests from any client and rewriting the path to add a Zope rewrite rule
# so that the a request on https://0.0.0.0:8080/path is rewritten to serve a Zope object at
# path /erp5/path , visible as /path.
# For "legacy-frontend-family-secure":
# - 0.0.0.0:8000 redirecting internaly to http://10.0.0.10:8001 and # - 0.0.0.0:8000 redirecting internaly to http://10.0.0.10:8001 and
# - [::1]:8000 redirecting internaly to http://10.0.0.10:8001 # - [::1]:8000 redirecting internaly to http://10.0.0.10:8001
# only accepting requests from clients providing a verified TLS certificate # only accepting requests from clients providing a verified TLS certificate
# emitted by a CA from `ca-cert` and not revoked in `crl`. # emitted by a CA from `ca-cert` and not revoked in `crl`.
# For "family-default": # For "legacy-frontend-family-default":
# - 0.0.0.0:8002 redirecting internaly to http://10.0.0.10:8003 # - 0.0.0.0:8002 redirecting internaly to http://10.0.0.10:8003 or http://10.0.0.10:8004
# - [::1]:8002 redirecting internaly to http://10.0.0.10:8003 # - [::1]:8002 redirecting internaly to http://10.0.0.10:8003 or http://10.0.0.10:8004
# accepting requests from any client. # accepting requests from any client.
# #
# For both families, X-Forwarded-For header will be stripped unless # For both families, X-Forwarded-For header will be stripped unless
...@@ -102,7 +132,7 @@ ...@@ -102,7 +132,7 @@
# with some VirtualHostMonster rewrite rules so zope writes URLs with # with some VirtualHostMonster rewrite rules so zope writes URLs with
# [::1]:8004 as server name. # [::1]:8004 as server name.
# For more details, refer to # For more details, refer to
# https://docs.zope.org/zope2/zope2book/VirtualHosting.html#using-virtualhostroot-and-virtualhostbase-together # https://zope.readthedocs.io/en/latest/zopebook/VirtualHosting.html#using-virtualhostroot-and-virtualhostbase-together
-#} -#}
{% set server_check_path = parameter_dict['server-check-path'] -%} {% set server_check_path = parameter_dict['server-check-path'] -%}
...@@ -148,26 +178,17 @@ defaults ...@@ -148,26 +178,17 @@ 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'])) -%}
listen family_{{ name }} {% for name, frontend in sorted(six.iteritems(parameter_dict['frontend-dict'])) %}
listen {{ 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 frontend['client-cert-required'] else ' optional crt-ignore-err all' ) ~ ' crl-file ' ~ parameter_dict['crl'] %}
{%- else %} {%- else %}
{%- set ssl_auth = '' %} {%- set ssl_auth = '' %}
{%- endif %} {%- endif %}
bind {{ parameter_dict['ipv4'] }}:{{ port }} {{ bind_ssl_crt }} {{ ssl_auth }} bind {{ parameter_dict['ipv4'] }}:{{ frontend['port'] }} {{ bind_ssl_crt }} {{ ssl_auth }}
bind {{ parameter_dict['ipv6'] }}:{{ port }} {{ bind_ssl_crt }} {{ ssl_auth }} bind {{ parameter_dict['ipv6'] }}:{{ frontend['port'] }} {{ bind_ssl_crt }} {{ ssl_auth }}
cookie SERVERID rewrite
http-request set-header X-Balancer-Current-Cookie SERVERID
{% if timeout %}
{#
Apply a slightly longer timeout than the zope timeout so that clients can see the
TimeoutReachedError from zope, that is a bit more informative than the 504 error
page from haproxy.
#}
timeout server {{ timeout + 3 }}s
{%- endif %}
# remove X-Forwarded-For unless client presented a verified certificate # remove X-Forwarded-For unless client presented a verified certificate
http-request del-header X-Forwarded-For unless { ssl_c_verify 0 } { ssl_c_used 1 } http-request del-header X-Forwarded-For unless { ssl_c_verify 0 } { ssl_c_used 1 }
...@@ -175,18 +196,43 @@ listen family_{{ name }} ...@@ -175,18 +196,43 @@ listen family_{{ name }}
http-request del-header Remote-User http-request del-header Remote-User
http-request set-header Remote-User %{+Q}[ssl_c_s_dn(cn)] if { ssl_c_verify 0 } { ssl_c_used 1 } http-request set-header Remote-User %{+Q}[ssl_c_s_dn(cn)] if { ssl_c_verify 0 } { ssl_c_used 1 }
# reject invalid host header before using it in path
http-request deny deny_status 400 if { req.hdr(host) -m sub / }
# logs # logs
capture request header Referer len 512 capture request header Referer len 512
capture request header User-Agent len 512 capture request header User-Agent len 512
log-format "%{+Q}o %{-Q}ci - - [%trg] %r %ST %B %{+Q}[capture.req.hdr(0)] %{+Q}[capture.req.hdr(1)] %Ta" log-format "%{+Q}o %{-Q}ci - - [%trg] %r %ST %B %{+Q}[capture.req.hdr(0)] %{+Q}[capture.req.hdr(1)] %Ta"
{% for outer_prefix, inner_prefix in family_path_routing_dict.get(name, []) + path_routing_list %} {% if frontend['request-path-prepend'] is not none %}
http-request replace-path ^/(.*) /VirtualHostBase/https/%[req.hdr(Host)]{{ frontend['request-path-prepend'] }}/VirtualHostRoot/\1
{% endif %}
{% for outer_prefix, inner_prefix in family_path_routing_dict.get(frontend['backend-name'], []) + path_routing_list %}
{% set outer_prefix = outer_prefix.strip('/') -%} {% set outer_prefix = outer_prefix.strip('/') -%}
http-request replace-path ^(/+VirtualHostBase/+[^/]+/+[^/]+)/+VirtualHostRoot/+{% if outer_prefix %}{{ outer_prefix }}($|/.*){% else %}(.*){% endif %} \1/{{ inner_prefix.strip('/') }}/VirtualHostRoot/{% if outer_prefix %}_vh_{{ outer_prefix.replace('/', '/_vh_') }}{% endif %}\2 http-request replace-path ^(/+VirtualHostBase/+[^/]+/+[^/]+)/+VirtualHostRoot/+{% if outer_prefix %}{{ outer_prefix }}($|/.*){% else %}(.*){% endif %} \1/{{ inner_prefix.strip('/') }}/VirtualHostRoot/{% if outer_prefix %}_vh_{{ outer_prefix.replace('/', '/_vh_') }}{% endif %}\2
{% endfor %} {% endfor %}
use_backend {{ frontend['backend-name'] }}
{% endfor %}
{% for name, backend in sorted(six.iteritems(parameter_dict['backend-dict'])) %}
backend {{ name }}
cookie SERVERID rewrite
http-request set-header X-Balancer-Current-Cookie SERVERID
{% if backend['timeout'] %}
{#
Apply a slightly longer timeout than the zope timeout so that clients can see the
TimeoutReachedError from zope, that is a bit more informative than the 504 error
page from haproxy.
#}
timeout server {{ backend['timeout'] + 3 }}s
{%- endif %}
{% set has_webdav = [] -%} {% set has_webdav = [] -%}
{% for address, connection_count, webdav in backend_list -%} {% for address, connection_count, webdav in backend['backend-list'] -%}
{% if webdav %}{% do has_webdav.append(None) %}{% endif -%} {% if webdav %}{% do has_webdav.append(None) %}{% endif -%}
{% set server_name = name ~ '-' ~ loop.index0 %} {% set server_name = name ~ '-' ~ loop.index0 %}
server {{ server_name }} {{ address }} cookie {{ server_name }} check inter 3s rise 1 fall 2 maxqueue 5 maxconn {{ connection_count }} server {{ server_name }} {{ address }} cookie {{ server_name }} check inter 3s rise 1 fall 2 maxqueue 5 maxconn {{ connection_count }}
......
...@@ -257,7 +257,8 @@ init = ...@@ -257,7 +257,8 @@ init =
port_dict[name] = port port_dict[name] = port
return port return port
haproxy_dict = {} backend_dict = {}
frontend_dict = {}
zope_virtualhost_monster_backend_dict = {} zope_virtualhost_monster_backend_dict = {}
for family_name, parameter_id_list in sorted( for family_name, parameter_id_list in sorted(
six.iteritems(slapparameter_dict['zope-family-dict'])): six.iteritems(slapparameter_dict['zope-family-dict'])):
...@@ -290,17 +291,33 @@ init = ...@@ -290,17 +291,33 @@ init =
# a port for monitoring promise (which port is not important, the promise checks # a port for monitoring promise (which port is not important, the promise checks
# that haproxy is healthy enough to listen on a port) # that haproxy is healthy enough to listen on a port)
options['haproxy-promise-port'] = legacy_port options['haproxy-promise-port'] = legacy_port
haproxy_dict[family_name] = ( frontend_dict['legacy-frontend-' + family_name] = {
legacy_port, 'port': legacy_port,
ssl_authentication, 'client-cert-required': ssl_authentication,
slapparameter_dict['timeout-dict'][family_name], 'backend-name': family_name,
zope_family_address_list, 'request-path-prepend': None,
) }
backend_dict[family_name] = {
'timeout': slapparameter_dict['timeout-dict'][family_name],
'backend-list': zope_family_address_list,
}
external_scheme = 'webdavs' if any(a[2] for a in zope_family_address_list) else 'https' 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] = "{external_scheme}://{ipv4}:{legacy_port}".format(**locals())
self.buildout['publish'][family_name + "-v6"] = "{external_scheme}://[{ipv6}]:{legacy_port}".format(**locals()) self.buildout['publish'][family_name + "-v6"] = "{external_scheme}://[{ipv6}]:{legacy_port}".format(**locals())
options['backend-dict'] = haproxy_dict for frontend_name, frontend in six.iteritems(slapparameter_dict['frontend-parameter-dict']):
frontend_port = get_port('frontend-' + frontend_name)
family_name = frontend['zope-family']
frontend_dict['frontend-' + frontend_name] = {
'port': frontend_port,
'client-cert-required': slapparameter_dict['ssl-authentication-dict'][family_name],
'backend-name': family_name,
'request-path-prepend': frontend['internal-path'],
}
self.buildout['publish']['url-backend-' + frontend_name] = "https://[{ipv6}]:{frontend_port}".format(**locals())
options['backend-dict'] = backend_dict
options['frontend-dict'] = frontend_dict
options['zope-virtualhost-monster-backend-dict'] = zope_virtualhost_monster_backend_dict options['zope-virtualhost-monster-backend-dict'] = zope_virtualhost_monster_backend_dict
if port_dict != previous_port_dict: if port_dict != previous_port_dict:
......
...@@ -388,13 +388,13 @@ return = ...@@ -388,13 +388,13 @@ return =
{% set request_frontend_name = 'request-frontend-' ~ frontend_name -%} {% set request_frontend_name = 'request-frontend-' ~ frontend_name -%}
{% set frontend_software_url = frontend_parameters.get('software-url', 'http://git.erp5.org/gitweb/slapos.git/blob_plain/HEAD:/software/apache-frontend/software.cfg') -%} {% set frontend_software_url = frontend_parameters.get('software-url', 'http://git.erp5.org/gitweb/slapos.git/blob_plain/HEAD:/software/apache-frontend/software.cfg') -%}
{% set frontend_software_type = frontend_parameters.get('software-type', '') -%} {% set frontend_software_type = frontend_parameters.get('software-type', '') -%}
{% do frontend_parameters.__setitem__('internal-path', frontend_parameters.get('internal-path', '/%(site-id)s') % {'site-id': site_id}) %}
{% set frontend_instance_parameters = frontend_parameters.get('instance-parameters', {}) -%} {% set frontend_instance_parameters = frontend_parameters.get('instance-parameters', {}) -%}
{% if frontend_instance_parameters.setdefault('type', 'zope') == 'zope' -%} {% if frontend_instance_parameters.setdefault('type', '') == '' -%}
{% do frontend_instance_parameters.setdefault('authenticate-to-backend', 'true') -%} {% do frontend_instance_parameters.setdefault('authenticate-to-backend', 'true') -%}
{% set zope_family_name = frontend_parameters['zope-family'] -%} {% set zope_family_name = frontend_parameters['zope-family'] -%}
{% do assert(zope_family_name in zope_family_dict, 'Unknown family %s for frontend %s' % (zope_family_name, frontend_name)) -%} {% do assert(zope_family_name in zope_family_dict, 'Unknown family %s for frontend %s' % (zope_family_name, frontend_name)) -%}
{% do frontend_instance_parameters.setdefault('url', '${request-balancer:connection-' ~ zope_family_name ~ '-v6}') -%} {% do frontend_instance_parameters.setdefault('url', '${request-balancer:connection-url-backend-' ~ frontend_name ~ '}') -%}
{% do frontend_instance_parameters.setdefault('path', frontend_parameters.get('internal-path', '/%(site-id)s') % {'site-id': site_id}) -%}
{% endif %} {% endif %}
[{{ request_frontend_name }}] [{{ request_frontend_name }}]
<= request-frontend-base <= request-frontend-base
...@@ -446,6 +446,9 @@ config-allow-redirects = 0 ...@@ -446,6 +446,9 @@ config-allow-redirects = 0
{% do balancer_ret_dict.__setitem__(family + '-test-runner-url-list', False) -%} {% do balancer_ret_dict.__setitem__(family + '-test-runner-url-list', False) -%}
{% endif -%} {% endif -%}
{% endfor -%} {% endfor -%}
{% for frontend_name in frontend_parameter_dict -%}
{% do balancer_ret_dict.__setitem__('url-backend-' ~ frontend_name, False) -%}
{% endfor -%}
{% set balancer_key_config_dict = { {% set balancer_key_config_dict = {
'monitor-passwd': 'monitor-htpasswd:passwd', 'monitor-passwd': 'monitor-htpasswd:passwd',
} -%} } -%}
...@@ -467,6 +470,7 @@ config-allow-redirects = 0 ...@@ -467,6 +470,7 @@ config-allow-redirects = 0
config_key='balancer', config_key='balancer',
config={ config={
'zope-family-dict': zope_family_parameter_dict, 'zope-family-dict': zope_family_parameter_dict,
'frontend-parameter-dict': frontend_parameter_dict,
'ssl-authentication-dict': ssl_authentication_dict, 'ssl-authentication-dict': ssl_authentication_dict,
'timeout-dict': balancer_timeout_dict, 'timeout-dict': balancer_timeout_dict,
'apachedex-promise-threshold': monitor_dict.get('apachedex-promise-threshold', 70), 'apachedex-promise-threshold': monitor_dict.get('apachedex-promise-threshold', 70),
......
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