import re from datetime import datetime from typing import Literal from pydantic import BaseModel, Field, field_validator, model_validator ALLOWED_NAME_REGEX = re.compile(r"^[A-Za-z0-9_-]+$") DeviceType = Literal["routeros", "switchos"] class RouterBase(BaseModel): name: str = Field(min_length=1, max_length=120) device_type: DeviceType = "routeros" host: str = Field(min_length=1, max_length=255) port: int | None = Field(default=None, ge=1, le=65535) ssh_user: str | None = Field(default=None, max_length=120) ssh_key: str | None = None ssh_password: str | None = None @field_validator("name") @classmethod def validate_name(cls, value: str) -> str: if not ALLOWED_NAME_REGEX.match(value): raise ValueError("Only letters, digits, dashes and underscores are allowed") return value @field_validator("host", "ssh_user", "ssh_key", "ssh_password", mode="before") @classmethod def normalize_text(cls, value: str | None) -> str | None: normalized = (value or "").strip() return normalized or None @model_validator(mode="after") def apply_device_defaults(self): if self.device_type == "routeros": self.port = self.port or 22 self.ssh_user = self.ssh_user or "admin" return self self.port = self.port or 80 self.ssh_key = None return self class RouterCreate(RouterBase): pass class RouterUpdate(BaseModel): name: str | None = None device_type: DeviceType | None = None host: str | None = None port: int | None = Field(default=None, ge=1, le=65535) ssh_user: str | None = None ssh_key: str | None = None ssh_password: str | None = None @field_validator("name", "host", "ssh_user", "ssh_key", "ssh_password", mode="before") @classmethod def normalize_text(cls, value: str | None) -> str | None: normalized = (value or "").strip() return normalized or None class RouterResponse(RouterBase): id: int owner_id: int effective_username: str | None = None uses_global_ssh_key: bool = False has_effective_ssh_key: bool = False uses_global_switchos_credentials: bool = False has_effective_password: bool = False supports_export: bool = False supports_restore_upload: bool = False last_connection_status: bool | None = None last_connection_tested_at: datetime | None = None last_connection_error: str | None = None last_connection_hostname: str | None = None last_connection_model: str | None = None last_connection_version: str | None = None last_connection_uptime: str | None = None last_connection_transport: str | None = None last_connection_server: str | None = None last_connection_auth_mode: str | None = None last_connection_http_status: str | None = None last_connection_backup_available: bool | None = None created_at: datetime | None = None model_config = {"from_attributes": True} class RouterTestConnection(BaseModel): success: bool tested_at: datetime model: str uptime: str hostname: str version: str | None = None error: str | None = None transport: str | None = None server: str | None = None auth_mode: str | None = None http_status: str | None = None backup_available: bool | None = None