test.py: Add possibility to run ldap tests from pytest
Add posibility to run ldap tests with pytest. LDAP server will be created for each worker if xdist will be used. For one thread one LDAP server will be used for all tests.
This commit is contained in:
167
test.py
167
test.py
@@ -37,6 +37,7 @@ from abc import ABC, abstractmethod
|
||||
from io import StringIO
|
||||
from scripts import coverage # type: ignore
|
||||
from test.pylib.artifact_registry import ArtifactRegistry
|
||||
from test.pylib.cpp.ldap.prepare_instance import try_something_backoff, can_connect, setup
|
||||
from test.pylib.host_registry import HostRegistry
|
||||
from test.pylib.pool import Pool
|
||||
from test.pylib.s3_proxy import S3ProxyServer
|
||||
@@ -62,72 +63,6 @@ all_modes = {'debug': 'Debug',
|
||||
'coverage': 'Coverage'}
|
||||
debug_modes = {'debug', 'sanitize'}
|
||||
|
||||
LDAP_SERVER_CONFIGURATION_FILE = os.path.join(os.path.dirname(__file__), 'test', 'resource', 'slapd.conf')
|
||||
|
||||
DEFAULT_ENTRIES = [
|
||||
"""dn: dc=example,dc=com
|
||||
objectClass: dcObject
|
||||
objectClass: organization
|
||||
dc: example
|
||||
o: Example
|
||||
description: Example directory.
|
||||
""",
|
||||
"""dn: cn=root,dc=example,dc=com
|
||||
objectClass: organizationalRole
|
||||
cn: root
|
||||
description: Directory manager.
|
||||
""",
|
||||
"""dn: ou=People,dc=example,dc=com
|
||||
objectClass: organizationalUnit
|
||||
ou: People
|
||||
description: Our people.
|
||||
""",
|
||||
"""# Default superuser for Scylla
|
||||
dn: uid=cassandra,ou=People,dc=example,dc=com
|
||||
objectClass: organizationalPerson
|
||||
objectClass: uidObject
|
||||
cn: cassandra
|
||||
ou: People
|
||||
sn: cassandra
|
||||
userid: cassandra
|
||||
userPassword: cassandra
|
||||
""",
|
||||
"""dn: uid=jsmith,ou=People,dc=example,dc=com
|
||||
objectClass: organizationalPerson
|
||||
objectClass: uidObject
|
||||
cn: Joe Smith
|
||||
ou: People
|
||||
sn: Smith
|
||||
userid: jsmith
|
||||
userPassword: joeisgreat
|
||||
""",
|
||||
"""dn: uid=jdoe,ou=People,dc=example,dc=com
|
||||
objectClass: organizationalPerson
|
||||
objectClass: uidObject
|
||||
cn: John Doe
|
||||
ou: People
|
||||
sn: Doe
|
||||
userid: jdoe
|
||||
userPassword: pa55w0rd
|
||||
""",
|
||||
"""dn: cn=role1,dc=example,dc=com
|
||||
objectClass: groupOfUniqueNames
|
||||
cn: role1
|
||||
uniqueMember: uid=jsmith,ou=People,dc=example,dc=com
|
||||
uniqueMember: uid=cassandra,ou=People,dc=example,dc=com
|
||||
""",
|
||||
"""dn: cn=role2,dc=example,dc=com
|
||||
objectClass: groupOfUniqueNames
|
||||
cn: role2
|
||||
uniqueMember: uid=cassandra,ou=People,dc=example,dc=com
|
||||
""",
|
||||
"""dn: cn=role3,dc=example,dc=com
|
||||
objectClass: groupOfUniqueNames
|
||||
cn: role3
|
||||
uniqueMember: uid=jdoe,ou=People,dc=example,dc=com
|
||||
""",
|
||||
]
|
||||
|
||||
|
||||
def create_formatter(*decorators) -> Callable[[Any], str]:
|
||||
"""Return a function which decorates its argument with the given
|
||||
@@ -975,37 +910,6 @@ class BoostTest(Test):
|
||||
print(read_log(self.log_filename))
|
||||
|
||||
|
||||
def can_connect(address, family=socket.AF_INET):
|
||||
s = socket.socket(family)
|
||||
try:
|
||||
s.connect(address)
|
||||
return True
|
||||
except OSError as e:
|
||||
if 'AF_UNIX path too long' in str(e):
|
||||
raise OSError(e.errno, "{} ({})".format(str(e), address)) from None
|
||||
else:
|
||||
return False
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def try_something_backoff(something):
|
||||
sleep_time = 0.05
|
||||
while not something():
|
||||
if sleep_time > 30:
|
||||
return False
|
||||
time.sleep(sleep_time)
|
||||
sleep_time *= 2
|
||||
return True
|
||||
|
||||
def make_saslauthd_conf(port, instance_path):
|
||||
"""Creates saslauthd.conf with appropriate contents under instance_path. Returns the path to the new file."""
|
||||
saslauthd_conf_path = os.path.join(instance_path, 'saslauthd.conf')
|
||||
with open(saslauthd_conf_path, 'w') as f:
|
||||
f.write('ldap_servers: ldap://localhost:{}\nldap_search_base: dc=example,dc=com'.format(port))
|
||||
return saslauthd_conf_path
|
||||
|
||||
|
||||
class LdapTest(BoostTest):
|
||||
"""A unit test which can produce its own XML output, and needs an ldap server"""
|
||||
|
||||
@@ -1013,73 +917,10 @@ class LdapTest(BoostTest):
|
||||
super().__init__(test_no, shortname, args, suite, None, False, None)
|
||||
|
||||
async def setup(self, port, options):
|
||||
instances_root = os.path.join(options.tmpdir, self.mode, 'ldap_instances');
|
||||
instance_path = os.path.join(os.path.abspath(instances_root), str(port))
|
||||
slapd_pid_file = os.path.join(instance_path, 'slapd.pid')
|
||||
saslauthd_socket_path = TemporaryDirectory()
|
||||
os.makedirs(instance_path, exist_ok=True)
|
||||
# This will always fail because it lacks the permissions to read the default slapd data
|
||||
# folder but it does create the instance folder so we don't want to fail here.
|
||||
try:
|
||||
subprocess.check_output(['slaptest', '-f', LDAP_SERVER_CONFIGURATION_FILE, '-F', instance_path],
|
||||
stderr=subprocess.DEVNULL)
|
||||
except:
|
||||
pass
|
||||
# Set up failure injection.
|
||||
proxy_name = 'p{}'.format(port)
|
||||
subprocess.check_output([
|
||||
'toxiproxy-cli', 'c', proxy_name,
|
||||
'--listen', 'localhost:{}'.format(port + 2), '--upstream', 'localhost:{}'.format(port)])
|
||||
# Sever the connection after byte_limit bytes have passed through:
|
||||
instances_root = pathlib.Path(options.tmpdir) / self.mode / 'ldap_instances'
|
||||
byte_limit = options.byte_limit if options.byte_limit else randint(0, 2000)
|
||||
subprocess.check_output(['toxiproxy-cli', 't', 'a', proxy_name, '-t', 'limit_data', '-n', 'limiter',
|
||||
'-a', 'bytes={}'.format(byte_limit)])
|
||||
# Change the data folder in the default config.
|
||||
replace_expression = 's/olcDbDirectory:.*/olcDbDirectory: {}/g'.format(
|
||||
os.path.abspath(instance_path).replace('/', r'\/'))
|
||||
subprocess.check_output(
|
||||
['find', instance_path, '-type', 'f', '-exec', 'sed', '-i', replace_expression, '{}', ';'])
|
||||
# Change the pid file to be kept with the instance.
|
||||
replace_expression = 's/olcPidFile:.*/olcPidFile: {}/g'.format(
|
||||
os.path.abspath(slapd_pid_file).replace('/', r'\/'))
|
||||
subprocess.check_output(
|
||||
['find', instance_path, '-type', 'f', '-exec', 'sed', '-i', replace_expression, '{}', ';'])
|
||||
# Put the test data in.
|
||||
cmd = ['slapadd', '-F', instance_path]
|
||||
subprocess.check_output(
|
||||
cmd, input='\n\n'.join(DEFAULT_ENTRIES).encode('ascii'), stderr=subprocess.STDOUT)
|
||||
# Set up the server.
|
||||
SLAPD_URLS='ldap://:{}/ ldaps://:{}/'.format(port, port + 1)
|
||||
def can_connect_to_slapd():
|
||||
return can_connect(('127.0.0.1', port)) and can_connect(('127.0.0.1', port + 1)) and can_connect(('127.0.0.1', port + 2))
|
||||
def can_connect_to_saslauthd():
|
||||
return can_connect(os.path.join(saslauthd_socket_path.name, 'mux'), socket.AF_UNIX)
|
||||
slapd_proc = subprocess.Popen(['prlimit', '-n1024', 'slapd', '-F', instance_path, '-h', SLAPD_URLS, '-d', '0'])
|
||||
saslauthd_conf_path = make_saslauthd_conf(port, instance_path)
|
||||
test_env = {
|
||||
"SEASTAR_LDAP_PORT" : str(port),
|
||||
"SASLAUTHD_MUX_PATH" : os.path.join(saslauthd_socket_path.name, "mux")
|
||||
}
|
||||
|
||||
saslauthd_proc = subprocess.Popen(
|
||||
['saslauthd', '-d', '-n', '1', '-a', 'ldap', '-O', saslauthd_conf_path, '-m', saslauthd_socket_path.name],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
|
||||
def finalize():
|
||||
slapd_proc.terminate()
|
||||
slapd_proc.wait() # Wait for slapd to remove slapd.pid, so it doesn't race with rmtree below.
|
||||
saslauthd_proc.kill() # Somehow, invoking terminate() here also terminates toxiproxy-server. o_O
|
||||
shutil.rmtree(instance_path)
|
||||
saslauthd_socket_path.cleanup()
|
||||
subprocess.check_output(['toxiproxy-cli', 'd', proxy_name])
|
||||
try:
|
||||
if not try_something_backoff(can_connect_to_slapd):
|
||||
raise Exception('Unable to connect to slapd')
|
||||
if not try_something_backoff(can_connect_to_saslauthd):
|
||||
raise Exception('Unable to connect to saslauthd')
|
||||
except:
|
||||
finalize()
|
||||
raise
|
||||
return finalize, '--byte-limit={}'.format(byte_limit), test_env
|
||||
project_root = pathlib.Path(os.path.dirname(__file__))
|
||||
return setup(project_root=project_root, port=port, instance_root=instances_root, byte_limit=byte_limit)
|
||||
|
||||
|
||||
class CQLApprovalTest(Test):
|
||||
|
||||
0
test/ldap/__init__.py
Normal file
0
test/ldap/__init__.py
Normal file
54
test/ldap/conftest.py
Normal file
54
test/ldap/conftest.py
Normal file
@@ -0,0 +1,54 @@
|
||||
#
|
||||
# Copyright (C) 2025-present ScyllaDB
|
||||
#
|
||||
# SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
|
||||
#
|
||||
import sys
|
||||
from pathlib import PosixPath
|
||||
from random import randint
|
||||
|
||||
import pytest
|
||||
from pytest import Collector
|
||||
|
||||
from test.pylib.cpp.boost.boost_facade import BoostTestFacade
|
||||
from test.pylib.cpp.ldap.prepare_instance import get_env_manager
|
||||
from test.pylib.cpp.common_cpp_conftest import collect_items, get_modes_to_run, get_root_path, get_combined_tests
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption('--byte-limit', action="store", default=None, type=int,
|
||||
help="Specific byte limit for failure injection (random by default)")
|
||||
|
||||
|
||||
def pytest_collect_file(file_path: PosixPath, parent: Collector):
|
||||
"""
|
||||
Method triggered automatically by pytest to collect files from a directory.
|
||||
These tests can use BoostFacade since they're Boost tests located in different directory.
|
||||
"""
|
||||
if file_path.suffix == '.cc':
|
||||
return collect_items(file_path, parent, facade=BoostTestFacade(parent.config))
|
||||
|
||||
|
||||
@pytest.hookimpl(wrapper=True)
|
||||
def pytest_runtestloop(session):
|
||||
"""
|
||||
https://docs.pytest.org/en/stable/reference/reference.html#pytest.hookspec.pytest_runtestloop
|
||||
This hook is needed to start the Minio and S3 mock servers before tests. After starting the servers, the default
|
||||
pytest's runtestloop takes control. Finally part is responsible for stopping servers regardless of failure in the tests.
|
||||
"""
|
||||
# make a collection only without starting unnecessary services
|
||||
if session.config.getoption('collectonly'):
|
||||
yield
|
||||
return
|
||||
root_dir = get_root_path(session)
|
||||
temp_dir = root_dir / session.config.getoption('tmpdir')
|
||||
try:
|
||||
byte_limit = session.config.getoption('byte-limit')
|
||||
except ValueError:
|
||||
byte_limit = randint(0, 2000)
|
||||
modes = get_modes_to_run(session)
|
||||
worker_id = 'master'
|
||||
if 'xdist' in sys.modules:
|
||||
worker_id = sys.modules['xdist'].get_xdist_worker_id(session)
|
||||
with get_env_manager(root_dir, temp_dir, worker_id, modes, byte_limit):
|
||||
yield
|
||||
17
test/pylib/cpp/ldap/README.md
Normal file
17
test/pylib/cpp/ldap/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Running tests with pytest
|
||||
|
||||
To run test with pytest execute
|
||||
```bash
|
||||
pytest test/ldap
|
||||
```
|
||||
To execute only one file, provide the path filename
|
||||
```bash
|
||||
pytest test/unit/role_manager_test.cc
|
||||
```
|
||||
Since it's a normal path, autocompletion works in the terminal out of the box.
|
||||
|
||||
To provide a specific mode, use the next parameter `--mode dev`,
|
||||
if parameter isn't provided pytest tries to use `ninja mode_list` to find out the compiled modes.
|
||||
|
||||
Parallel execution is controlled by `pytest-xdist` and the parameter `-n auto`.
|
||||
This command starts tests with the number of workers equal to CPU cores.
|
||||
0
test/pylib/cpp/ldap/__init__.py
Normal file
0
test/pylib/cpp/ldap/__init__.py
Normal file
318
test/pylib/cpp/ldap/prepare_instance.py
Normal file
318
test/pylib/cpp/ldap/prepare_instance.py
Normal file
@@ -0,0 +1,318 @@
|
||||
#
|
||||
# Copyright (C) 2025-present ScyllaDB
|
||||
#
|
||||
# SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from time import sleep
|
||||
from typing import Any, Generator
|
||||
|
||||
LDAP_SERVER_CONFIGURATION_FILE = Path('test', 'resource', 'slapd.conf')
|
||||
DEFAULT_ENTRIES = ["""dn: dc=example,dc=com
|
||||
objectClass: dcObject
|
||||
objectClass: organization
|
||||
dc: example
|
||||
o: Example
|
||||
description: Example directory.
|
||||
""", """dn: cn=root,dc=example,dc=com
|
||||
objectClass: organizationalRole
|
||||
cn: root
|
||||
description: Directory manager.
|
||||
""", """dn: ou=People,dc=example,dc=com
|
||||
objectClass: organizationalUnit
|
||||
ou: People
|
||||
description: Our people.
|
||||
""", """# Default superuser for Scylla
|
||||
dn: uid=cassandra,ou=People,dc=example,dc=com
|
||||
objectClass: organizationalPerson
|
||||
objectClass: uidObject
|
||||
cn: cassandra
|
||||
ou: People
|
||||
sn: cassandra
|
||||
userid: cassandra
|
||||
userPassword: cassandra
|
||||
""", """dn: uid=jsmith,ou=People,dc=example,dc=com
|
||||
objectClass: organizationalPerson
|
||||
objectClass: uidObject
|
||||
cn: Joe Smith
|
||||
ou: People
|
||||
sn: Smith
|
||||
userid: jsmith
|
||||
userPassword: joeisgreat
|
||||
""", """dn: uid=jdoe,ou=People,dc=example,dc=com
|
||||
objectClass: organizationalPerson
|
||||
objectClass: uidObject
|
||||
cn: John Doe
|
||||
ou: People
|
||||
sn: Doe
|
||||
userid: jdoe
|
||||
userPassword: pa55w0rd
|
||||
""", """dn: cn=role1,dc=example,dc=com
|
||||
objectClass: groupOfUniqueNames
|
||||
cn: role1
|
||||
uniqueMember: uid=jsmith,ou=People,dc=example,dc=com
|
||||
uniqueMember: uid=cassandra,ou=People,dc=example,dc=com
|
||||
""", """dn: cn=role2,dc=example,dc=com
|
||||
objectClass: groupOfUniqueNames
|
||||
cn: role2
|
||||
uniqueMember: uid=cassandra,ou=People,dc=example,dc=com
|
||||
""", """dn: cn=role3,dc=example,dc=com
|
||||
objectClass: groupOfUniqueNames
|
||||
cn: role3
|
||||
uniqueMember: uid=jdoe,ou=People,dc=example,dc=com
|
||||
""", ]
|
||||
|
||||
|
||||
class PrepareChildProcessEnv:
|
||||
"""
|
||||
Class responsible to get environment variables from the main thread through the shared file and set them for the process
|
||||
"""
|
||||
|
||||
def __init__(self, root_dir, temp_dir: Path, modes: list[str], env_file: Path, byte_limit: int, worker_id):
|
||||
self.id = int(worker_id[2:]) + 1
|
||||
self.temp_dir = temp_dir
|
||||
self.modes = modes
|
||||
self.root_dir = root_dir
|
||||
self.byte_limit = byte_limit
|
||||
self.env_file = env_file
|
||||
self.finalize = None
|
||||
|
||||
def prepare(self) -> None:
|
||||
"""
|
||||
Setup LDAP proxy and set environment variables
|
||||
"""
|
||||
ldap_port = 5000 + (self.id * 3) % 55000
|
||||
|
||||
timeout = 10
|
||||
sleep_for = 0.01
|
||||
start_time = time.time()
|
||||
while True:
|
||||
if os.path.exists(self.env_file):
|
||||
(self.finalize, _, test_env) = setup(self.root_dir, ldap_port, self.temp_dir / 'ldap_instances',
|
||||
self.byte_limit)
|
||||
|
||||
for key, value in test_env.items():
|
||||
os.environ[key] = value
|
||||
break
|
||||
|
||||
if time.time() - start_time > timeout:
|
||||
raise TimeoutError(f"Timeout waiting for file {self.env_file}")
|
||||
# Sleep needed to wait when the controller will create a file with environment variables.
|
||||
# Without sleep checking of the file existence will be too fast,
|
||||
# so it will finish before the file is created
|
||||
time.sleep(sleep_for)
|
||||
sleep_for *= 2
|
||||
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""
|
||||
Stop LDAP
|
||||
"""
|
||||
if self.finalize:
|
||||
self.finalize()
|
||||
|
||||
def __enter__(self):
|
||||
try:
|
||||
self.prepare()
|
||||
except Exception:
|
||||
self.cleanup()
|
||||
raise
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.cleanup()
|
||||
|
||||
|
||||
class PrepareMainProcessEnv:
|
||||
"""
|
||||
A class responsible for starting additional services needed by tests.
|
||||
|
||||
It starts up a Minio server and an S3 mock server.
|
||||
The environment settings are saved to a file for later consumption by child processes.
|
||||
"""
|
||||
|
||||
def __init__(self, root_dir, temp_dir: Path, modes: list[str], env_file: Path, byte_limit: int):
|
||||
self.temp_dir = temp_dir
|
||||
self.modes = modes
|
||||
self.root_dir = root_dir
|
||||
pytest_dirs = [self.temp_dir / mode / 'pytest' for mode in modes]
|
||||
for directory in [self.temp_dir, *pytest_dirs]:
|
||||
if not directory.exists():
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
self.env_file = env_file
|
||||
self.tp_server = subprocess.Popen('toxiproxy-server', stderr=subprocess.DEVNULL)
|
||||
|
||||
def can_connect_to_toxiproxy():
|
||||
return can_connect(('127.0.0.1', 8474))
|
||||
|
||||
if not try_something_backoff(can_connect_to_toxiproxy):
|
||||
raise Exception('Could not connect to toxiproxy')
|
||||
self.ldap_port = 5000
|
||||
self.byte_limit = byte_limit
|
||||
self.finalize = None
|
||||
|
||||
def prepare(self) -> None:
|
||||
"""
|
||||
Start the LDAP.
|
||||
Create a file with environment variables for connecting to them.
|
||||
"""
|
||||
(self.finalize, _, test_env) = setup(self.root_dir, self.ldap_port, self.temp_dir / 'ldap_instances',
|
||||
self.byte_limit)
|
||||
|
||||
for key, value in test_env.items():
|
||||
os.environ[key] = value
|
||||
|
||||
with open(self.env_file, 'w') as file:
|
||||
for key, value in test_env.items():
|
||||
file.write(f"{key}={value}\n")
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""
|
||||
Stop LDAP.
|
||||
Remove the file with environment variables to not mess for consecutive runs.
|
||||
"""
|
||||
if os.path.exists(self.env_file):
|
||||
self.env_file.unlink()
|
||||
if self.finalize:
|
||||
self.finalize()
|
||||
if self.tp_server is not None:
|
||||
self.tp_server.terminate()
|
||||
|
||||
def __enter__(self):
|
||||
try:
|
||||
self.prepare()
|
||||
except Exception:
|
||||
self.cleanup()
|
||||
raise
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.cleanup()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_env_manager(root_dir: Path, temp_dir: Path, worker_id: str, modes: list[str],
|
||||
byte_limit: int) -> Generator[None, Any, None]:
|
||||
"""
|
||||
xdist helps to execute test in parallel.
|
||||
For that purpose it creates one main controller and workers.
|
||||
Pytest itself doesn't know if it's a worker or controller, so it will execute all fixtures and methods.
|
||||
Tests need S3 mock server and minio to start only once for the whole run, since they can share the one instance and
|
||||
share the environment variables with workers.
|
||||
|
||||
So the part of starting the servers executes on non-workers' machines.
|
||||
That means when xdist isn't used, servers start as intended in the main process.
|
||||
Tests on workers should know the endpoints of the servers, so the controller prepares this information.
|
||||
According classes responsible for configuration controller and workers.
|
||||
"""
|
||||
env_file = Path(f"{temp_dir}/test_env").absolute()
|
||||
if worker_id != 'master':
|
||||
with PrepareChildProcessEnv(root_dir, temp_dir, modes, env_file, byte_limit, worker_id):
|
||||
yield
|
||||
else:
|
||||
with PrepareMainProcessEnv(root_dir, temp_dir, modes, env_file, byte_limit):
|
||||
yield
|
||||
|
||||
|
||||
def can_connect(address, family=socket.AF_INET):
|
||||
s = socket.socket(family)
|
||||
try:
|
||||
s.connect(address)
|
||||
return True
|
||||
except OSError as e:
|
||||
if 'AF_UNIX path too long' in str(e):
|
||||
raise OSError(e.errno, "{} ({})".format(str(e), address)) from None
|
||||
else:
|
||||
return False
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def try_something_backoff(something):
|
||||
sleep_time = 0.05
|
||||
while not something():
|
||||
if sleep_time > 30:
|
||||
return False
|
||||
time.sleep(sleep_time)
|
||||
sleep_time *= 2
|
||||
return True
|
||||
|
||||
|
||||
def make_saslauthd_conf(port, instance_path):
|
||||
"""Creates saslauthd.conf with appropriate contents under instance_path. Returns the path to the new file."""
|
||||
saslauthd_conf_path = os.path.join(instance_path, 'saslauthd.conf')
|
||||
with open(saslauthd_conf_path, 'w') as f:
|
||||
f.write('ldap_servers: ldap://localhost:{}\nldap_search_base: dc=example,dc=com'.format(port))
|
||||
return saslauthd_conf_path
|
||||
|
||||
|
||||
def setup(project_root: Path, port: int, instance_root: Path, byte_limit: int):
|
||||
instance_path = instance_root / str(port)
|
||||
slapd_pid_file = instance_path / 'slapd.pid'
|
||||
saslauthd_socket_path = TemporaryDirectory()
|
||||
os.makedirs(instance_path, exist_ok=True)
|
||||
# This will always fail because it lacks the permissions to read the default slapd data
|
||||
# folder but it does create the instance folder so we don't want to fail here.
|
||||
try:
|
||||
subprocess.check_output(['slaptest', '-f', project_root / LDAP_SERVER_CONFIGURATION_FILE, '-F', instance_path],
|
||||
stderr=subprocess.DEVNULL)
|
||||
except:
|
||||
pass
|
||||
# Set up failure injection.
|
||||
proxy_name = 'p{}'.format(port)
|
||||
subprocess.check_output(
|
||||
['toxiproxy-cli', 'c', proxy_name, '--listen', 'localhost:{}'.format(port + 2), '--upstream',
|
||||
'localhost:{}'.format(port)])
|
||||
subprocess.check_output(['toxiproxy-cli', 't', 'a', proxy_name, '-t', 'limit_data', '-n', 'limiter', '-a',
|
||||
'bytes={}'.format(byte_limit)])
|
||||
# Change the data folder in the default config.
|
||||
replace_expression = 's/olcDbDirectory:.*/olcDbDirectory: {}/g'.format(str(instance_path).replace('/', r'\/'))
|
||||
subprocess.check_output(['find', instance_path, '-type', 'f', '-exec', 'sed', '-i', replace_expression, '{}', ';'])
|
||||
# Change the pid file to be kept with the instance.
|
||||
replace_expression = 's/olcPidFile:.*/olcPidFile: {}/g'.format(str(slapd_pid_file).replace('/', r'\/'))
|
||||
subprocess.check_output(['find', instance_path, '-type', 'f', '-exec', 'sed', '-i', replace_expression, '{}', ';'])
|
||||
# Put the test data in.
|
||||
cmd = ['slapadd', '-F', instance_path]
|
||||
subprocess.check_output(cmd, input='\n\n'.join(DEFAULT_ENTRIES).encode('ascii'), stderr=subprocess.STDOUT)
|
||||
# Set up the server.
|
||||
SLAPD_URLS = 'ldap://:{}/ ldaps://:{}/'.format(port, port + 1)
|
||||
|
||||
def can_connect_to_slapd():
|
||||
return can_connect(('127.0.0.1', port)) and can_connect(('127.0.0.1', port + 1)) and can_connect(
|
||||
('127.0.0.1', port + 2))
|
||||
|
||||
def can_connect_to_saslauthd():
|
||||
return can_connect(os.path.join(saslauthd_socket_path.name, 'mux'), socket.AF_UNIX)
|
||||
|
||||
slapd_proc = subprocess.Popen(['prlimit', '-n1024', 'slapd', '-F', instance_path, '-h', SLAPD_URLS, '-d', '0'])
|
||||
saslauthd_conf_path = make_saslauthd_conf(port, instance_path)
|
||||
test_env = {"SEASTAR_LDAP_PORT": str(port), "SASLAUTHD_MUX_PATH": os.path.join(saslauthd_socket_path.name, "mux")}
|
||||
|
||||
saslauthd_proc = subprocess.Popen(
|
||||
['saslauthd', '-d', '-n', '1', '-a', 'ldap', '-O', saslauthd_conf_path, '-m', saslauthd_socket_path.name],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
|
||||
|
||||
def finalize():
|
||||
slapd_proc.terminate()
|
||||
slapd_proc.wait() # Wait for slapd to remove slapd.pid, so it doesn't race with rmtree below.
|
||||
saslauthd_proc.kill() # Somehow, invoking terminate() here also terminates toxiproxy-server. o_O
|
||||
shutil.rmtree(instance_path)
|
||||
saslauthd_socket_path.cleanup()
|
||||
subprocess.check_output(['toxiproxy-cli', 'd', proxy_name])
|
||||
|
||||
try:
|
||||
if not try_something_backoff(can_connect_to_slapd):
|
||||
raise Exception('Unable to connect to slapd')
|
||||
if not try_something_backoff(can_connect_to_saslauthd):
|
||||
raise Exception('Unable to connect to saslauthd')
|
||||
except:
|
||||
finalize()
|
||||
raise
|
||||
return finalize, '--byte-limit={}'.format(byte_limit), test_env
|
||||
Reference in New Issue
Block a user