test: probe hardlink staging in deployment preflight
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate a host's Archive Control Docker deployment without changing it.
|
||||
"""Validate a host's Archive Control Docker deployment before it is used.
|
||||
|
||||
This intentionally relies only on the config file and the named containers on
|
||||
the host where it runs. It never reads secret contents or calls a remote
|
||||
control daemon.
|
||||
control daemon. The hard-link probe creates uniquely named, empty files in
|
||||
the configured qB and automatic-route directories and removes them afterward.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -168,6 +169,43 @@ raise SystemExit(0 if all(p.state == 1 for p in probes) else 1)'''
|
||||
print(result.stdout.strip())
|
||||
|
||||
|
||||
def run_hardlink_probe(container: str, qb_root: str, route_root: str) -> None:
|
||||
"""Prove that the daemon can hard-link from qB data into automatic routes."""
|
||||
|
||||
program = r'''import os,sys,tempfile
|
||||
source_root,route_root=sys.argv[1:]
|
||||
source_path=None
|
||||
destination_path=None
|
||||
try:
|
||||
source_fd,source_path=tempfile.mkstemp(prefix=".archive-control-preflight-",dir=source_root)
|
||||
os.close(source_fd)
|
||||
destination_fd,destination_path=tempfile.mkstemp(prefix=".archive-control-preflight-",dir=route_root)
|
||||
os.close(destination_fd)
|
||||
os.unlink(destination_path)
|
||||
os.link(source_path,destination_path)
|
||||
source_stat=os.stat(source_path)
|
||||
destination_stat=os.stat(destination_path)
|
||||
if source_stat.st_dev != destination_stat.st_dev or source_stat.st_ino != destination_stat.st_ino:
|
||||
raise RuntimeError("link(2) did not produce the same filesystem inode")
|
||||
print("hard-link staging probe passed")
|
||||
finally:
|
||||
for path in (destination_path,source_path):
|
||||
if path:
|
||||
try:
|
||||
os.unlink(path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
'''
|
||||
result = subprocess.run(
|
||||
["docker", "exec", container, "python", "-c", program, qb_root, route_root],
|
||||
check=False, text=True, capture_output=True,
|
||||
)
|
||||
if result.returncode:
|
||||
detail = result.stderr.strip() or result.stdout.strip() or "failed"
|
||||
raise CheckFailure(f"hard-link staging probe failed: {detail}")
|
||||
print(result.stdout.strip())
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Read-only Archive Control Docker deployment preflight"
|
||||
@@ -209,6 +247,9 @@ def main(argv: list[str] | None = None) -> int:
|
||||
"qBittorrent and future route roots use separate client bind "
|
||||
"mounts; hard-link staging would be unavailable"
|
||||
)
|
||||
run_hardlink_probe(
|
||||
args.client_container, qb["local_root"], route_local_path(sync)
|
||||
)
|
||||
for key in ("shared_token_file",):
|
||||
require_regular_secret(map_path(client, config[key]).source)
|
||||
for service, key in ((qb, "password_file"), (sync, "api_key_file")):
|
||||
@@ -218,7 +259,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
except (CheckFailure, KeyError, OSError) as exc:
|
||||
print(f"preflight failed: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
print("preflight passed: bind mappings, secrets, filesystem capabilities, and local APIs are healthy")
|
||||
print("preflight passed: bind mappings, hard-link staging, secrets, filesystem capabilities, and local APIs are healthy")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user