feat: read-only sysctl audit ergänzen
This commit is contained in:
parent
c7d7a9af8a
commit
f96e9abfd4
1 changed files with 73 additions and 1 deletions
74
app.py
74
app.py
|
|
@ -11,7 +11,7 @@ from fastapi.responses import JSONResponse, RedirectResponse
|
|||
from contextlib import asynccontextmanager
|
||||
|
||||
log = logging.getLogger("butler")
|
||||
VERSION = "2.3.2"
|
||||
VERSION = "2.3.3"
|
||||
|
||||
API_DIR = os.environ.get("API_KEY_DIR", "/data/api")
|
||||
VAULT_CACHE_DIR = os.environ.get("VAULT_CACHE_DIR", "/data/vault-cache")
|
||||
|
|
@ -278,6 +278,7 @@ async def root():
|
|||
"status": "GET /status - health of all backends",
|
||||
"overview": "GET /overview?details=false - deterministic homelab verdict for small models",
|
||||
"audit": "GET /audit - recent API calls",
|
||||
"sysctl_audit": "GET /system/sysctl/{host} - read-only live and persistent network tuning",
|
||||
},
|
||||
"vault_items": len(_vault_cache),
|
||||
}
|
||||
|
|
@ -1049,6 +1050,7 @@ SPEEDTEST_REPO_FILES = (
|
|||
"streamscope/static/index.html",
|
||||
"streamscope/static/assets/app.css",
|
||||
"streamscope/static/assets/app.js",
|
||||
"streamscope/static/assets/longterm-metrics.js",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1185,6 +1187,76 @@ def _ssh(host, cmd, timeout=600):
|
|||
except _sp.TimeoutExpired:
|
||||
return 124, "", f"SSH command timed out after {timeout} seconds"
|
||||
|
||||
|
||||
SYSCTL_AUDIT_KEYS = (
|
||||
"net.core.default_qdisc",
|
||||
"net.core.rmem_default",
|
||||
"net.core.rmem_max",
|
||||
"net.core.wmem_default",
|
||||
"net.core.wmem_max",
|
||||
"net.core.netdev_max_backlog",
|
||||
"net.core.somaxconn",
|
||||
"net.ipv4.ip_forward",
|
||||
"net.ipv4.tcp_congestion_control",
|
||||
"net.ipv4.tcp_fastopen",
|
||||
"net.ipv4.tcp_mtu_probing",
|
||||
"net.ipv4.tcp_no_metrics_save",
|
||||
"net.ipv4.tcp_rmem",
|
||||
"net.ipv4.tcp_slow_start_after_idle",
|
||||
"net.ipv4.tcp_window_scaling",
|
||||
"net.ipv4.tcp_wmem",
|
||||
)
|
||||
|
||||
|
||||
def _sysctl_audit_command() -> str:
|
||||
script = f'''import glob, json, subprocess
|
||||
keys = {SYSCTL_AUDIT_KEYS!r}
|
||||
live, errors = {{}}, {{}}
|
||||
for key in keys:
|
||||
result = subprocess.run(["sysctl", "-n", key], capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
live[key] = result.stdout.strip()
|
||||
else:
|
||||
errors[key] = result.stderr.strip()[:160]
|
||||
persistent = {{}}
|
||||
for path in ["/etc/sysctl.conf", *sorted(glob.glob("/etc/sysctl.d/*.conf"))]:
|
||||
try:
|
||||
with open(path, encoding="utf-8", errors="replace") as handle:
|
||||
for raw in handle:
|
||||
line = raw.split("#", 1)[0].strip()
|
||||
if "=" not in line:
|
||||
continue
|
||||
key, value = (part.strip() for part in line.split("=", 1))
|
||||
if key in keys:
|
||||
persistent.setdefault(key, []).append({{"file": path, "value": value}})
|
||||
except (FileNotFoundError, PermissionError):
|
||||
pass
|
||||
print(json.dumps({{"live": live, "persistent": persistent, "errors": errors}}))
|
||||
'''
|
||||
encoded = base64.b64encode(script.encode()).decode()
|
||||
return f'python3 -c "import base64;exec(base64.b64decode(\'{encoded}\'))"'
|
||||
|
||||
|
||||
@app.get("/system/sysctl/{host}")
|
||||
async def system_sysctl_audit(host: str, _=Depends(_verify)):
|
||||
if not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,62}", host):
|
||||
raise HTTPException(400, "Invalid host name")
|
||||
if host == "vps":
|
||||
target = VPS_SSH
|
||||
else:
|
||||
inventory = await asyncio.to_thread(_find_inventory_host, host)
|
||||
if not inventory:
|
||||
raise HTTPException(404, f"Host {host} not found")
|
||||
target = f'{inventory["user"]}@{inventory["ip"]}'
|
||||
rc, out, err = await asyncio.to_thread(_ssh, target, _sysctl_audit_command(), 30)
|
||||
if rc != 0:
|
||||
raise HTTPException(502, (err or out).strip()[:300] or "sysctl audit failed")
|
||||
try:
|
||||
result = json.loads(out)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise HTTPException(502, "sysctl audit returned invalid JSON") from exc
|
||||
return {"host": host, **result}
|
||||
|
||||
def _pve_auth():
|
||||
pv = _parse_kv("proxmox")
|
||||
return f"PVEAPIToken={pv.get('tokenid','')}={pv.get('secret','')}"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue