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:
+18
-7
@@ -1,11 +1,22 @@
|
||||
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")
|
||||
def health():
|
||||
return {"status": "ok"}
|
||||
app = FastAPI(title='NetMapper Backend', version='0.1.0')
|
||||
|
||||
@app.get("/api/v1")
|
||||
def root():
|
||||
return {"service": "netmapper-backend", "version": "0.1.0"}
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
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