Add Pydantic models and FastAPI router structure for agent and backend
CI / basic-check (push) Has been cancelled

This commit is contained in:
Perplexity Bot
2026-08-04 18:55:54 +00:00
parent dc1b2ab99b
commit 69124b31f1
15 changed files with 546 additions and 111 deletions
+15
View File
@@ -0,0 +1,15 @@
from pydantic import BaseModel
class AgentConfig(BaseModel):
service_name: str = 'netmapper-agent'
version: str = '0.1.0'
monitored_services: list[str] = [
'netmapper-agent.service',
'netmapper-backend.service',
'netmapper-worker.service',
'netmapper-frontend.service',
]
settings = AgentConfig()
+12 -104
View File
@@ -1,112 +1,20 @@
import os
import systemd.dbus
import dbus
from fastapi import FastAPI, HTTPException
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import List
app = FastAPI(title="NetMapper Agent", version="0.1.0")
from app.routers.health import router as health_router
from app.routers.runtime import router as runtime_router
from app.routers.services import router as services_router
app = FastAPI(title='NetMapper Agent', version='0.1.0')
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # da limitare in produzione
allow_origins=['*'],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
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"}
app.include_router(health_router)
app.include_router(runtime_router)
app.include_router(services_router)
+62
View File
@@ -0,0 +1,62 @@
from datetime import datetime
from typing import Dict, List, Literal, Optional
from pydantic import BaseModel, Field
HealthStatus = Literal['pass', 'warn', 'fail']
ActionStatus = Literal['accepted', 'completed', 'failed']
ServiceActiveState = Literal['active', 'reloading', 'inactive', 'failed', 'activating', 'deactivating']
class CheckResult(BaseModel):
status: HealthStatus
latency_ms: Optional[float] = None
detail: Optional[str] = None
class HealthResponse(BaseModel):
status: HealthStatus
service: str
version: Optional[str] = None
time: datetime
uptime_s: Optional[float] = None
checks: Dict[str, CheckResult] = Field(default_factory=dict)
class ServiceState(BaseModel):
name: str
description: Optional[str] = None
active_state: ServiceActiveState
sub_state: str
loaded: str
status: HealthStatus
pid: Optional[int] = None
since: Optional[datetime] = None
health_url: Optional[str] = None
reachable: Optional[bool] = None
notes: Optional[str] = None
class ServiceListResponse(BaseModel):
count: int
time: datetime
items: List[ServiceState]
class RuntimeResponse(BaseModel):
status: HealthStatus
service: str
version: Optional[str] = None
time: datetime
hostname: str
uptime_s: Optional[float] = None
services: List[ServiceState]
checks: Dict[str, CheckResult] = Field(default_factory=dict)
class ServiceActionResponse(BaseModel):
status: ActionStatus
action: Literal['start', 'stop', 'restart']
service: str
job_id: Optional[str] = None
message: Optional[str] = None
time: datetime
+21
View File
@@ -0,0 +1,21 @@
from fastapi import APIRouter
from app.models import HealthResponse
from app.services.runtime_probe import build_health
router = APIRouter(tags=['health'])
@router.get('/live', response_model=HealthResponse)
def live() -> HealthResponse:
return build_health()
@router.get('/ready', response_model=HealthResponse)
def ready() -> HealthResponse:
return build_health()
@router.get('/health', response_model=HealthResponse)
def health() -> HealthResponse:
return build_health()
+13
View File
@@ -0,0 +1,13 @@
from fastapi import APIRouter
from app.models import RuntimeResponse
from app.services.runtime_probe import build_runtime
from app.services.systemd_manager import SystemdManager
router = APIRouter(tags=['runtime'])
manager = SystemdManager()
@router.get('/runtime', response_model=RuntimeResponse)
def runtime() -> RuntimeResponse:
return build_runtime(manager)
+42
View File
@@ -0,0 +1,42 @@
from datetime import datetime, timezone
from typing import Literal
from fastapi import APIRouter, HTTPException
from app.config import settings
from app.models import ServiceActionResponse, ServiceListResponse, ServiceState
from app.services.systemd_manager import SystemdManager
router = APIRouter(prefix='/services', tags=['services'])
manager = SystemdManager()
@router.get('', response_model=ServiceListResponse)
def list_services() -> ServiceListResponse:
items = manager.list_services(settings.monitored_services)
return ServiceListResponse(count=len(items), time=datetime.now(timezone.utc), items=items)
@router.get('/{name}', response_model=ServiceState)
def get_service(name: str) -> ServiceState:
items = manager.list_services([name])
if not items:
raise HTTPException(status_code=404, detail='Service not found')
return items[0]
@router.post('/{name}/{action}', response_model=ServiceActionResponse)
def service_action(name: str, action: Literal['start', 'stop', 'restart']) -> ServiceActionResponse:
if action == 'start':
manager.start(name)
elif action == 'stop':
manager.stop(name)
elif action == 'restart':
manager.restart(name)
return ServiceActionResponse(
status='accepted',
action=action,
service=name,
time=datetime.now(timezone.utc),
message=f'{action} requested',
)
+59
View File
@@ -0,0 +1,59 @@
import socket
import time
from datetime import datetime, timezone
from urllib.request import urlopen
from app.config import settings
from app.models import CheckResult, HealthResponse, RuntimeResponse
from app.services.systemd_manager import SystemdManager
START_TIME = time.time()
HEALTH_TARGETS = {
'backend': 'http://127.0.0.1:8000/health',
'worker': None,
'frontend': 'http://127.0.0.1:5173/',
}
def probe_http(url: str | None) -> CheckResult:
if not url:
return CheckResult(status='warn', detail='no health endpoint configured')
try:
started = time.time()
with urlopen(url, timeout=2) as resp:
latency = round((time.time() - started) * 1000, 2)
return CheckResult(status='pass' if resp.status < 400 else 'fail', latency_ms=latency, detail=f'HTTP {resp.status}')
except Exception as exc:
return CheckResult(status='fail', detail=str(exc))
def build_health() -> HealthResponse:
return HealthResponse(
status='pass',
service=settings.service_name,
version=settings.version,
time=datetime.now(timezone.utc),
uptime_s=round(time.time() - START_TIME, 2),
checks={'dbus': CheckResult(status='pass', detail='system bus reachable')},
)
def build_runtime(systemd_manager: SystemdManager) -> RuntimeResponse:
services = systemd_manager.list_services(settings.monitored_services)
checks = {'dbus': CheckResult(status='pass', detail='system bus reachable')}
checks.update({k: probe_http(v) for k, v in HEALTH_TARGETS.items()})
overall = 'pass'
if any(c.status == 'fail' for c in checks.values()) or any(s.status == 'fail' for s in services):
overall = 'fail'
elif any(c.status == 'warn' for c in checks.values()) or any(s.status == 'warn' for s in services):
overall = 'warn'
return RuntimeResponse(
status=overall,
service=settings.service_name,
version=settings.version,
time=datetime.now(timezone.utc),
hostname=socket.gethostname(),
uptime_s=round(time.time() - START_TIME, 2),
services=services,
checks=checks,
)
+64
View File
@@ -0,0 +1,64 @@
from datetime import datetime, timezone
from typing import List
import dbus
from app.models import ServiceState
class SystemdManager:
def __init__(self) -> None:
self.bus = dbus.SystemBus()
systemd = self.bus.get_object('org.freedesktop.systemd1', '/org/freedesktop/systemd1')
self.manager = dbus.Interface(systemd, 'org.freedesktop.systemd1.Manager')
def list_services(self, names: list[str]) -> List[ServiceState]:
items: list[ServiceState] = []
for name in names:
try:
unit_path = self.manager.GetUnit(name)
unit = self.bus.get_object('org.freedesktop.systemd1', unit_path)
props = dbus.Interface(unit, 'org.freedesktop.DBus.Properties')
active_state = str(props.Get('org.freedesktop.systemd1.Unit', 'ActiveState'))
sub_state = str(props.Get('org.freedesktop.systemd1.Unit', 'SubState'))
loaded = str(props.Get('org.freedesktop.systemd1.Unit', 'LoadState'))
description = str(props.Get('org.freedesktop.systemd1.Unit', 'Description'))
try:
pid = int(props.Get('org.freedesktop.systemd1.Service', 'MainPID'))
if pid == 0:
pid = None
except Exception:
pid = None
status = 'pass' if active_state == 'active' else 'warn' if active_state in {'activating', 'deactivating'} else 'fail'
items.append(ServiceState(
name=name,
description=description,
active_state=active_state,
sub_state=sub_state,
loaded=loaded,
status=status,
pid=pid,
since=datetime.now(timezone.utc),
))
except Exception as exc:
items.append(ServiceState(
name=name,
active_state='failed',
sub_state='not-found',
loaded='not-found',
status='fail',
notes=str(exc),
))
return items
def _act(self, name: str, method: str) -> None:
fn = getattr(self.manager, method)
fn(name, 'replace')
def start(self, name: str) -> None:
self._act(name, 'StartUnit')
def stop(self, name: str) -> None:
self._act(name, 'StopUnit')
def restart(self, name: str) -> None:
self._act(name, 'RestartUnit')