Compare commits
5 Commits
2c5cb07cdb
...
e922cea167
| Author | SHA1 | Date | |
|---|---|---|---|
| e922cea167 | |||
| 7d2c205930 | |||
| 580c301718 | |||
| 1c61854143 | |||
| bacab3b20a |
@@ -1,143 +0,0 @@
|
|||||||
Chat export (zichtbare user-assistant conversatie)
|
|
||||||
Datum: 2026-03-04
|
|
||||||
|
|
||||||
User:
|
|
||||||
Onderzoek de mogelikhein om in het container overzicht in het three-dot menu naast elke container(dus niet pod) De podman exec -it functionaliteit toe te voegen. Je hebt mijn toestemming om zelf curls op http://host.containers.internal:8081 uit te voeren met escalated execution
|
|
||||||
|
|
||||||
Assistant:
|
|
||||||
Ik ga eerst de huidige backend- en WebUI-implementatie van het container-overzicht en het three-dot menu in kaart brengen, daarna test ik met curl welke API-capaciteiten al beschikbaar zijn voor exec zodat ik een minimale, backward-compatible aanpak kan voorstellen.
|
|
||||||
|
|
||||||
Assistant (samenvatting):
|
|
||||||
- Relevante code gevonden in control/app_containers.py en webui/html/assets/js/tabs/containers.js.
|
|
||||||
- OpenAPI en live endpoints gecontroleerd via curl.
|
|
||||||
- Bevestigd dat er nog geen exec-endpoint bestond.
|
|
||||||
|
|
||||||
User:
|
|
||||||
Ik wil eerst backend-sessie + curl-werkend, doen nog helemaal niets aan de ui. Maak een kort implementatie plan en toen welke bestande je gaat aanpassen. Nog geen apply, nog niet doorvoeren
|
|
||||||
|
|
||||||
Assistant:
|
|
||||||
Korte plan + bestanden:
|
|
||||||
1. Scope backend-only.
|
|
||||||
2. Nieuwe endpoints voor exec start/stream/input/resize/stop.
|
|
||||||
3. In-memory sessiemanager.
|
|
||||||
4. Foutafhandeling/guardrails.
|
|
||||||
5. Curl-validatie.
|
|
||||||
Bestanden: control/app_containers.py (primair), control/common.py (alleen indien nodig), control/app.py (liefst niet).
|
|
||||||
|
|
||||||
User:
|
|
||||||
Doe nog een keer goed odnerzoek of het wel haalbaar is want "podman auto-update --dry-run" werkt bijvoorbeeld niet in de api container
|
|
||||||
|
|
||||||
Assistant (samenvatting onderzoek):
|
|
||||||
- podman CLI ontbreekt in backend image/runtime (command not found).
|
|
||||||
- Backend kan wel met Podman REST via socket praten (health/test-hybrid/containers endpoints).
|
|
||||||
- Conclusie: CLI-route niet haalbaar zonder imagewijziging; REST-gebaseerde exec-sessie wel haalbaar.
|
|
||||||
|
|
||||||
User:
|
|
||||||
Nog geen akkoord, vertel eerst wat je gaat doen met de REST-gebaseerde exec-sessie (backend + curl)
|
|
||||||
|
|
||||||
Assistant:
|
|
||||||
Plan REST-gebaseerd:
|
|
||||||
1. In-memory sessies.
|
|
||||||
2. POST /containers/{name}/exec/start.
|
|
||||||
3. GET /containers/exec/{session_id}/stream (SSE), POST input, POST resize.
|
|
||||||
4. POST stop.
|
|
||||||
5. Strikte validatie + backward compatibility.
|
|
||||||
6. Curl tests.
|
|
||||||
|
|
||||||
User:
|
|
||||||
Doe eerst de validatie nog geen implementatie akkoord
|
|
||||||
|
|
||||||
Assistant (samenvatting validatie):
|
|
||||||
- Socketpad en runtime bevestigd via inspect.
|
|
||||||
- Directe host-socket test vanuit agent-namespace beperkt.
|
|
||||||
- Conclusie bleef: REST-route haalbaar, maar laatste 100%-check via host-curl gewenst.
|
|
||||||
|
|
||||||
User:
|
|
||||||
"Als je wilt, doe ik nu alleen die laatste read-only socket-validatie (nog steeds zonder implementatie)." Ja
|
|
||||||
|
|
||||||
Assistant:
|
|
||||||
Read-only socket-curl set gegeven voor host:
|
|
||||||
- _ping/version/info
|
|
||||||
- containers list
|
|
||||||
- exec create
|
|
||||||
- exec inspect
|
|
||||||
- exec resize
|
|
||||||
(optioneel exec start)
|
|
||||||
|
|
||||||
User (host outputs):
|
|
||||||
- Exec create gaf Id.
|
|
||||||
- Exec inspect werkte.
|
|
||||||
- Resize vóór start gaf expected 500 state improper.
|
|
||||||
- Exec start gaf 200 raw-stream.
|
|
||||||
|
|
||||||
Assistant:
|
|
||||||
Conclusie: haalbaarheid bevestigd. Start->resize volgorde belangrijk. Klaar voor implementatie.
|
|
||||||
|
|
||||||
User:
|
|
||||||
akkoord
|
|
||||||
|
|
||||||
Assistant (implementatie):
|
|
||||||
- Backend-only exec sessie toegevoegd in control/app_containers.py.
|
|
||||||
- Nieuwe endpoints:
|
|
||||||
- POST /containers/{name}/exec/start
|
|
||||||
- GET /containers/exec/{session_id}
|
|
||||||
- GET /containers/exec/{session_id}/stream
|
|
||||||
- POST /containers/exec/{session_id}/input
|
|
||||||
- POST /containers/exec/{session_id}/resize
|
|
||||||
- POST /containers/exec/{session_id}/stop
|
|
||||||
- Compile checks OK.
|
|
||||||
- Op actieve runtime eerst 404 tot backend redeploy (verwacht).
|
|
||||||
|
|
||||||
User:
|
|
||||||
ja
|
|
||||||
|
|
||||||
Assistant:
|
|
||||||
Deploy/run stappen gegeven (build image, restart backend container, curl tests).
|
|
||||||
|
|
||||||
User (testresultaten):
|
|
||||||
- OpenAPI toont nieuwe exec routes.
|
|
||||||
- start/stream/input/resize/stop werken.
|
|
||||||
- Ping bleef doorlopen in terminal 1.
|
|
||||||
|
|
||||||
Assistant:
|
|
||||||
Uitleg: twee verschillende session_id’s gebruikt; stop de juiste sessie vanuit andere terminal.
|
|
||||||
|
|
||||||
User:
|
|
||||||
Kan niet in terminal 1 want ik kan niets invoeren, ping zit in soort loop
|
|
||||||
|
|
||||||
Assistant:
|
|
||||||
Gebruik andere terminal of Ctrl+C in stream-terminal.
|
|
||||||
|
|
||||||
User:
|
|
||||||
Ja curl .../stop werkt in een andere terminal :)
|
|
||||||
|
|
||||||
Assistant:
|
|
||||||
Bevestigd. Volgende stap: UI (1 menu-item, 2 modal terminal, 3 resize).
|
|
||||||
|
|
||||||
User:
|
|
||||||
Als 1, 2 en drie klaar zijn kan ik dan in een terminal windows vanuit de webui? (nog niets implementeren)
|
|
||||||
|
|
||||||
Assistant:
|
|
||||||
Ja, met 1/2/3 heb je functionele interactieve web-terminal (basis), met optionele latere polish.
|
|
||||||
|
|
||||||
User:
|
|
||||||
Aplly 1, 2 en 3
|
|
||||||
|
|
||||||
Assistant (UI implementatie):
|
|
||||||
- Exec shell toegevoegd aan container three-dot menu (niet pod).
|
|
||||||
- Terminal modal toegevoegd in index.html.
|
|
||||||
- Frontend logic toegevoegd in containers.js:
|
|
||||||
- session start
|
|
||||||
- SSE stream
|
|
||||||
- input verzenden
|
|
||||||
- stop bij modal sluiten
|
|
||||||
- resize via ResizeObserver
|
|
||||||
- CSS styling toegevoegd in app.css.
|
|
||||||
- JS syntax check OK.
|
|
||||||
|
|
||||||
User:
|
|
||||||
Kun je deze gehel chat in en text bestand zetten, zodat ik deze kan bewaren?
|
|
||||||
|
|
||||||
Assistant:
|
|
||||||
Ik zet een export in een tekstbestand.
|
|
||||||
|
|
||||||
+1
-9
@@ -1,4 +1,3 @@
|
|||||||
import subprocess
|
|
||||||
from app_images import init_images_router
|
from app_images import init_images_router
|
||||||
from app_files import init_files_router
|
from app_files import init_files_router
|
||||||
from app_pods import init_pods_router
|
from app_pods import init_pods_router
|
||||||
@@ -9,6 +8,7 @@ from fastapi import FastAPI
|
|||||||
import requests_unixsocket
|
import requests_unixsocket
|
||||||
from common import (
|
from common import (
|
||||||
_systemctl as _common_systemctl,
|
_systemctl as _common_systemctl,
|
||||||
|
run,
|
||||||
)
|
)
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
@@ -44,13 +44,5 @@ app.include_router(init_pods_router(
|
|||||||
app.include_router(init_system_router(SESSION, PODMAN_API_BASE, WORKLOADS_DIR))
|
app.include_router(init_system_router(SESSION, PODMAN_API_BASE, WORKLOADS_DIR))
|
||||||
|
|
||||||
|
|
||||||
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__":
|
if __name__ == "__main__":
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from fastapi import APIRouter, HTTPException
|
|||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from common import (
|
from common import (
|
||||||
|
_helper_call,
|
||||||
_map_pod_to_unit,
|
_map_pod_to_unit,
|
||||||
_podman_action_post,
|
_podman_action_post,
|
||||||
_podman_get_json,
|
_podman_get_json,
|
||||||
@@ -563,7 +564,7 @@ def init_containers_router(
|
|||||||
|
|
||||||
def _systemd_call():
|
def _systemd_call():
|
||||||
if name in defined:
|
if name in defined:
|
||||||
code, out = systemctl_func(["systemctl", "--user", action, name])
|
code, out = _helper_call(action, f"{name}.service")
|
||||||
_sys["code"] = code
|
_sys["code"] = code
|
||||||
_sys["out"] = out
|
_sys["out"] = out
|
||||||
if code == 0:
|
if code == 0:
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ def init_files_router(session, podman_api_base: str, workloads_dir: str) -> APIR
|
|||||||
|
|
||||||
@router.get("/workloads/read/{filename:path}")
|
@router.get("/workloads/read/{filename:path}")
|
||||||
def read_workload(filename: str):
|
def read_workload(filename: str):
|
||||||
path = os.path.join(workloads_dir, filename)
|
path = _files_safe_join(filename)
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
raise HTTPException(404)
|
raise HTTPException(404)
|
||||||
with open(path, 'r') as f:
|
with open(path, 'r') as f:
|
||||||
@@ -55,7 +55,7 @@ def init_files_router(session, podman_api_base: str, workloads_dir: str) -> APIR
|
|||||||
def save_workload_file(data: dict):
|
def save_workload_file(data: dict):
|
||||||
path = data.get("path")
|
path = data.get("path")
|
||||||
content = data.get("content")
|
content = data.get("content")
|
||||||
full_path = os.path.join(workloads_dir, path)
|
full_path = _files_safe_join(path)
|
||||||
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
||||||
with open(full_path, "w") as f:
|
with open(full_path, "w") as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
@@ -63,7 +63,7 @@ def init_files_router(session, podman_api_base: str, workloads_dir: str) -> APIR
|
|||||||
|
|
||||||
@router.post("/workloads/deploy/{filename:path}")
|
@router.post("/workloads/deploy/{filename:path}")
|
||||||
def deploy_workload(filename: str):
|
def deploy_workload(filename: str):
|
||||||
path = os.path.join(workloads_dir, filename)
|
path = _files_safe_join(filename)
|
||||||
with open(path, 'r') as f:
|
with open(path, 'r') as f:
|
||||||
yaml_content = f.read()
|
yaml_content = f.read()
|
||||||
url = f"{podman_api_base}/libpod/kube/play"
|
url = f"{podman_api_base}/libpod/kube/play"
|
||||||
|
|||||||
+5
-17
@@ -3,8 +3,10 @@ import subprocess
|
|||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
from common import (
|
from common import (
|
||||||
|
_helper_call,
|
||||||
_podman_get_json as _common_podman_get_json,
|
_podman_get_json as _common_podman_get_json,
|
||||||
_systemctl as _common_systemctl,
|
_systemctl as _common_systemctl,
|
||||||
|
run,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -68,14 +70,6 @@ def init_system_router(session, podman_api_base: str, workloads_dir: str) -> API
|
|||||||
"api_raw_sample": api_containers[0] if isinstance(api_containers, list) and api_containers else api_containers,
|
"api_raw_sample": api_containers[0] if isinstance(api_containers, list) and api_containers else api_containers,
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
def _systemctl(cmd):
|
def _systemctl(cmd):
|
||||||
return _common_systemctl(cmd, run)
|
return _common_systemctl(cmd, run)
|
||||||
|
|
||||||
@@ -95,20 +89,14 @@ def init_system_router(session, podman_api_base: str, workloads_dir: str) -> API
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
@router.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}
|
|
||||||
|
|
||||||
@router.post("/{action}/{unit}")
|
@router.post("/{action}/{unit}")
|
||||||
def api_action(action: str, unit: str):
|
def api_action(action: str, unit: str):
|
||||||
if action not in ("status", "start", "stop", "restart"):
|
if action not in ("status", "start", "stop", "restart"):
|
||||||
raise HTTPException(status_code=400, detail="Invalid action")
|
raise HTTPException(status_code=400, detail="Invalid action")
|
||||||
cmd = ["systemctl", "--user", action, unit]
|
cmd = ["systemctl", "--user", action, unit]
|
||||||
|
if action in ("start", "stop", "restart"):
|
||||||
|
code, out = _helper_call(action, unit)
|
||||||
|
else:
|
||||||
code, out = _run_systemctl_action(action, unit)
|
code, out = _run_systemctl_action(action, unit)
|
||||||
return {"cmd": " ".join(cmd), "exit": code, "output": out}
|
return {"cmd": " ".join(cmd), "exit": code, "output": out}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,44 @@
|
|||||||
|
import json
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
HELPER_SOCKET = "/run/podman-helper.sock"
|
||||||
|
|
||||||
|
|
||||||
|
def _helper_call(action: str, unit: str) -> tuple[int, str]:
|
||||||
|
"""Stuur start/stop/restart naar de host-helper via Unix socket.
|
||||||
|
Returntype identiek aan run(): (returncode, output)."""
|
||||||
|
payload = json.dumps({"action": action, "unit": unit}).encode()
|
||||||
|
try:
|
||||||
|
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s:
|
||||||
|
s.settimeout(35)
|
||||||
|
s.connect(HELPER_SOCKET)
|
||||||
|
s.sendall(payload)
|
||||||
|
s.shutdown(socket.SHUT_WR)
|
||||||
|
data = b""
|
||||||
|
while True:
|
||||||
|
chunk = s.recv(4096)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
data += chunk
|
||||||
|
resp = json.loads(data.decode())
|
||||||
|
if resp.get("ok"):
|
||||||
|
return 0, resp.get("output", "")
|
||||||
|
return 1, resp.get("error", "mislukt")
|
||||||
|
except Exception as e:
|
||||||
|
return 1, f"helper niet bereikbaar: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
def _podman_get_json_checked(session, url: str):
|
def _podman_get_json_checked(session, url: str):
|
||||||
r = session.get(url)
|
r = session.get(url)
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
podman-helper.py
|
||||||
|
----------------
|
||||||
|
Unix socket helper die op de HOST draait als de gewone gebruiker.
|
||||||
|
Ontvangt JSON verzoeken van de API container en voert systemctl --user uit.
|
||||||
|
|
||||||
|
Beveiligingsmodel:
|
||||||
|
- Alleen start / stop / restart toegestaan
|
||||||
|
- Alleen .service units
|
||||||
|
- Unit naam mag alleen letters, cijfers, punt, koppelteken en underscore bevatten
|
||||||
|
- Meerdere gelijktijdige verbindingen worden afgehandeld via asyncio
|
||||||
|
|
||||||
|
Protocol:
|
||||||
|
Inkomend: {"action": "start"|"stop"|"restart", "unit": "naam.service"}
|
||||||
|
Uitkomend: {"ok": true, "output": "..."} of {"ok": false, "error": "..."}
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# ── Configuratie ─────────────────────────────────────────────────────────────
|
||||||
|
SOCKET_PATH = os.getenv(
|
||||||
|
"HELPER_SOCKET",
|
||||||
|
os.path.join(os.getenv("XDG_RUNTIME_DIR", f"/run/user/{os.getuid()}"), "podman-helper.sock")
|
||||||
|
)
|
||||||
|
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
|
||||||
|
TIMEOUT = 30 # seconden maximaal per systemctl aanroep
|
||||||
|
|
||||||
|
# ── Logging ───────────────────────────────────────────────────────────────────
|
||||||
|
logging.basicConfig(
|
||||||
|
level = getattr(logging, LOG_LEVEL.upper(), logging.INFO),
|
||||||
|
format = "%(asctime)s [%(levelname)s] %(message)s",
|
||||||
|
datefmt = "%Y-%m-%d %H:%M:%S",
|
||||||
|
stream = sys.stdout,
|
||||||
|
)
|
||||||
|
log = logging.getLogger("podman-helper")
|
||||||
|
|
||||||
|
# ── Whitelist ─────────────────────────────────────────────────────────────────
|
||||||
|
ALLOWED_ACTIONS = {"start", "stop", "restart"}
|
||||||
|
UNIT_PATTERN = re.compile(r'^[a-zA-Z0-9._\-]+\.service$')
|
||||||
|
|
||||||
|
|
||||||
|
def validate(action: str, unit: str) -> str | None:
|
||||||
|
"""Geeft een foutmelding terug als het verzoek niet toegestaan is, anders None."""
|
||||||
|
if action not in ALLOWED_ACTIONS:
|
||||||
|
return f"Actie '{action}' niet toegestaan. Gebruik: {', '.join(sorted(ALLOWED_ACTIONS))}"
|
||||||
|
if not UNIT_PATTERN.match(unit):
|
||||||
|
return f"Ongeldige unit naam '{unit}'. Alleen .service units met veilige tekens."
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def run_systemctl(action: str, unit: str) -> dict:
|
||||||
|
"""Voert systemctl --user <action> <unit> uit en geeft het resultaat terug."""
|
||||||
|
cmd = ["systemctl", "--user", action, unit]
|
||||||
|
log.info("Uitvoeren: %s", " ".join(cmd))
|
||||||
|
|
||||||
|
try:
|
||||||
|
proc = await asyncio.create_subprocess_exec(
|
||||||
|
*cmd,
|
||||||
|
stdout = asyncio.subprocess.PIPE,
|
||||||
|
stderr = asyncio.subprocess.PIPE,
|
||||||
|
)
|
||||||
|
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=TIMEOUT)
|
||||||
|
rc = proc.returncode
|
||||||
|
|
||||||
|
output = stdout.decode().strip() or stderr.decode().strip()
|
||||||
|
|
||||||
|
if rc == 0:
|
||||||
|
log.info("OK: %s %s (rc=0)", action, unit)
|
||||||
|
return {"ok": True, "output": output or f"{unit} {action} geslaagd"}
|
||||||
|
else:
|
||||||
|
log.warning("Mislukt: %s %s (rc=%d) %s", action, unit, rc, output)
|
||||||
|
return {"ok": False, "error": output or f"{unit} {action} mislukt (rc={rc})"}
|
||||||
|
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
log.error("Timeout na %ds: %s %s", TIMEOUT, action, unit)
|
||||||
|
return {"ok": False, "error": f"Timeout na {TIMEOUT} seconden"}
|
||||||
|
except Exception as e:
|
||||||
|
log.error("Onverwachte fout: %s", e)
|
||||||
|
return {"ok": False, "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
|
||||||
|
"""Verwerkt één client verbinding."""
|
||||||
|
peer = writer.get_extra_info("peername") or "onbekend"
|
||||||
|
log.debug("Verbinding van: %s", peer)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Lees tot maximaal 4KB (meer dan genoeg voor een JSON verzoek)
|
||||||
|
data = await asyncio.wait_for(reader.read(4096), timeout=10)
|
||||||
|
if not data:
|
||||||
|
return
|
||||||
|
|
||||||
|
# JSON parsen
|
||||||
|
try:
|
||||||
|
request = json.loads(data.decode())
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
log.warning("Ongeldige JSON: %s", e)
|
||||||
|
response = {"ok": False, "error": f"Ongeldige JSON: {e}"}
|
||||||
|
writer.write(json.dumps(response).encode())
|
||||||
|
await writer.drain()
|
||||||
|
return
|
||||||
|
|
||||||
|
action = str(request.get("action", "")).strip().lower()
|
||||||
|
unit = str(request.get("unit", "")).strip()
|
||||||
|
|
||||||
|
# Valideren
|
||||||
|
error = validate(action, unit)
|
||||||
|
if error:
|
||||||
|
log.warning("Afgewezen: %s", error)
|
||||||
|
response = {"ok": False, "error": error}
|
||||||
|
writer.write(json.dumps(response).encode())
|
||||||
|
await writer.drain()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Uitvoeren
|
||||||
|
response = await run_systemctl(action, unit)
|
||||||
|
writer.write(json.dumps(response).encode())
|
||||||
|
await writer.drain()
|
||||||
|
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
log.warning("Client timeout bij lezen")
|
||||||
|
response = {"ok": False, "error": "Timeout bij lezen verzoek"}
|
||||||
|
try:
|
||||||
|
writer.write(json.dumps(response).encode())
|
||||||
|
await writer.drain()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
log.error("Fout bij verwerken verbinding: %s", e)
|
||||||
|
try:
|
||||||
|
response = {"ok": False, "error": str(e)}
|
||||||
|
writer.write(json.dumps(response).encode())
|
||||||
|
await writer.drain()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
writer.close()
|
||||||
|
await writer.wait_closed()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
# Ruim oude socket op als die nog bestaat
|
||||||
|
if os.path.exists(SOCKET_PATH):
|
||||||
|
os.unlink(SOCKET_PATH)
|
||||||
|
log.info("Oude socket verwijderd: %s", SOCKET_PATH)
|
||||||
|
|
||||||
|
# Zorg dat de map bestaat
|
||||||
|
os.makedirs(os.path.dirname(SOCKET_PATH), exist_ok=True)
|
||||||
|
|
||||||
|
server = await asyncio.start_unix_server(handle_client, path=SOCKET_PATH)
|
||||||
|
|
||||||
|
# Socket alleen leesbaar voor eigenaar (de kodi user)
|
||||||
|
os.chmod(SOCKET_PATH, 0o600)
|
||||||
|
|
||||||
|
log.info("podman-helper gestart op %s", SOCKET_PATH)
|
||||||
|
log.info("Toegestane acties: %s", ", ".join(sorted(ALLOWED_ACTIONS)))
|
||||||
|
|
||||||
|
async with server:
|
||||||
|
await server.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
asyncio.run(main())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
log.info("Gestopt")
|
||||||
|
finally:
|
||||||
|
if os.path.exists(SOCKET_PATH):
|
||||||
|
os.unlink(SOCKET_PATH)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Podman systemctl helper socket service
|
||||||
|
Documentation=man:systemctl(1)
|
||||||
|
After=default.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=3s
|
||||||
|
|
||||||
|
Environment=XDG_RUNTIME_DIR=/run/user/%U
|
||||||
|
Environment=LOG_LEVEL=INFO
|
||||||
|
|
||||||
|
ExecStart=/usr/bin/python3 %h/.config/podman-mvp/podman-helper/podman-helper.py
|
||||||
|
ExecStopPost=-/bin/rm -f ${XDG_RUNTIME_DIR}/podman-helper.sock
|
||||||
|
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
Executable
+84
@@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# test-helper.sh — Test de podman-helper direct op de host
|
||||||
|
# Gebruik: ./test-helper.sh <unit> (standaard: test-web.service)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
UNIT="${1:-test-web.service}"
|
||||||
|
SOCKET="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/podman-helper.sock"
|
||||||
|
|
||||||
|
GREEN='\033[0;32m'; RED='\033[0;31m'; NC='\033[0m'
|
||||||
|
ok() { echo -e "${GREEN}✓${NC} $*"; }
|
||||||
|
fail() { echo -e "${RED}✗${NC} $*"; }
|
||||||
|
|
||||||
|
send() {
|
||||||
|
local action="$1"
|
||||||
|
local result
|
||||||
|
result=$(echo "{\"action\": \"$action\", \"unit\": \"$UNIT\"}" | \
|
||||||
|
socat - UNIX-CONNECT:"$SOCKET" 2>/dev/null)
|
||||||
|
echo "$result"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Socket: $SOCKET"
|
||||||
|
echo "Unit: $UNIT"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check socat
|
||||||
|
command -v socat &>/dev/null || { echo "socat niet gevonden — installeer: sudo apt install socat"; exit 1; }
|
||||||
|
|
||||||
|
# Check socket
|
||||||
|
[[ -S "$SOCKET" ]] || { fail "Socket niet gevonden. Is podman-helper.service actief?"; exit 1; }
|
||||||
|
|
||||||
|
# ── Test 1: stop ──────────────────────────────────────────────────────────────
|
||||||
|
echo "Test 1: stop"
|
||||||
|
systemctl --user start "$UNIT" 2>/dev/null || true
|
||||||
|
sleep 2
|
||||||
|
RESULT=$(send "stop")
|
||||||
|
echo " Response: $RESULT"
|
||||||
|
sleep 5
|
||||||
|
STATE=$(systemctl --user is-active "$UNIT" 2>/dev/null || true)
|
||||||
|
echo " State na stop: $STATE"
|
||||||
|
[[ "$STATE" == "inactive" ]] && ok "Stop werkt" || fail "Stop mislukt (state: $STATE)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ── Test 2: start ─────────────────────────────────────────────────────────────
|
||||||
|
echo "Test 2: start"
|
||||||
|
RESULT=$(send "start")
|
||||||
|
echo " Response: $RESULT"
|
||||||
|
sleep 5
|
||||||
|
STATE=$(systemctl --user is-active "$UNIT" 2>/dev/null || true)
|
||||||
|
echo " State na start: $STATE"
|
||||||
|
[[ "$STATE" == "active" ]] && ok "Start werkt" || fail "Start mislukt (state: $STATE)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ── Test 3: restart ───────────────────────────────────────────────────────────
|
||||||
|
echo "Test 3: restart"
|
||||||
|
RESULT=$(send "restart")
|
||||||
|
echo " Response: $RESULT"
|
||||||
|
sleep 5
|
||||||
|
STATE=$(systemctl --user is-active "$UNIT" 2>/dev/null || true)
|
||||||
|
echo " State na restart: $STATE"
|
||||||
|
[[ "$STATE" == "active" ]] && ok "Restart werkt" || fail "Restart mislukt (state: $STATE)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ── Test 4: ongeldige actie (whitelist check) ─────────────────────────────────
|
||||||
|
echo "Test 4: ongeldige actie (whitelist)"
|
||||||
|
RESULT=$(echo '{"action": "kill", "unit": "'"$UNIT"'"}' | \
|
||||||
|
socat - UNIX-CONNECT:"$SOCKET" 2>/dev/null)
|
||||||
|
echo " Response: $RESULT"
|
||||||
|
echo "$RESULT" | grep -q '"ok": false' && ok "Whitelist werkt" || fail "Whitelist werkt NIET"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ── Test 5: gelijktijdige aanvragen ───────────────────────────────────────────
|
||||||
|
echo "Test 5: gelijktijdig (5 status aanvragen)"
|
||||||
|
for i in {1..5}; do
|
||||||
|
echo '{"action": "restart", "unit": "'"$UNIT"'"}' | \
|
||||||
|
socat - UNIX-CONNECT:"$SOCKET" 2>/dev/null &
|
||||||
|
done
|
||||||
|
wait
|
||||||
|
sleep 5
|
||||||
|
STATE=$(systemctl --user is-active "$UNIT" 2>/dev/null || true)
|
||||||
|
echo " State na gelijktijdige aanvragen: $STATE"
|
||||||
|
[[ "$STATE" == "active" ]] && ok "Gelijktijdig werkt" || fail "Gelijktijdig mislukt (state: $STATE)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Tests klaar."
|
||||||
Reference in New Issue
Block a user