"""Pydantic schemas for API requests and responses.""" from datetime import datetime from typing import List, Optional, Dict, Any, Union from pydantic import BaseModel, Field, validator # User schemas class UserBase(BaseModel): username: str class UserCreate(UserBase): password: str = Field(..., min_length=8) class UserLogin(UserBase): password: str class UserResponse(UserBase): id: int is_active: bool created_at: datetime last_login: Optional[datetime] class Config: from_attributes = True # Token schemas class Token(BaseModel): access_token: str token_type: str = "bearer" class TokenData(BaseModel): username: Optional[str] = None # Fan curve schemas class FanCurvePoint(BaseModel): temp: float = Field(..., ge=0, le=150, description="Temperature in Celsius") speed: int = Field(..., ge=0, le=100, description="Fan speed percentage") class FanCurveBase(BaseModel): name: str = "Default" curve_data: List[FanCurvePoint] sensor_source: str = "cpu" is_active: bool = True class FanCurveCreate(FanCurveBase): pass class FanCurveResponse(FanCurveBase): id: int server_id: int created_at: datetime updated_at: datetime class Config: from_attributes = True @validator('curve_data', pre=True) def parse_curve_data(cls, v): """Parse curve_data from JSON string if needed.""" if isinstance(v, str): import json return json.loads(v) return v # IPMI Settings schema class IPMISettings(BaseModel): ipmi_host: str = Field(..., description="IPMI IP address or hostname") ipmi_port: int = Field(623, description="IPMI port (default 623)") ipmi_username: str = Field(..., description="IPMI username") ipmi_password: str = Field(..., description="IPMI password") # SSH Settings schema class SSHSettings(BaseModel): use_ssh: bool = Field(False, description="Enable SSH for sensor data collection") ssh_host: Optional[str] = Field(None, description="SSH host (leave empty to use IPMI host)") ssh_port: int = Field(22, description="SSH port (default 22)") ssh_username: Optional[str] = Field(None, description="SSH username") ssh_password: Optional[str] = Field(None, description="SSH password (or use key)") ssh_key_file: Optional[str] = Field(None, description="Path to SSH private key file") # Server schemas class ServerBase(BaseModel): name: str = Field(..., description="Server name/display name") vendor: str = Field("dell", description="Server vendor (dell, hpe, supermicro, other)") @validator('vendor') def validate_vendor(cls, v): allowed = ['dell', 'hpe', 'supermicro', 'other'] if v.lower() not in allowed: raise ValueError(f'Vendor must be one of: {allowed}') return v.lower() class ServerCreate(ServerBase): ipmi: IPMISettings ssh: SSHSettings = Field(default_factory=lambda: SSHSettings(use_ssh=False)) class ServerUpdate(BaseModel): name: Optional[str] = None ipmi_host: Optional[str] = None ipmi_port: Optional[int] = None ipmi_username: Optional[str] = None ipmi_password: Optional[str] = None ssh_host: Optional[str] = None ssh_port: Optional[int] = None ssh_username: Optional[str] = None ssh_password: Optional[str] = None ssh_key_file: Optional[str] = None use_ssh: Optional[bool] = None vendor: Optional[str] = None manual_control_enabled: Optional[bool] = None third_party_pcie_response: Optional[bool] = None auto_control_enabled: Optional[bool] = None panic_mode_enabled: Optional[bool] = None panic_timeout_seconds: Optional[int] = None class ServerResponse(ServerBase): id: int ipmi_host: str ipmi_port: int ipmi_username: str ssh_host: Optional[str] ssh_port: int ssh_username: Optional[str] use_ssh: bool manual_control_enabled: bool third_party_pcie_response: bool auto_control_enabled: bool panic_mode_enabled: bool panic_timeout_seconds: int created_at: datetime updated_at: datetime last_seen: Optional[datetime] is_active: bool class Config: from_attributes = True class ServerDetailResponse(ServerResponse): fan_curves: List[FanCurveResponse] = [] class ServerStatusResponse(BaseModel): server: ServerResponse is_connected: bool controller_status: Dict[str, Any] # Sensor schemas class SensorReading(BaseModel): name: str sensor_type: str value: float unit: str status: str class TemperatureReading(BaseModel): name: str location: str value: float status: str class FanReading(BaseModel): fan_id: str fan_number: int speed_rpm: Optional[int] speed_percent: Optional[int] class SensorDataResponse(BaseModel): id: int sensor_name: str sensor_type: str value: float unit: Optional[str] timestamp: datetime class Config: from_attributes = True class FanDataResponse(BaseModel): id: int fan_number: int fan_id: str speed_rpm: Optional[int] speed_percent: Optional[int] is_manual: bool timestamp: datetime class Config: from_attributes = True class ServerSensorsResponse(BaseModel): server_id: int temperatures: List[TemperatureReading] fans: List[FanReading] all_sensors: List[SensorReading] timestamp: datetime # Fan control schemas class FanControlCommand(BaseModel): fan_id: str = Field(default="0xff", description="Fan ID (0xff for all, 0x00-0x07 for specific)") speed_percent: int = Field(..., ge=0, le=100, description="Fan speed percentage") class FanCurveApply(BaseModel): curve_id: Optional[int] = None curve_data: Optional[List[FanCurvePoint]] = None sensor_source: Optional[str] = "cpu" class AutoControlSettings(BaseModel): enabled: bool curve_id: Optional[int] = None # System log schemas class SystemLogResponse(BaseModel): id: int server_id: Optional[int] event_type: str message: str details: Optional[str] timestamp: datetime class Config: from_attributes = True # Setup wizard schemas class SetupStatus(BaseModel): setup_complete: bool class SetupComplete(BaseModel): username: str = Field(..., min_length=3, max_length=50) password: str = Field(..., min_length=8) confirm_password: str = Field(..., min_length=8) @validator('confirm_password') def passwords_match(cls, v, values): if 'password' in values and v != values['password']: raise ValueError('Passwords do not match') return v # Dashboard schemas class DashboardStats(BaseModel): total_servers: int active_servers: int manual_control_servers: int auto_control_servers: int panic_mode_servers: int recent_logs: List[SystemLogResponse] class ServerDashboardData(BaseModel): server: ServerResponse current_temperatures: List[TemperatureReading] current_fans: List[FanReading] recent_sensor_data: List[SensorDataResponse] recent_fan_data: List[FanDataResponse] power_consumption: Optional[Dict[str, str]]