Files
podman-mvp/control/app.py
T

207 lines
6.5 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
from common import (
_build_pod_to_containers_map as _common_build_pod_to_containers_map,
_map_pod_to_unit as _common_map_pod_to_unit,
_podman_action_post as _common_podman_action_post,
_podman_delete as _common_podman_delete,
_podman_get_json as _common_podman_get_json,
_podman_get_json_checked as _common_podman_get_json_checked,
_podman_get_text as _common_podman_get_text,
_podman_post as _common_podman_post,
_systemctl as _common_systemctl,
_systemd_then_podman as _common_systemd_then_podman,
)
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_checked(url: str):
return _common_podman_get_json_checked(SESSION, url)
def _podman_get_json(url: str):
return _common_podman_get_json(SESSION, url)
def _podman_get_text(url: str) -> str:
return _common_podman_get_text(SESSION, url)
def _podman_post(url: str, **kwargs):
return _common_podman_post(SESSION, url, **kwargs)
def _podman_action_post(kind: str, name: str, action: str):
return _common_podman_action_post(SESSION, PODMAN_API_BASE, kind, name, action)
def _podman_delete(url: str):
return _common_podman_delete(SESSION, url)
def _systemctl(cmd):
return _common_systemctl(cmd, run)
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):
return _common_build_pod_to_containers_map(containers)
def _map_pod_to_unit(podname: str) -> str | None:
return _common_map_pod_to_unit(podname)
def _systemd_then_podman(systemd_callable, podman_callable):
return _common_systemd_then_podman(systemd_callable, podman_callable)
# --- 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)