9fe6b25786
The backend validator was rejecting the three new device types added in the previous commit, returning HTTP 422. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
177 lines
5.2 KiB
Python
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", "smart_tv", "printer", "smartphone", "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}
|