Files
stupid-simple-network-inven…/backend/routers/devices.py
T
olivier 88cf6458d0 Initial commit — Stupid Simple Network Inventory
Application web d'inventaire réseau manuel avec FastAPI, Vue 3 et Docker.
Inclut l'authentification JWT, la découverte ICMP, et la topologie en cards CSS.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 09:19:19 +02:00

177 lines
5.2 KiB
Python

import ipaddress
import re
from typing import Optional, List
from urllib.parse import urlparse
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, field_validator
from sqlalchemy.orm import Session
from database import get_db
import models
router = APIRouter()
_VALID_TYPES = {
"server", "switch", "router", "nas", "gateway", "livebox", "access_point",
"camera", "temperature", "sensor", "hub", "smart_plug", "alarm", "light",
"doorbell", "desktop", "laptop", "other",
}
_VALID_VIRT_TYPES = {None, "baremetal", "lxc", "qemu"}
class InterfaceCreate(BaseModel):
name: str = "eth0"
ip_address: Optional[str] = ""
vlan_id: Optional[int] = None
is_upstream: bool = False
@field_validator("name")
@classmethod
def _name(cls, v: str) -> str:
v = v.strip()
if not v:
raise ValueError("Interface name cannot be empty")
if len(v) > 50:
raise ValueError("Interface name too long (max 50 characters)")
return v
@field_validator("ip_address")
@classmethod
def _ip(cls, v: Optional[str]) -> Optional[str]:
if v:
try:
ipaddress.ip_address(v)
except ValueError:
raise ValueError(f"Invalid IP address: {v!r}")
return v
class InterfaceOut(InterfaceCreate):
id: int
device_id: int
model_config = {"from_attributes": True}
class DeviceCreate(BaseModel):
name: str
type: str = "other"
description: str = ""
is_gateway: bool = False
is_livebox: bool = False
virt_type: Optional[str] = None
url: Optional[str] = None
interfaces: List[InterfaceCreate] = []
@field_validator("name")
@classmethod
def _name(cls, v: str) -> str:
v = v.strip()
if not v:
raise ValueError("name cannot be empty")
if len(v) > 100:
raise ValueError("name too long (max 100 characters)")
return v
@field_validator("description")
@classmethod
def _description(cls, v: str) -> str:
if len(v) > 500:
raise ValueError("description too long (max 500 characters)")
return v
@field_validator("type")
@classmethod
def _type(cls, v: str) -> str:
if v not in _VALID_TYPES:
raise ValueError(f"Invalid type: {v!r}. Must be one of: {sorted(_VALID_TYPES)}")
return v
@field_validator("virt_type")
@classmethod
def _virt_type(cls, v: Optional[str]) -> Optional[str]:
if v not in _VALID_VIRT_TYPES:
raise ValueError(f"Invalid virt_type: {v!r}. Must be one of: baremetal, lxc, qemu")
return v
@field_validator("url")
@classmethod
def _url(cls, v: Optional[str]) -> Optional[str]:
if v:
parsed = urlparse(v)
if parsed.scheme not in ("http", "https") or not parsed.netloc:
raise ValueError("url must be a valid http or https URL")
return v
class DeviceOut(BaseModel):
id: int
name: str
type: str
description: str
is_gateway: bool
is_livebox: bool
virt_type: Optional[str] = None
url: Optional[str] = None
interfaces: List[InterfaceOut] = []
model_config = {"from_attributes": True}
@router.get("/", response_model=List[DeviceOut])
def list_devices(db: Session = Depends(get_db)):
return db.query(models.Device).order_by(models.Device.name).all()
@router.post("/", response_model=DeviceOut)
def create_device(device: DeviceCreate, db: Session = Depends(get_db)):
db_device = models.Device(
name=device.name,
type=device.type,
description=device.description,
is_gateway=device.is_gateway,
is_livebox=device.is_livebox,
virt_type=device.virt_type,
url=device.url,
)
db.add(db_device)
db.flush()
for iface in device.interfaces:
db.add(models.DeviceInterface(device_id=db_device.id, **iface.model_dump()))
db.commit()
db.refresh(db_device)
return db_device
@router.put("/{device_id}", response_model=DeviceOut)
def update_device(device_id: int, device: DeviceCreate, db: Session = Depends(get_db)):
db_device = db.query(models.Device).filter(models.Device.id == device_id).first()
if not db_device:
raise HTTPException(status_code=404, detail="Équipement introuvable")
db_device.name = device.name
db_device.type = device.type
db_device.description = device.description
db_device.is_gateway = device.is_gateway
db_device.is_livebox = device.is_livebox
db_device.virt_type = device.virt_type
db_device.url = device.url
db.query(models.DeviceInterface).filter(
models.DeviceInterface.device_id == device_id
).delete()
for iface in device.interfaces:
db.add(models.DeviceInterface(device_id=device_id, **iface.model_dump()))
db.commit()
db.refresh(db_device)
return db_device
@router.delete("/{device_id}")
def delete_device(device_id: int, db: Session = Depends(get_db)):
db_device = db.query(models.Device).filter(models.Device.id == device_id).first()
if not db_device:
raise HTTPException(status_code=404, detail="Équipement introuvable")
db.delete(db_device)
db.commit()
return {"ok": True}