Exclude local agent state from deploy archives, avoid recursive ownership over active runtime mounts, and let Alembic own schema upgrades before app startup.
205 lines
7.9 KiB
Python
205 lines
7.9 KiB
Python
import os
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
DEPLOY = ROOT / "deploy"
|
|
|
|
|
|
WEB_UNIT = DEPLOY / "files/usr/local/lib/systemd/system/left4me-web.service"
|
|
SERVER_UNIT = DEPLOY / "files/usr/local/lib/systemd/system/left4me-server@.service"
|
|
SYSTEMCTL_HELPER = DEPLOY / "files/usr/local/libexec/left4me/left4me-systemctl"
|
|
JOURNALCTL_HELPER = DEPLOY / "files/usr/local/libexec/left4me/left4me-journalctl"
|
|
SUDOERS = DEPLOY / "files/etc/sudoers.d/left4me"
|
|
HOST_ENV = DEPLOY / "templates/etc/left4me/host.env"
|
|
WEB_ENV_TEMPLATE = DEPLOY / "templates/etc/left4me/web.env.template"
|
|
DEPLOY_SCRIPT = DEPLOY / "deploy-test-server.sh"
|
|
|
|
|
|
def test_global_unit_files_exist_at_product_level_paths():
|
|
assert WEB_UNIT.is_file()
|
|
assert SERVER_UNIT.is_file()
|
|
|
|
|
|
def test_web_unit_contains_required_runtime_contract():
|
|
unit = WEB_UNIT.read_text()
|
|
|
|
assert "User=left4me" in unit
|
|
assert "Group=left4me" in unit
|
|
assert "WorkingDirectory=/opt/left4me" in unit
|
|
assert "Environment=PATH=/opt/left4me/.venv/bin:" in unit
|
|
assert "EnvironmentFile=/etc/left4me/host.env" in unit
|
|
assert "EnvironmentFile=/etc/left4me/web.env" in unit
|
|
assert "ExecStart=/opt/left4me/.venv/bin/gunicorn" in unit
|
|
assert "--workers 1" in unit
|
|
assert "NoNewPrivileges=true" not in unit
|
|
assert "PrivateTmp=true" not in unit
|
|
assert "ProtectSystem=full" in unit
|
|
assert "ReadWritePaths=/var/lib/left4me" in unit
|
|
assert "MountFlags=shared" in unit
|
|
|
|
|
|
def test_server_unit_contains_required_runtime_contract():
|
|
unit = SERVER_UNIT.read_text()
|
|
|
|
assert "User=left4me" in unit
|
|
assert "Group=left4me" in unit
|
|
assert "EnvironmentFile=/etc/left4me/host.env" in unit
|
|
assert "EnvironmentFile=/var/lib/left4me/instances/%i/instance.env" in unit
|
|
assert "WorkingDirectory=/var/lib/left4me/runtime/%i/merged/left4dead2" in unit
|
|
assert "ExecStart=/var/lib/left4me/installation/srcds_run" in unit
|
|
assert "$L4D2_ARGS" in unit
|
|
assert "${L4D2_ARGS}" not in unit
|
|
assert "NoNewPrivileges=true" in unit
|
|
assert "PrivateTmp=true" in unit
|
|
assert "PrivateDevices=true" in unit
|
|
assert "ProtectHome=true" in unit
|
|
assert "ProtectSystem=strict" in unit
|
|
assert "ReadOnlyPaths=/var/lib/left4me/installation /var/lib/left4me/overlays" in unit
|
|
assert "ReadWritePaths=/var/lib/left4me/runtime/%i" in unit
|
|
assert "RestrictSUIDSGID=true" in unit
|
|
assert "LockPersonality=true" in unit
|
|
|
|
|
|
def _fake_command(tmp_path, command_name):
|
|
marker = tmp_path / f"{command_name}.args"
|
|
command = tmp_path / command_name
|
|
command.write_text(f"#!/bin/sh\nprintf '%s\n' \"$*\" > '{marker}'\nexit 0\n")
|
|
command.chmod(0o755)
|
|
return marker
|
|
|
|
|
|
def _env_with_fake_commands(tmp_path):
|
|
env = os.environ.copy()
|
|
env["PATH"] = f"{tmp_path}{os.pathsep}{env.get('PATH', '')}"
|
|
return env
|
|
|
|
|
|
def test_helpers_use_fixed_system_tool_paths_not_sudo_path():
|
|
systemctl = SYSTEMCTL_HELPER.read_text()
|
|
journalctl = JOURNALCTL_HELPER.read_text()
|
|
|
|
assert "command -v systemctl" not in systemctl
|
|
assert "command -v journalctl" not in journalctl
|
|
assert "/bin/systemctl" in systemctl or "/usr/bin/systemctl" in systemctl
|
|
assert "/bin/journalctl" in journalctl or "/usr/bin/journalctl" in journalctl
|
|
|
|
|
|
def test_systemctl_helper_passes_shell_syntax_check_and_rejects_bad_args(tmp_path):
|
|
subprocess.run(["sh", "-n", str(SYSTEMCTL_HELPER)], check=True)
|
|
marker = _fake_command(tmp_path, "systemctl")
|
|
|
|
for args in [
|
|
["bad/action", "alpha"],
|
|
["start", ""],
|
|
["start", ".hidden"],
|
|
["start", "bad..name"],
|
|
["start", "bad/name"],
|
|
["start", "bad\\name"],
|
|
["start", "bad name"],
|
|
]:
|
|
result = subprocess.run(["sh", str(SYSTEMCTL_HELPER), *args], env=_env_with_fake_commands(tmp_path), check=False)
|
|
assert result.returncode != 0
|
|
assert not marker.exists()
|
|
|
|
script = SYSTEMCTL_HELPER.read_text()
|
|
assert 'unit="left4me-server@${name}.service"' in script
|
|
assert 'start) exec "$systemctl" start "$unit"' in script
|
|
assert 'stop) exec "$systemctl" stop "$unit"' in script
|
|
assert "--property=ActiveState" in script
|
|
assert "--property=SubState" in script
|
|
|
|
|
|
def test_journalctl_helper_passes_shell_syntax_check_and_rejects_bad_args(tmp_path):
|
|
subprocess.run(["sh", "-n", str(JOURNALCTL_HELPER)], check=True)
|
|
marker = _fake_command(tmp_path, "journalctl")
|
|
|
|
for args in [
|
|
["../evil", "--lines", "25", "--no-follow"],
|
|
["alpha", "--bad", "25", "--no-follow"],
|
|
["alpha", "--lines", "not-number", "--no-follow"],
|
|
["alpha", "--lines", "25", "--bad-follow"],
|
|
["bad/name", "--lines", "25", "--no-follow"],
|
|
]:
|
|
result = subprocess.run(["sh", str(JOURNALCTL_HELPER), *args], env=_env_with_fake_commands(tmp_path), check=False)
|
|
assert result.returncode != 0
|
|
assert not marker.exists()
|
|
|
|
script = JOURNALCTL_HELPER.read_text()
|
|
assert 'unit="left4me-server@${name}.service"' in script
|
|
assert 'exec "$journalctl" -u "$unit" -n "$lines" -o cat "$follow_arg"' in script
|
|
assert 'exec "$journalctl" -u "$unit" -n "$lines" -o cat' in script
|
|
|
|
|
|
def test_sudoers_allows_only_left4me_helpers_not_raw_system_tools():
|
|
sudoers = SUDOERS.read_text()
|
|
|
|
assert (
|
|
"left4me ALL=(root) NOPASSWD: "
|
|
"/usr/local/libexec/left4me/left4me-systemctl *"
|
|
) in sudoers
|
|
assert (
|
|
"left4me ALL=(root) NOPASSWD: "
|
|
"/usr/local/libexec/left4me/left4me-journalctl *"
|
|
) in sudoers
|
|
assert "/bin/systemctl" not in sudoers
|
|
assert "/usr/bin/systemctl" not in sudoers
|
|
assert "/bin/journalctl" not in sudoers
|
|
assert "/usr/bin/journalctl" not in sudoers
|
|
|
|
|
|
def test_env_templates_contain_required_defaults():
|
|
host_env = HOST_ENV.read_text()
|
|
assert "Deployment units use fixed /var/lib/left4me paths" in host_env
|
|
assert host_env.endswith("LEFT4ME_ROOT=/var/lib/left4me\n")
|
|
assert WEB_ENV_TEMPLATE.read_text() == (
|
|
"DATABASE_URL=sqlite:////var/lib/left4me/left4me.db\n"
|
|
"SECRET_KEY=replace-with-generated-secret\n"
|
|
"JOB_WORKER_THREADS=4\n"
|
|
)
|
|
|
|
|
|
def test_deploy_script_has_safe_defaults_and_preserves_state() -> None:
|
|
script = DEPLOY_SCRIPT.read_text()
|
|
|
|
assert "useradd --system --home-dir /var/lib/left4me" in script
|
|
assert "/var/lib/left4me/installation" in script
|
|
assert "/var/lib/left4me/overlays" in script
|
|
assert "/var/lib/left4me/instances" in script
|
|
assert "/var/lib/left4me/runtime" in script
|
|
assert "tar" in script
|
|
assert "--exclude .venv" in script
|
|
assert "--exclude .claude" in script
|
|
assert "pip install -e /opt/left4me/l4d2host -e /opt/left4me/l4d2web" in script
|
|
assert "systemctl enable --now left4me-web.service" in script
|
|
assert "for attempt in" in script
|
|
assert "/opt/left4me/.venv" in script
|
|
assert "visudo -cf /etc/sudoers.d/left4me" in script
|
|
assert "if [ ! -f /etc/left4me/web.env ]" in script
|
|
assert ". /etc/left4me/web.env\n" not in script
|
|
assert "run_left4me_with_env" in script
|
|
assert "LEFT4ME_ADMIN_USERNAME" in script
|
|
assert "LEFT4ME_ADMIN_PASSWORD" in script
|
|
assert "user already exists" in script
|
|
assert "deploy/files" in script
|
|
|
|
|
|
def test_deploy_script_does_not_recurse_into_runtime_state_mounts() -> None:
|
|
script = DEPLOY_SCRIPT.read_text()
|
|
|
|
assert "$sudo_cmd chown -R left4me:left4me /var/lib/left4me" not in script
|
|
assert "$sudo_cmd chown left4me:left4me \\" in script
|
|
assert "/var/lib/left4me/runtime \\" in script
|
|
assert "$sudo_cmd chown -R left4me:left4me /opt/left4me" in script
|
|
|
|
|
|
def test_deploy_script_runs_migrations_before_app_initialization() -> None:
|
|
script = DEPLOY_SCRIPT.read_text()
|
|
|
|
assert "alembic -c /opt/left4me/l4d2web/alembic.ini upgrade head" in script
|
|
assert "from l4d2web.app import create_app; create_app()" not in script
|
|
|
|
|
|
def test_deploy_script_shell_syntax() -> None:
|
|
subprocess.run(["sh", "-n", str(DEPLOY_SCRIPT)], check=True)
|