217 lines
6.7 KiB
Python
217 lines
6.7 KiB
Python
import os
|
|
import subprocess
|
|
from app_images import init_images_router
|
|
from app_files import init_files_router
|
|
from app_pods import init_pods_router
|
|
from app_containers import init_containers_router, start_stats_poller
|
|
from app_networks import init_networks_router
|
|
from fastapi import FastAPI, HTTPException
|
|
import requests_unixsocket
|
|
import uvicorn
|
|
|
|
app = FastAPI(title="Podman MVP Control Plane", root_path="/api")
|
|
SESSION = requests_unixsocket.Session()
|
|
PODMAN_API_BASE = "http+unix://%2Frun%2Fuser%2F1000%2Fpodman%2Fpodman.sock/v5.4.2"
|
|
WORKLOADS_DIR = "/app/workloads"
|
|
|
|
@app.on_event("startup")
|
|
async def _startup_stats_poller():
|
|
await start_stats_poller()
|
|
|
|
# --- ADAPTERS (contract-neutral helpers) ---
|
|
# Centralize Podman socket and systemctl invocation.
|
|
# MUST NOT change endpoint outputs, status codes, or side-effects.
|
|
|
|
def _podman_get_json(url: str):
|
|
return SESSION.get(url).json()
|
|
|
|
def _podman_get_text(url: str) -> str:
|
|
return SESSION.get(url).text
|
|
|
|
def _podman_post(url: str, **kwargs):
|
|
return SESSION.post(url, **kwargs)
|
|
|
|
def _podman_action_post(kind: str, name: str, action: str):
|
|
if kind == "pods":
|
|
url = f"{PODMAN_API_BASE}/libpod/pods/{name}/{action}"
|
|
else:
|
|
url = f"{PODMAN_API_BASE}/libpod/containers/{name}/{action}"
|
|
return _podman_post(url)
|
|
|
|
def _podman_delete(url: str):
|
|
return SESSION.delete(url)
|
|
|
|
def _systemctl(cmd):
|
|
# Proxy to existing run() to avoid behavioral changes.
|
|
return run(cmd)
|
|
|
|
def _run_systemctl_action(action: str, unit: str):
|
|
cmd = ["systemctl", "--user", action, unit]
|
|
return _systemctl(cmd)
|
|
|
|
@app.get("/health")
|
|
def health():
|
|
podman_ok = False
|
|
try:
|
|
r = SESSION.get(f"{PODMAN_API_BASE}/libpod/info", timeout=2)
|
|
if r.status_code == 200:
|
|
try:
|
|
r.json()
|
|
podman_ok = True
|
|
except Exception:
|
|
podman_ok = False
|
|
except Exception:
|
|
podman_ok = False
|
|
|
|
systemd_reachable = False
|
|
try:
|
|
res = subprocess.run(
|
|
["systemctl", "--user", "list-units", "--no-pager", "--no-legend"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
timeout=2,
|
|
)
|
|
systemd_reachable = (res.returncode == 0)
|
|
except Exception:
|
|
systemd_reachable = False
|
|
|
|
ok = podman_ok and systemd_reachable
|
|
return {"ok": ok, "podman": {"ok": podman_ok}, "systemd_user": {"reachable": systemd_reachable}}
|
|
|
|
|
|
# --- DASHBOARD HELPERS (contract-neutral, no ordering/sorting changes) ---
|
|
|
|
def _build_pod_to_containers_map(containers: list):
|
|
# preserves original order of containers processing; no sorting added
|
|
pod_to_containers = {}
|
|
for c in containers:
|
|
pod_name = c.get("PodName") or ""
|
|
if pod_name:
|
|
pod_to_containers.setdefault(pod_name, []).append((c.get("Names") or ["?"])[0])
|
|
return pod_to_containers
|
|
|
|
|
|
def _map_pod_to_unit(podname: str) -> str | None:
|
|
"""
|
|
HOTFIX 3.1 FIX 1:
|
|
If podname starts with "pod", map to <rest>.service (e.g. podmediaserver -> mediaserver.service)
|
|
Else: <podname>.service
|
|
"""
|
|
if not podname:
|
|
return None
|
|
if podname.startswith("pod"):
|
|
return f"{podname[3:]}.service"
|
|
return f"{podname}.service"
|
|
|
|
|
|
def _systemd_then_podman(systemd_callable, podman_callable):
|
|
systemd_res = systemd_callable()
|
|
if systemd_res is not None:
|
|
if isinstance(systemd_res, dict) and systemd_res.get("exit", 1) == 0:
|
|
return systemd_res
|
|
return podman_callable(systemd_res)
|
|
return podman_callable(None)
|
|
|
|
|
|
# --- ROUTERS ---
|
|
# Images API lives in dedicated modules to keep this file from growing further.
|
|
app.include_router(init_images_router(SESSION, PODMAN_API_BASE))
|
|
app.include_router(init_files_router(SESSION, PODMAN_API_BASE, WORKLOADS_DIR))
|
|
app.include_router(init_networks_router(SESSION, PODMAN_API_BASE))
|
|
app.include_router(init_containers_router(
|
|
SESSION,
|
|
PODMAN_API_BASE,
|
|
WORKLOADS_DIR,
|
|
_podman_get_json,
|
|
_podman_get_text,
|
|
_podman_post,
|
|
_podman_action_post,
|
|
_podman_delete,
|
|
_systemctl,
|
|
_systemd_then_podman,
|
|
_map_pod_to_unit,
|
|
_build_pod_to_containers_map,
|
|
))
|
|
app.include_router(init_pods_router(
|
|
SESSION,
|
|
PODMAN_API_BASE,
|
|
WORKLOADS_DIR,
|
|
_podman_get_json,
|
|
_podman_post,
|
|
_podman_delete,
|
|
_systemctl,
|
|
_podman_action_post,
|
|
_map_pod_to_unit,
|
|
_systemd_then_podman,
|
|
_build_pod_to_containers_map,
|
|
))
|
|
|
|
|
|
@app.get("/test-hybrid")
|
|
def test_hybrid():
|
|
# 1. Check filesystem
|
|
try:
|
|
bestanden = []
|
|
for root, _, files in os.walk(WORKLOADS_DIR):
|
|
for f in files:
|
|
bestanden.append(os.path.join(root, f))
|
|
except Exception as e:
|
|
bestanden = f"FS Fout: {str(e)}"
|
|
|
|
# 2. Check Podman API
|
|
try:
|
|
api_containers = _podman_get_json(f"{PODMAN_API_BASE}/libpod/containers/json?all=true")
|
|
except Exception as e:
|
|
api_containers = f"API Fout: {str(e)}"
|
|
|
|
return {
|
|
"bestanden_gevonden": bestanden if isinstance(bestanden, list) else [],
|
|
"api_containers_aantal": len(api_containers) if isinstance(api_containers, list) else -1,
|
|
"api_raw_sample": api_containers[0] if isinstance(api_containers, list) and api_containers else api_containers,
|
|
}
|
|
|
|
|
|
@app.post("/daemon-reload")
|
|
def api_daemon_reload():
|
|
try:
|
|
code, out = _systemctl(["systemctl", "--user", "daemon-reload"])
|
|
return {
|
|
"cmd": "systemctl --user daemon-reload",
|
|
"exit": code,
|
|
"output": out,
|
|
}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@app.post("/{action}/{unit}")
|
|
def api_action(action: str, unit: str):
|
|
if action not in ("status", "start", "stop", "restart"):
|
|
raise HTTPException(status_code=400, detail="Invalid action")
|
|
cmd = ["systemctl", "--user", action, unit]
|
|
code, out = _run_systemctl_action(action, unit)
|
|
return {"cmd": " ".join(cmd), "exit": code, "output": out}
|
|
|
|
|
|
@app.post("/api/<action>/<unit>")
|
|
def legacy_api_action(action: str, unit: str):
|
|
# legacy flask-like path; keep behavior (even if not used by index.html)
|
|
if action not in ("status", "start", "stop", "restart"):
|
|
return {"error": "Invalid action"}, 400
|
|
cmd = ["systemctl", "--user", action, unit]
|
|
code, out = _run_systemctl_action(action, unit)
|
|
return {"cmd": " ".join(cmd), "exit": code, "output": out}
|
|
|
|
|
|
def run(cmd):
|
|
try:
|
|
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
|
output = (result.stdout or "") + (result.stderr or "")
|
|
return result.returncode, output.strip()
|
|
except Exception as e:
|
|
return 1, str(e)
|
|
|
|
if __name__ == "__main__":
|
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|