back to scripts

docker-level2.py

python 107 lines secrets redacted

The L2 Docker agent's logic — container triage and remediation decisions.

Note Live script from my home-lab server. Tokens, IDs, phone numbers and other secrets have been replaced with placeholders like <WHATSAPP_GROUP_ID> — everything else is the real, running code.
#!/usr/bin/env python3
import datetime
import json
import os
import re
import subprocess
import sys

REPORT_DIR = "/home/lanky/reports"
AUDIT_LOG = os.path.join(REPORT_DIR, "docker-level2-audit.jsonl")
VALID_NAME = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]*$")


def docker(*args: str, timeout: int = 120) -> subprocess.CompletedProcess[str]:
    return subprocess.run(
        ["docker", *args],
        text=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        timeout=timeout,
        check=False,
    )


def existing_containers() -> set[str]:
    result = docker("ps", "-a", "--format", "{{.Names}}")
    if result.returncode:
        raise RuntimeError(result.stdout.strip())
    return {line.strip() for line in result.stdout.splitlines() if line.strip()}


def audit(action: str, containers: list[str], result: str) -> None:
    os.makedirs(REPORT_DIR, exist_ok=True)
    record = {
        "timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(),
        "level": 2,
        "action": action,
        "containers": containers,
        "result": result,
    }
    with open(AUDIT_LOG, "a", encoding="utf-8") as log:
        log.write(json.dumps(record, ensure_ascii=True) + "\n")


def status() -> int:
    result = docker(
        "ps", "-a", "--format",
        "{{.Names}}\t{{.Status}}\t{{.Image}}",
    )
    print(result.stdout.strip())
    audit("status", [], "success" if result.returncode == 0 else "failed")
    return result.returncode


def restart(names: list[str]) -> int:
    if not names:
        print("No container names were supplied.", file=sys.stderr)
        return 2
    if any(not VALID_NAME.fullmatch(name) for name in names):
        print("Invalid container name.", file=sys.stderr)
        return 2

    existing = existing_containers()
    missing = sorted(set(names) - existing)
    if missing:
        print("Refusing restart; container does not exist: " + ", ".join(missing))
        audit("restart", names, "refused_missing_container")
        return 3

    output = []
    failures = []
    for name in names:
        result = docker("restart", "--time", "30", name, timeout=90)
        if result.returncode:
            failures.append(name)
            output.append(f"{name}: FAILED\n{result.stdout.strip()}")
            continue
        inspect = docker(
            "inspect", "--format",
            "{{.State.Status}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}",
            name,
        )
        state = inspect.stdout.strip() if inspect.returncode == 0 else "state unavailable"
        output.append(f"{name}: restarted; {state}")

    print("\n".join(output))
    result_text = "failed:" + ",".join(failures) if failures else "success"
    audit("restart", names, result_text)
    return 1 if failures else 0


def main() -> int:
    if len(sys.argv) < 2:
        print("Usage: docker-level2.py status|restart [container ...]", file=sys.stderr)
        return 2
    action = sys.argv[1]
    if action == "status" and len(sys.argv) == 2:
        return status()
    if action == "restart":
        return restart(sys.argv[2:])
    print("Level 2 permits only status and restart.", file=sys.stderr)
    audit(action, sys.argv[2:], "refused_action")
    return 2


if __name__ == "__main__":
    raise SystemExit(main())

back to scripts