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>
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
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}
|
||||
Reference in New Issue
Block a user