Files
panda-rpki-oss/tests/support/test_daemon.py
yuyr 4975128945
Some checks failed
ci / rust (push) Has been cancelled
ci / docker (push) Has been cancelled
ci / audit (push) Has been cancelled
20260914 强制 TOML 配置并完善 TA 刷新与 daemon
2026-09-14 16:58:08 +08:00

227 lines
9.2 KiB
Python

"""Real CLI regression for the forced-TOML validate and daemon contracts."""
import json
from pathlib import Path
import signal
import socket
import subprocess
import sys
import tempfile
import time
def read(path):
return json.loads(path.read_text())
def wait_for(check, label, timeout=30):
start = time.monotonic()
while time.monotonic() - start < timeout:
if check():
return
time.sleep(0.05)
raise AssertionError(f"timed out waiting for {label}")
def state_is(root, state):
path = root / "daemon-status.json"
return path.exists() and read(path)["state"] == state
def validate_config(path, fixture, out, state, mode="auto", ccr=None, timeout=2,
progress=10):
ccr = ccr or (out / "result.ccr")
path.write_text(
f'''config-version = 1
mode = "validate"
out = {str(out)!r}
rrdp-state-dir = {str(state)!r}
rrdp-sync-mode = "{mode}"
ccr-out = {str(ccr)!r}
http-timeout-secs = {timeout}
http-root-cert = [{str(fixture / "certs/rrdp-ca.pem")!r}]
parallel-max-repo-sync-workers-global = 2
parallel-phase2-object-workers = 2
log-level = "info"
log-format = "json"
log-progress-interval-secs = {progress}
[[anchors]]
id = "custom"
tal = {str(fixture / "tal/custom.tal")!r}
ta = {str(fixture / "ta/custom-ta.cer")!r}
''',
encoding="utf-8",
)
def daemon_config(path, fixture, root, max_runs=None, interval=2, retain=3,
timeout=0, grace=30, tal=None, auto=False, progress=10):
tal = tal or (fixture / "tal/custom.tal")
ta_line = "" if auto else f'ta = {str(fixture / "ta/custom-ta.cer")!r}\n'
max_line = "" if max_runs is None else f"max-runs = {max_runs}\n"
path.write_text(
f'''config-version = 1
mode = "daemon"
state-root = {str(root)!r}
interval-secs = {interval}
{max_line}retain-runs = {retain}
run-timeout-secs = {timeout}
shutdown-grace-secs = {grace}
http-timeout-secs = 2
http-root-cert = [{str(fixture / "certs/rrdp-ca.pem")!r}]
parallel-max-repo-sync-workers-global = 2
parallel-phase2-object-workers = 2
log-level = "info"
log-format = "json"
log-progress-interval-secs = {progress}
[[anchors]]
id = "custom"
tal = {str(tal)!r}
{ta_line}''',
encoding="utf-8",
)
def run(binary):
support = Path(__file__).resolve().parent
with tempfile.TemporaryDirectory() as tmp:
base = Path(tmp)
fixture = base / "fixture"
with socket.socket() as sock:
sock.bind(("127.0.0.1", 0))
port = sock.getsockname()[1]
subprocess.run(
[sys.executable, str(support / "generate_repository.py"),
"--rrdp-host", "127.0.0.1", "--rrdp-port", str(port),
"--output", str(fixture)], check=True, timeout=120)
active = fixture / "active"
active.symlink_to("cases/baseline-v1/http")
server = subprocess.Popen(
[sys.executable, str(support / "serve_repository.py"), str(active),
str(port), str(fixture / "certs/rrdp-server.pem"),
str(fixture / "certs/rrdp-server.key")],
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
processes = []
logs = []
try:
assert server.stdout.readline().strip() == "ready"
def launch(root, *, max_runs=None, interval=2, retain=3, timeout=0,
grace=30, tal=None, auto=False, progress=10):
config = base / f"daemon-{len(logs)}.toml"
daemon_config(config, fixture, root, max_runs=max_runs,
interval=interval, retain=retain, timeout=timeout,
grace=grace, tal=tal, auto=auto, progress=progress)
log = (base / f"controller-{len(logs)}.log").open("w")
logs.append(log)
proc = subprocess.Popen(
[binary, "daemon", "--config", str(config)],
stdout=log, stderr=log)
processes.append(proc)
return proc
root = base / "normal"
proc = launch(root, max_runs=2, interval=2, retain=2)
wait_for(lambda: state_is(root, "sleeping"), "snapshot completion")
first = read(root / "runs/run_000001/run-summary.json")
assert first["status"] == "success" and first["sync_mode"] == "snapshot", first
assert first["summary"]["vrps"] == 1, first
rival = launch(root, max_runs=1)
assert rival.wait(timeout=10) == 2
active.unlink()
active.symlink_to("cases/baseline-v2/http")
assert proc.wait(timeout=30) == 0
second = read(root / "runs/run_000002/run-summary.json")
assert second["status"] == "success" and second["sync_mode"] == "delta", second
assert second["summary"]["vrps"] == 2, second
timing = read(root / "runs/run_000002/stage-timing.json")
assert timing["analysis_counts"]["rrdp_delta_ops_applied_total"] > 0
assert (root / "latest").is_symlink()
for seq in (1, 2):
out = root / f"runs/run_{seq:06}"
assert (out / "validator-config.toml").is_file()
assert (out / "result-complete.json").is_file()
records = [json.loads(line) for line in
(out / "validator.log").read_text().splitlines()]
assert records[-1]["event"] == "validation_completed", records[-1]
assert all(record["fields"]["run_seq"] == seq for record in records)
assert len((out / "vrps.csv").read_text().splitlines()) - 1 == seq
subprocess.run(["openssl", "asn1parse", "-inform", "DER", "-in",
str(out / "result.ccr"), "-noout"], check=True)
(root / "runs/operator-notes").mkdir()
(root / "runs/run_000003").mkdir()
restart = launch(root, max_runs=1, interval=0, retain=1)
assert restart.wait(timeout=30) == 0
fourth = read(root / "runs/run_000004/run-summary.json")
assert fourth["sync_mode"] == "auto" and fourth["summary"]["vrps"] == 2
assert not (root / "runs/run_000001").exists()
assert (root / "runs/operator-notes").is_dir()
assert (root / "runs/run_000003").is_dir()
# A malformed output destination is rejected during read-only config validation.
bad_config = base / "bad.toml"
validate_config(bad_config, fixture, base / "bad-output", base / "bad-state",
ccr=base)
output_failure = subprocess.run(
[binary, "validate", "--config", str(bad_config)],
capture_output=True, text=True, timeout=30)
assert output_failure.returncode == 2 and not output_failure.stdout
assert "ccr-out must be a file" in output_failure.stderr
# A valid TAL with an unreachable automatic TA URI creates failed daemon runs.
bad_tal = base / "unreachable.tal"
tal_text = (fixture / "tal/custom.tal").read_text()
bad_tal.write_text(
tal_text
.replace("rpki-local-rsync:873", "127.0.0.1:1")
.replace(f"https://127.0.0.1:{port}", "https://127.0.0.1:1")
)
failed = base / "failed"
failure = launch(failed, max_runs=2, interval=0, tal=bad_tal, auto=True)
assert failure.wait(timeout=30) == 2
for seq in (1, 2):
assert read(failed / f"runs/run_{seq:06}/run-summary.json")["status"] == "failed"
# Whole-run timeout and SIGTERM are exercised while the RRDP server is stopped.
server.send_signal(signal.SIGSTOP)
timeout_root = base / "timeout"
timeout_proc = launch(timeout_root, max_runs=1, timeout=2, progress=0)
assert timeout_proc.wait(timeout=15) == 2
timeout_run = read(timeout_root / "runs/run_000001/run-summary.json")
assert timeout_run["error"] == "timeout", timeout_run
interrupted = base / "interrupted"
active_proc = launch(interrupted, grace=0)
wait_for(lambda: state_is(interrupted, "running"), "active validator")
active_proc.send_signal(signal.SIGTERM)
assert active_proc.wait(timeout=15) == 2
assert read(interrupted / "runs/run_000001/run-summary.json")["error"] == "interrupted"
server.send_signal(signal.SIGCONT)
print("Real daemon E2E passed: TOML config/snapshot/delta/restart/retention/lock/timeout/SIGTERM")
except BaseException:
for log in logs:
log.flush()
print(Path(log.name).read_text(), file=sys.stderr)
raise
finally:
server.send_signal(signal.SIGCONT)
for proc in processes:
if proc.poll() is None:
proc.terminate()
try:
proc.wait(timeout=35)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait()
server.terminate()
server.wait(timeout=5)
for log in logs:
log.close()
if __name__ == "__main__":
run(str(Path(sys.argv[1]).resolve()))