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" 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 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 class DeviceCreate(BaseModel): mac_address: str = Field(pattern=_MAC_RE, max_length=17) group: str = Field(min_length=1, max_length=64) @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 @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 self.group is None and self.status is None: raise ValueError("provide at least one of: group, status") 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