You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
143 lines
6.5 KiB
143 lines
6.5 KiB
"""Fresh-install tests for the NetBird control plane.
|
|
|
|
The test ignition (fcos-test.ign) contains the cookbook and its dependencies but NOT the
|
|
examples or the dependency hooks (both are excluded from the production package). So this
|
|
module injects, via ``fcos_extra_files``:
|
|
|
|
* the PostgreSQL server config (config.env) — otherwise postgresql never starts and
|
|
management loops waiting for the database,
|
|
* the operator-provided NetBird config (read from this cookbook's own config/examples/),
|
|
* the PostgreSQL init hook that creates the netbird database (other/postgresql/netbird.sql),
|
|
* the Traefik routing hook (other/traefik/netbird.yaml) and a minimal Traefik main config.
|
|
|
|
The tests are self-sufficient: they assert what holds without a working OIDC provider (units
|
|
active, ports listening, generated config in place, database reachable, Traefik routing to the
|
|
right backend). They do not attempt a login flow.
|
|
"""
|
|
|
|
import subprocess
|
|
import textwrap
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from helpers import TestNetbird
|
|
|
|
COOKBOOK_DIR = Path(__file__).resolve().parent.parent
|
|
|
|
# PostgreSQL server config for the dependency. config.env ships only as an example (excluded
|
|
# from the test ignition), so it must be injected or postgresql.target never comes up.
|
|
POSTGRESQL_CONFIG_ENV = textwrap.dedent("""\
|
|
POSTGRES_USER=postgres
|
|
POSTGRES_PASSWORD=postgres
|
|
POSTGRES_DB=postgres
|
|
POSTGRES_HOST_AUTH_METHOD=scram-sha-256
|
|
POSTGRES_INITDB_ARGS=--auth-host=scram-sha-256
|
|
POSTGRES_ARGS=-h 127.0.0.1
|
|
PGPORT=5432
|
|
PG_MAJOR=18
|
|
POSTGRES_BACKUP_RETENTION=7
|
|
""")
|
|
|
|
# Minimal Traefik main configuration: a file provider watching conf.d and the two entry
|
|
# points. TLS is served self-signed (the netbird.yaml routers use bare `tls: {}`), so no ACME
|
|
# / certificate resolver is needed for the tests.
|
|
TRAEFIK_YAML = """\
|
|
api:
|
|
dashboard: false
|
|
# The traefik cookbook ships a packaged conf.d/ping.yaml that references ping@internal;
|
|
# enable manualRouting so that router loads cleanly.
|
|
ping:
|
|
manualRouting: true
|
|
log:
|
|
level: INFO
|
|
accesslog: false
|
|
global:
|
|
sendanonymoususage: false
|
|
checknewversion: false
|
|
entryPoints:
|
|
http:
|
|
address: ":80"
|
|
https:
|
|
address: ":443"
|
|
providers:
|
|
file:
|
|
directory: /etc/traefik/conf.d/
|
|
watch: true
|
|
"""
|
|
|
|
|
|
def _read(rel: str) -> str:
|
|
return (COOKBOOK_DIR / rel).read_text()
|
|
|
|
|
|
@pytest.fixture(scope="package")
|
|
def fcos_vm_config() -> tuple[int, int, int, int]:
|
|
"""More resources: five NetBird images plus PostgreSQL and Traefik to pull and start."""
|
|
return (6144, 4, 50, 100)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def fcos_extra_files() -> dict:
|
|
"""Inject operator config + dependency config/hooks that the test ignition does not carry."""
|
|
return {
|
|
# PostgreSQL server config (dependency): without it postgresql.target never starts.
|
|
"/etc/quadlets/postgresql/config.env": (POSTGRESQL_CONFIG_ENV, 0, 0, 0o600),
|
|
# Traefik main config + the routing hook shipped by this cookbook.
|
|
"/etc/quadlets/traefik/traefik.yaml": (TRAEFIK_YAML, 10001, 10000, 0o644),
|
|
"/etc/quadlets/traefik/conf.d/netbird.yaml": (_read("other/traefik/netbird.yaml"), 10001, 10000, 0o644),
|
|
# PostgreSQL init hook: creates the netbird database/user on first boot.
|
|
"/etc/quadlets/postgresql/init.d/netbird.sql": (_read("other/postgresql/netbird.sql"), 10004, 10000, 0o600),
|
|
# Operator-provided NetBird config (working-but-insecure examples).
|
|
"/etc/quadlets/netbird/management.json": (_read("config/examples/management.json"), 10035, 10000, 0o640),
|
|
"/etc/quadlets/netbird/management.env": (_read("config/examples/management.env"), 0, 0, 0o600),
|
|
"/etc/quadlets/netbird/dashboard.env": (_read("config/examples/dashboard.env"), 0, 0, 0o600),
|
|
"/etc/quadlets/netbird/relay.env": (_read("config/examples/relay.env"), 0, 0, 0o600),
|
|
"/etc/quadlets/netbird/turnserver.conf": (_read("config/examples/turnserver.conf"), 10035, 10000, 0o640),
|
|
}
|
|
|
|
|
|
class TestNetbirdInstall(TestNetbird):
|
|
"""Verify the NetBird control plane comes up cleanly on a fresh VM boot."""
|
|
|
|
def test_management_connected_to_postgres(self, fcos_host):
|
|
"""Management must have selected the Postgres store engine and not be crash-looping."""
|
|
result = fcos_host.run("podman logs netbird-management 2>&1 | grep -i 'Postgres store engine'")
|
|
assert result.rc == 0, "management did not report using the Postgres store engine"
|
|
restarts = fcos_host.run("systemctl show -p NRestarts --value netbird-management.service")
|
|
assert restarts.stdout.strip() == "0", f"management restarted {restarts.stdout.strip()} times"
|
|
|
|
def test_netbird_database_exists(self, fcos_host):
|
|
"""The postgresql hook must have created the netbird database."""
|
|
result = fcos_host.run(
|
|
"podman exec postgresql-server psql -U postgres -tAc "
|
|
"\"SELECT 1 FROM pg_database WHERE datname='netbird'\""
|
|
)
|
|
assert result.stdout.strip() == "1", f"netbird database missing: {result.stdout} {result.stderr}"
|
|
|
|
def test_dashboard_served_through_traefik(self, fcos_vm):
|
|
"""Traefik must serve the dashboard SPA over HTTPS (self-signed) on the netbird host."""
|
|
result = subprocess.run(
|
|
["curl", "-sS", "-k", "-o", "/dev/null", "-w", "%{http_code}",
|
|
"--resolve", f"netbird:443:{fcos_vm.ip}", "https://netbird/"],
|
|
check=False, capture_output=True, text=True,
|
|
)
|
|
assert result.stdout.strip() == "200", f"dashboard not served: got {result.stdout!r} {result.stderr}"
|
|
|
|
def test_management_api_reachable_through_traefik(self, fcos_vm):
|
|
"""Traefik must route /api to management (401 = reached and demands auth, not 502)."""
|
|
result = subprocess.run(
|
|
["curl", "-sS", "-k", "-o", "/dev/null", "-w", "%{http_code}",
|
|
"--resolve", f"netbird:443:{fcos_vm.ip}", "https://netbird/api/users"],
|
|
check=False, capture_output=True, text=True,
|
|
)
|
|
assert result.stdout.strip() == "401", f"/api not routed to management: got {result.stdout!r}"
|
|
|
|
def test_relay_reachable_through_traefik(self, fcos_vm):
|
|
"""Traefik must route /relay to the relay (426 = WebSocket upgrade required, not 502)."""
|
|
result = subprocess.run(
|
|
["curl", "-sS", "-k", "-o", "/dev/null", "-w", "%{http_code}",
|
|
"--resolve", f"netbird:443:{fcos_vm.ip}", "https://netbird/relay"],
|
|
check=False, capture_output=True, text=True,
|
|
)
|
|
assert result.stdout.strip() == "426", f"/relay not routed to relay: got {result.stdout!r}"
|
|
|