246 lines
7.8 KiB
Python
246 lines
7.8 KiB
Python
from datetime import datetime
|
|
from typing import Literal
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
|
|
|
|
class ORMModel(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
|
|
# ---------- customers ----------
|
|
class CustomerBase(BaseModel):
|
|
username: str = Field(max_length=64)
|
|
mac_address: str = Field(max_length=17)
|
|
status: Literal["new", "paid", "unpaid"] = "new"
|
|
name: str | None = Field(default=None, max_length=128)
|
|
phone: str | None = Field(default=None, max_length=32)
|
|
device_alias: str | None = Field(default=None, max_length=64)
|
|
|
|
|
|
class CustomerCreate(CustomerBase):
|
|
pass
|
|
|
|
|
|
class CustomerUpdate(BaseModel):
|
|
username: str | None = Field(default=None, max_length=64)
|
|
mac_address: str | None = Field(default=None, max_length=17)
|
|
status: Literal["new", "paid", "unpaid"] | None = None
|
|
name: str | None = Field(default=None, max_length=128)
|
|
phone: str | None = Field(default=None, max_length=32)
|
|
device_alias: str | None = Field(default=None, max_length=64)
|
|
|
|
|
|
class CustomerOut(ORMModel, CustomerBase):
|
|
id: int
|
|
created_at: datetime | None = None
|
|
|
|
|
|
# ---------- nas ----------
|
|
class NasBase(BaseModel):
|
|
nasname: str = Field(max_length=128)
|
|
shortname: str | None = Field(default=None, max_length=32)
|
|
type: str | None = Field(default="other", max_length=30)
|
|
ports: int | None = None
|
|
secret: str = Field(default="secret", max_length=60)
|
|
server: str | None = Field(default=None, max_length=64)
|
|
community: str | None = Field(default=None, max_length=50)
|
|
description: str | None = Field(default="RADIUS Client", max_length=200)
|
|
|
|
|
|
class NasCreate(NasBase):
|
|
pass
|
|
|
|
|
|
class NasUpdate(BaseModel):
|
|
nasname: str | None = Field(default=None, max_length=128)
|
|
shortname: str | None = Field(default=None, max_length=32)
|
|
type: str | None = Field(default=None, max_length=30)
|
|
ports: int | None = None
|
|
secret: str | None = Field(default=None, max_length=60)
|
|
server: str | None = Field(default=None, max_length=64)
|
|
community: str | None = Field(default=None, max_length=50)
|
|
description: str | None = Field(default=None, max_length=200)
|
|
|
|
|
|
class NasOut(ORMModel, NasBase):
|
|
id: int
|
|
|
|
|
|
# ---------- attribute pair tables (radcheck / radreply) ----------
|
|
class UserAttrBase(BaseModel):
|
|
username: str = Field(max_length=64)
|
|
attribute: str = Field(max_length=64)
|
|
op: str = Field(max_length=2)
|
|
value: str = Field(max_length=253)
|
|
|
|
|
|
class UserAttrCreate(UserAttrBase):
|
|
pass
|
|
|
|
|
|
class UserAttrUpdate(BaseModel):
|
|
username: str | None = Field(default=None, max_length=64)
|
|
attribute: str | None = Field(default=None, max_length=64)
|
|
op: str | None = Field(default=None, max_length=2)
|
|
value: str | None = Field(default=None, max_length=253)
|
|
|
|
|
|
class UserAttrOut(ORMModel, UserAttrBase):
|
|
id: int
|
|
|
|
|
|
# ---------- group attribute tables (radgroupcheck / radgroupreply / vlans) ----------
|
|
class GroupAttrBase(BaseModel):
|
|
groupname: str = Field(max_length=64)
|
|
attribute: str = Field(max_length=64)
|
|
op: str = Field(max_length=2)
|
|
value: str = Field(max_length=253)
|
|
|
|
|
|
class GroupAttrCreate(GroupAttrBase):
|
|
pass
|
|
|
|
|
|
class GroupAttrUpdate(BaseModel):
|
|
groupname: str | None = Field(default=None, max_length=64)
|
|
attribute: str | None = Field(default=None, max_length=64)
|
|
op: str | None = Field(default=None, max_length=2)
|
|
value: str | None = Field(default=None, max_length=253)
|
|
|
|
|
|
class GroupAttrOut(ORMModel, GroupAttrBase):
|
|
id: int
|
|
|
|
|
|
# ---------- vlans (logical view over radgroupreply) ----------
|
|
class VlanOut(BaseModel):
|
|
alias: str # radgroupreply.groupname
|
|
vlanid: int # value of the Tunnel-Private-Group-Id attribute
|
|
|
|
|
|
class VlanCreate(BaseModel):
|
|
vlanid: int = Field(ge=1, le=4094, description="802.1Q VLAN ID")
|
|
alias: str = Field(min_length=1, max_length=64, description="Group name for this VLAN")
|
|
|
|
|
|
class VlanEdit(BaseModel):
|
|
vlanid: int = Field(ge=1, le=4094, description="VLAN ID identifying the group to rename")
|
|
alias: str = Field(min_length=1, max_length=64, description="New alias (groupname)")
|
|
|
|
|
|
# ---------- devices (logical view over radcheck + radusergroup + customers) ----------
|
|
_MAC_RE = r"^[0-9A-Fa-f]{2}([-:][0-9A-Fa-f]{2}){5}$"
|
|
|
|
|
|
def _normalize_mac(mac: str) -> str:
|
|
"""Canonicalize a MAC to uppercase, hyphen-separated (AA-BB-CC-DD-EE-FF)."""
|
|
return mac.strip().upper().replace(":", "-")
|
|
|
|
|
|
class DeviceOut(BaseModel):
|
|
mac_address: str
|
|
group: str | None = None # radusergroup.groupname
|
|
status: str | None = None # customers.status
|
|
name: str | None = None # customers.name (human metadata)
|
|
phone: str | None = None # customers.phone (human metadata)
|
|
alias: str | None = None # customers.device_alias (human metadata)
|
|
|
|
|
|
class DeviceCreate(BaseModel):
|
|
mac_address: str = Field(pattern=_MAC_RE, max_length=17)
|
|
group: str = Field(min_length=1, max_length=64)
|
|
name: str = Field(min_length=1, max_length=128, description="Customer name (metadata, ignored by RADIUS)")
|
|
phone: str = Field(min_length=1, max_length=32, description="Phone number (metadata, ignored by RADIUS)")
|
|
alias: str | None = Field(default=None, max_length=64, description="Optional device alias (metadata)")
|
|
|
|
@field_validator("mac_address")
|
|
@classmethod
|
|
def _norm(cls, v: str) -> str:
|
|
return _normalize_mac(v)
|
|
|
|
|
|
class DeviceEdit(BaseModel):
|
|
mac_address: str = Field(pattern=_MAC_RE, max_length=17)
|
|
group: str | None = Field(default=None, max_length=64)
|
|
status: Literal["new", "paid", "unpaid"] | None = None
|
|
name: str | None = Field(default=None, max_length=128)
|
|
phone: str | None = Field(default=None, max_length=32)
|
|
alias: str | None = Field(default=None, max_length=64)
|
|
|
|
@field_validator("mac_address")
|
|
@classmethod
|
|
def _norm(cls, v: str) -> str:
|
|
return _normalize_mac(v)
|
|
|
|
@model_validator(mode="after")
|
|
def _at_least_one(self):
|
|
if all(v is None for v in (self.group, self.status, self.name, self.phone, self.alias)):
|
|
raise ValueError("provide at least one of: group, status, name, phone, alias")
|
|
return self
|
|
|
|
|
|
# ---------- radusergroup ----------
|
|
class UserGroupBase(BaseModel):
|
|
username: str = Field(max_length=64)
|
|
groupname: str = Field(max_length=64)
|
|
priority: int = 1
|
|
|
|
|
|
class UserGroupCreate(UserGroupBase):
|
|
pass
|
|
|
|
|
|
class UserGroupUpdate(BaseModel):
|
|
username: str | None = Field(default=None, max_length=64)
|
|
groupname: str | None = Field(default=None, max_length=64)
|
|
priority: int | None = None
|
|
|
|
|
|
class UserGroupOut(ORMModel, UserGroupBase):
|
|
id: int
|
|
|
|
|
|
# ---------- read-only: radacct ----------
|
|
class RadAcctOut(ORMModel):
|
|
radacctid: int
|
|
acctsessionid: str
|
|
acctuniqueid: str
|
|
username: str
|
|
realm: str | None = None
|
|
nasipaddress: str
|
|
nasportid: str | None = None
|
|
nasporttype: str | None = None
|
|
acctstarttime: datetime | None = None
|
|
acctupdatetime: datetime | None = None
|
|
acctstoptime: datetime | None = None
|
|
acctinterval: int | None = None
|
|
acctsessiontime: int | None = None
|
|
acctauthentic: str | None = None
|
|
connectinfo_start: str | None = None
|
|
connectinfo_stop: str | None = None
|
|
acctinputoctets: int | None = None
|
|
acctoutputoctets: int | None = None
|
|
calledstationid: str
|
|
callingstationid: str
|
|
acctterminatecause: str
|
|
servicetype: str | None = None
|
|
framedprotocol: str | None = None
|
|
framedipaddress: str
|
|
class_: str | None = Field(default=None, alias="class")
|
|
|
|
|
|
# ---------- read-only: radpostauth ----------
|
|
class RadPostAuthOut(ORMModel):
|
|
id: int
|
|
username: str
|
|
reply: str
|
|
authdate: datetime | None = None
|
|
class_: str | None = Field(default=None, alias="class")
|
|
|
|
|
|
# ---------- read-only: nasreload ----------
|
|
class NasReloadOut(ORMModel):
|
|
nasipaddress: str
|
|
reloadtime: datetime
|