Add Pydantic models and FastAPI router structure for agent and backend
CI / basic-check (push) Has been cancelled
CI / basic-check (push) Has been cancelled
This commit is contained in:
@@ -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
@@ -1,112 +1,20 @@
|
|||||||
import os
|
from fastapi import FastAPI
|
||||||
import systemd.dbus
|
|
||||||
import dbus
|
|
||||||
from fastapi import FastAPI, HTTPException
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
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(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["*"], # da limitare in produzione
|
allow_origins=['*'],
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=['*'],
|
||||||
allow_headers=["*"],
|
allow_headers=['*'],
|
||||||
)
|
)
|
||||||
|
|
||||||
ALLOWED_SERVICES = [
|
app.include_router(health_router)
|
||||||
"netmapper-frontend.service",
|
app.include_router(runtime_router)
|
||||||
"netmapper-backend.service",
|
app.include_router(services_router)
|
||||||
"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"}
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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()
|
||||||
@@ -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)
|
||||||
@@ -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',
|
||||||
|
)
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -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')
|
||||||
+18
-7
@@ -1,11 +1,22 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
app = FastAPI(title="NetMapper API", version="0.1.0")
|
from app.routers.discoveries import router as discoveries_router
|
||||||
|
from app.routers.health import router as health_router
|
||||||
|
from app.routers.jobs import router as jobs_router
|
||||||
|
from app.routers.objects import router as objects_router
|
||||||
|
|
||||||
@app.get("/health")
|
app = FastAPI(title='NetMapper Backend', version='0.1.0')
|
||||||
def health():
|
|
||||||
return {"status": "ok"}
|
|
||||||
|
|
||||||
@app.get("/api/v1")
|
app.add_middleware(
|
||||||
def root():
|
CORSMiddleware,
|
||||||
return {"service": "netmapper-backend", "version": "0.1.0"}
|
allow_origins=['*'],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=['*'],
|
||||||
|
allow_headers=['*'],
|
||||||
|
)
|
||||||
|
|
||||||
|
app.include_router(health_router)
|
||||||
|
app.include_router(objects_router)
|
||||||
|
app.include_router(jobs_router)
|
||||||
|
app.include_router(discoveries_router)
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
|
from app.schemas import DiscoveryRun, EntityResponse, ListResponse
|
||||||
|
from app.services.store import DISCOVERY_RUNS
|
||||||
|
|
||||||
|
router = APIRouter(prefix='/discoveries', tags=['discoveries'])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('', response_model=ListResponse[DiscoveryRun])
|
||||||
|
def list_discoveries() -> ListResponse[DiscoveryRun]:
|
||||||
|
return ListResponse(count=len(DISCOVERY_RUNS), items=DISCOVERY_RUNS)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/{discovery_id}', response_model=EntityResponse[DiscoveryRun])
|
||||||
|
def get_discovery(discovery_id: str) -> EntityResponse[DiscoveryRun]:
|
||||||
|
for item in DISCOVERY_RUNS:
|
||||||
|
if item.id == discovery_id:
|
||||||
|
return EntityResponse(item=item)
|
||||||
|
raise HTTPException(status_code=404, detail='Discovery not found')
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import time
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from app.schemas import HealthCheck, HealthResponse
|
||||||
|
|
||||||
|
router = APIRouter(tags=['health'])
|
||||||
|
START_TIME = time.time()
|
||||||
|
|
||||||
|
|
||||||
|
def build_health() -> HealthResponse:
|
||||||
|
return HealthResponse(
|
||||||
|
status='pass',
|
||||||
|
service='netmapper-backend',
|
||||||
|
version='0.1.0',
|
||||||
|
time=datetime.now(timezone.utc),
|
||||||
|
uptime_s=round(time.time() - START_TIME, 2),
|
||||||
|
checks={'db': HealthCheck(status='warn', detail='database not configured yet')},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@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()
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
|
from app.schemas import EntityResponse, Job, ListResponse
|
||||||
|
from app.services.store import JOBS
|
||||||
|
|
||||||
|
router = APIRouter(prefix='/jobs', tags=['jobs'])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('', response_model=ListResponse[Job])
|
||||||
|
def list_jobs() -> ListResponse[Job]:
|
||||||
|
return ListResponse(count=len(JOBS), items=JOBS)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/{job_id}', response_model=EntityResponse[Job])
|
||||||
|
def get_job(job_id: str) -> EntityResponse[Job]:
|
||||||
|
for item in JOBS:
|
||||||
|
if item.id == job_id:
|
||||||
|
return EntityResponse(item=item)
|
||||||
|
raise HTTPException(status_code=404, detail='Job not found')
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
|
from app.schemas import EntityResponse, ListResponse, NetObject
|
||||||
|
from app.services.store import OBJECTS
|
||||||
|
|
||||||
|
router = APIRouter(prefix='/objects', tags=['objects'])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('', response_model=ListResponse[NetObject])
|
||||||
|
def list_objects() -> ListResponse[NetObject]:
|
||||||
|
return ListResponse(count=len(OBJECTS), items=OBJECTS)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/{object_id}', response_model=EntityResponse[NetObject])
|
||||||
|
def get_object(object_id: str) -> EntityResponse[NetObject]:
|
||||||
|
for item in OBJECTS:
|
||||||
|
if item.id == object_id:
|
||||||
|
return EntityResponse(item=item)
|
||||||
|
raise HTTPException(status_code=404, detail='Object not found')
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, Generic, List, Literal, Optional, TypeVar
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
T = TypeVar('T')
|
||||||
|
|
||||||
|
JobStatus = Literal['pending', 'running', 'completed', 'failed', 'cancelled']
|
||||||
|
ObjectStatus = Literal['active', 'planned', 'inactive', 'retired', 'unknown']
|
||||||
|
ObjectType = Literal[
|
||||||
|
'site', 'area', 'rack', 'physical_host', 'vm', 'lxc', 'container',
|
||||||
|
'app', 'switch', 'firewall', 'router', 'ap', 'camera', 'storage', 'other'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class HealthCheck(BaseModel):
|
||||||
|
status: Literal['pass', 'warn', 'fail']
|
||||||
|
latency_ms: Optional[float] = None
|
||||||
|
detail: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class HealthResponse(BaseModel):
|
||||||
|
status: Literal['pass', 'warn', 'fail']
|
||||||
|
service: str
|
||||||
|
version: Optional[str] = None
|
||||||
|
time: datetime
|
||||||
|
uptime_s: Optional[float] = None
|
||||||
|
checks: Dict[str, HealthCheck] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class NetObject(BaseModel):
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
slug: Optional[str] = None
|
||||||
|
type: ObjectType
|
||||||
|
parent_id: Optional[str] = None
|
||||||
|
status: ObjectStatus
|
||||||
|
vendor: Optional[str] = None
|
||||||
|
model: Optional[str] = None
|
||||||
|
serial: Optional[str] = None
|
||||||
|
notes: Optional[str] = None
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class IPAddress(BaseModel):
|
||||||
|
id: str
|
||||||
|
address: str
|
||||||
|
version: Literal[4, 6]
|
||||||
|
subnet_id: Optional[str] = None
|
||||||
|
object_id: Optional[str] = None
|
||||||
|
interface_id: Optional[str] = None
|
||||||
|
hostname: Optional[str] = None
|
||||||
|
source: Literal['manual', 'snmp', 'arp', 'lldp', 'nmap', 'import', 'other']
|
||||||
|
status: Literal['assigned', 'reserved', 'free', 'unknown'] = 'unknown'
|
||||||
|
|
||||||
|
|
||||||
|
class Subnet(BaseModel):
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
cidr: str
|
||||||
|
gateway: Optional[str] = None
|
||||||
|
vlan_id: Optional[int] = None
|
||||||
|
notes: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class Link(BaseModel):
|
||||||
|
id: str
|
||||||
|
source_object_id: str
|
||||||
|
target_object_id: str
|
||||||
|
source_interface_id: Optional[str] = None
|
||||||
|
target_interface_id: Optional[str] = None
|
||||||
|
type: Literal['physical', 'logical', 'hosted_on', 'member_of', 'uplink', 'other']
|
||||||
|
confidence: float = 1.0
|
||||||
|
source: Literal['manual', 'snmp', 'lldp', 'arp', 'import', 'inference']
|
||||||
|
|
||||||
|
|
||||||
|
class Job(BaseModel):
|
||||||
|
id: str
|
||||||
|
type: str
|
||||||
|
status: JobStatus
|
||||||
|
progress: int = 0
|
||||||
|
message: Optional[str] = None
|
||||||
|
error: Optional[str] = None
|
||||||
|
payload: Optional[Dict[str, Any]] = None
|
||||||
|
result: Optional[Dict[str, Any]] = None
|
||||||
|
created_at: datetime
|
||||||
|
started_at: Optional[datetime] = None
|
||||||
|
finished_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoveryProfile(BaseModel):
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
strategy: Literal['fast', 'balanced', 'deep', 'custom']
|
||||||
|
use_icmp: bool = True
|
||||||
|
use_arp: bool = True
|
||||||
|
use_snmp: bool = False
|
||||||
|
use_lldp: bool = False
|
||||||
|
use_portscan: bool = False
|
||||||
|
notes: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoveryRun(BaseModel):
|
||||||
|
id: str
|
||||||
|
profile_id: str
|
||||||
|
subnet_ids: List[str] = Field(default_factory=list)
|
||||||
|
job_id: Optional[str] = None
|
||||||
|
status: JobStatus
|
||||||
|
summary: Optional[Dict[str, Any]] = None
|
||||||
|
created_at: datetime
|
||||||
|
started_at: Optional[datetime] = None
|
||||||
|
finished_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ListResponse(BaseModel, Generic[T]):
|
||||||
|
count: int
|
||||||
|
items: List[T]
|
||||||
|
page: Optional[int] = None
|
||||||
|
page_size: Optional[int] = None
|
||||||
|
total: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
class EntityResponse(BaseModel, Generic[T]):
|
||||||
|
item: T
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from app.schemas import DiscoveryProfile, DiscoveryRun, Job, NetObject
|
||||||
|
|
||||||
|
NOW = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
OBJECTS = [
|
||||||
|
NetObject(id='obj_1', name='Site HQ', slug='site-hq', type='site', status='active', created_at=NOW, updated_at=NOW),
|
||||||
|
NetObject(id='obj_2', name='Rack A', slug='rack-a', type='rack', parent_id='obj_1', status='active', created_at=NOW, updated_at=NOW),
|
||||||
|
NetObject(id='obj_3', name='Proxmox Host', slug='proxmox-host', type='physical_host', parent_id='obj_2', status='active', vendor='Lenovo', model='ThinkCentre', created_at=NOW, updated_at=NOW),
|
||||||
|
]
|
||||||
|
|
||||||
|
JOBS = [
|
||||||
|
Job(id='job_1', type='discovery.scan', status='pending', progress=0, created_at=NOW, payload={'subnet_ids': ['subnet_1'], 'profile_id': 'profile_1'})
|
||||||
|
]
|
||||||
|
|
||||||
|
DISCOVERY_PROFILES = [
|
||||||
|
DiscoveryProfile(id='profile_1', name='Fast LAN', strategy='fast', use_icmp=True, use_arp=True)
|
||||||
|
]
|
||||||
|
|
||||||
|
DISCOVERY_RUNS = [
|
||||||
|
DiscoveryRun(id='disc_1', profile_id='profile_1', subnet_ids=['subnet_1'], job_id='job_1', status='pending', created_at=NOW)
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user