From be55c35c3a2f0177ba9956abd85ce0e4e2e9b692 Mon Sep 17 00:00:00 2001 From: Perplexity Bot Date: Mon, 3 Aug 2026 22:13:46 +0000 Subject: [PATCH] Add minimal control agent and systemd unit templates --- README.md | 4 + agent/app/main.py | 112 ++++++++++++++++++++++ agent/requirements.txt | 4 + deploy/systemd/netmapper-agent.service | 16 ++++ deploy/systemd/netmapper-backend.service | 16 ++++ deploy/systemd/netmapper-frontend.service | 16 ++++ deploy/systemd/netmapper-worker.service | 16 ++++ 7 files changed, 184 insertions(+) create mode 100644 agent/app/main.py create mode 100644 agent/requirements.txt create mode 100644 deploy/systemd/netmapper-agent.service create mode 100644 deploy/systemd/netmapper-backend.service create mode 100644 deploy/systemd/netmapper-frontend.service create mode 100644 deploy/systemd/netmapper-worker.service diff --git a/README.md b/README.md index 211e3e6..00eb4c0 100644 --- a/README.md +++ b/README.md @@ -33,3 +33,7 @@ NetMapper รจ una web app self-hosted per la mappatura dinamica di un homelab su - Database: PostgreSQL - Cache/queue: Redis - Deploy: Docker Compose + +### Agent + +- `agent/` - control agent per gestire i servizi systemd (start/stop/status). diff --git a/agent/app/main.py b/agent/app/main.py new file mode 100644 index 0000000..52605de --- /dev/null +++ b/agent/app/main.py @@ -0,0 +1,112 @@ +import os +import systemd.dbus +import dbus +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +from typing import List + +app = FastAPI(title="NetMapper Agent", version="0.1.0") + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # da limitare in produzione + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +ALLOWED_SERVICES = [ + "netmapper-frontend.service", + "netmapper-backend.service", + "netmapper-worker.service", +] + +class ServiceInfo(BaseModel): + name: str + active_state: str + sub_state: str + loaded: str + pid: int | None = None + +class ActionRequest(BaseModel): + mode: str = "sync" + +def get_unit(name: str): + bus = dbus.SystemBus() + systemd = bus.get_object('org.freedesktop.systemd1', '/org/freedesktop/systemd1') + manager = dbus.Interface(systemd, 'org.freedesktop.systemd1.Manager') + unit_path = manager.GetUnit(name) + unit = bus.get_object('org.freedesktop.systemd1', str(unit_path)) + props = dbus.Interface(unit, 'org.freedesktop.DBus.Properties') + return props.GetAll('org.freedesktop.systemd1.Unit') + +def refresh_dbus(): + bus = dbus.SystemBus() + systemd = bus.get_object('org.freedesktop.systemd1', '/org/freedesktop/systemd1') + manager = dbus.Interface(systemd, 'org.freedesktop.systemd1.Manager') + return manager + +@app.get("/health") +def health(): + return {"status": "ok"} + +@app.get("/services", response_model=List[ServiceInfo]) +def list_services(): + bus = dbus.SystemBus() + systemd = bus.get_object('org.freedesktop.systemd1', '/org/freedesktop/systemd1') + manager = dbus.Interface(systemd, 'org.freedesktop.systemd1.Manager') + units = manager.ListUnits() + result = [] + for u in units: + name = u[0] + if name not in ALLOWED_SERVICES: + continue + result.append(ServiceInfo( + name=name, + active_state=u[3], + sub_state=u[4], + loaded=u[2], + pid=None, + )) + return result + +@app.get("/services/{name}") +def get_service(name: str): + if name not in ALLOWED_SERVICES: + raise HTTPException(status_code=403, detail="Service not allowed") + props = get_unit(name) + pid = None + if "MainPID" in props: + pid = int(props["MainPID"]) or None + return ServiceInfo( + name=name, + active_state=str(props.get("ActiveState", "unknown")), + sub_state=str(props.get("SubState", "unknown")), + loaded=str(props.get("LoadState", "unknown")), + pid=pid, + ) + +@app.post("/services/{name}/start") +def start_service(name: str): + if name not in ALLOWED_SERVICES: + raise HTTPException(status_code=403, detail="Service not allowed") + manager = refresh_dbus() + job = manager.StartUnit(name, "replace") + return {"job": str(job), "service": name, "action": "start"} + +@app.post("/services/{name}/stop") +def stop_service(name: str): + if name not in ALLOWED_SERVICES: + raise HTTPException(status_code=403, detail="Service not allowed") + manager = refresh_dbus() + job = manager.StopUnit(name, "replace") + return {"job": str(job), "service": name, "action": "stop"} + +@app.post("/services/{name}/restart") +def restart_service(name: str): + if name not in ALLOWED_SERVICES: + raise HTTPException(status_code=403, detail="Service not allowed") + manager = refresh_dbus() + job = manager.RestartUnit(name, "replace") + return {"job": str(job), "service": name, "action": "restart"} diff --git a/agent/requirements.txt b/agent/requirements.txt new file mode 100644 index 0000000..33ff438 --- /dev/null +++ b/agent/requirements.txt @@ -0,0 +1,4 @@ +fastapi==0.115.0 +uvicorn[standard]==0.30.6 +dbus-python==1.3.2 +pydantic==2.9.2 diff --git a/deploy/systemd/netmapper-agent.service b/deploy/systemd/netmapper-agent.service new file mode 100644 index 0000000..94166a3 --- /dev/null +++ b/deploy/systemd/netmapper-agent.service @@ -0,0 +1,16 @@ +[Unit] +Description=NetMapper Control Agent +After=network.target + +[Service] +Type=simple +User=root +WorkingDirectory=/opt/NetMapper2026/agent +ExecStart=/opt/NetMapper2026/agent/.venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8001 +Restart=on-failure +RestartSec=5 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target diff --git a/deploy/systemd/netmapper-backend.service b/deploy/systemd/netmapper-backend.service new file mode 100644 index 0000000..d3ae50b --- /dev/null +++ b/deploy/systemd/netmapper-backend.service @@ -0,0 +1,16 @@ +[Unit] +Description=NetMapper Backend +After=network.target + +[Service] +Type=simple +User=root +WorkingDirectory=/opt/NetMapper2026/backend +ExecStart=/opt/NetMapper2026/backend/.venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000 +Restart=on-failure +RestartSec=5 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target diff --git a/deploy/systemd/netmapper-frontend.service b/deploy/systemd/netmapper-frontend.service new file mode 100644 index 0000000..e993477 --- /dev/null +++ b/deploy/systemd/netmapper-frontend.service @@ -0,0 +1,16 @@ +[Unit] +Description=NetMapper Frontend +After=network.target + +[Service] +Type=simple +User=root +WorkingDirectory=/opt/NetMapper2026/frontend +ExecStart=/usr/bin/npm run dev -- --host 0.0.0.0 --port 5173 +Restart=on-failure +RestartSec=5 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target diff --git a/deploy/systemd/netmapper-worker.service b/deploy/systemd/netmapper-worker.service new file mode 100644 index 0000000..0b92bce --- /dev/null +++ b/deploy/systemd/netmapper-worker.service @@ -0,0 +1,16 @@ +[Unit] +Description=NetMapper Worker +After=network.target + +[Service] +Type=simple +User=root +WorkingDirectory=/opt/NetMapper2026/worker +ExecStart=/opt/NetMapper2026/worker/.venv/bin/python app/main.py +Restart=on-failure +RestartSec=5 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target