Files
minerops/backend/app/agent_zones/models.py
T
2026-08-26 15:48:59 +00:00

115 lines
3.4 KiB
Python

"""Typed Agent Zone and check-in contracts."""
from ipaddress import ip_network
from typing import Any
from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
class AgentZoneCreateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
site_id: UUID
name: str = Field(min_length=1, max_length=120)
description: str | None = Field(default=None, max_length=1000)
networks: list[str] = Field(default_factory=list, max_length=64)
@field_validator("name")
@classmethod
def normalize_name(cls, value: str) -> str:
normalized = value.strip()
if not normalized:
raise ValueError("name cannot be blank")
return normalized
@field_validator("description")
@classmethod
def normalize_description(cls, value: str | None) -> str | None:
if value is None:
return None
return value.strip() or None
@field_validator("networks")
@classmethod
def normalize_networks(cls, values: list[str]) -> list[str]:
normalized: list[str] = []
for value in values:
try:
network = str(ip_network(value.strip(), strict=True))
except ValueError as exc:
raise ValueError(f"invalid CIDR network: {value}") from exc
if network in normalized:
raise ValueError(f"duplicate CIDR network: {network}")
normalized.append(network)
return normalized
class AgentZoneNetwork(BaseModel):
id: UUID
network: str
class AgentZoneSummary(BaseModel):
id: UUID
site_id: UUID
name: str
description: str | None = None
is_enabled: bool
assigned_agent_id: str | None = None
networks: list[AgentZoneNetwork] = Field(default_factory=list)
class AgentZoneListResponse(BaseModel):
data: list[AgentZoneSummary]
meta: dict[str, object] = Field(default_factory=dict)
error: None = None
class AgentZoneCreateResponse(BaseModel):
data: AgentZoneSummary
meta: dict[str, object] = Field(default_factory=dict)
error: None = None
class AgentCheckinRequest(BaseModel):
model_config = ConfigDict(extra="allow")
agent_id: str | None = Field(default=None, max_length=255)
site_id: UUID | None = None
agent_zone_id: UUID | None = None
site_name: str | None = Field(default=None, max_length=255)
hostname: str | None = Field(default=None, max_length=255)
version: str | None = Field(default=None, max_length=120)
ip_address: str | None = Field(default=None, max_length=255)
@model_validator(mode="after")
def require_site_for_explicit_zone(self) -> "AgentCheckinRequest":
if self.agent_zone_id is not None and self.site_id is None:
raise ValueError("site_id is required when agent_zone_id is supplied")
return self
class AgentCheckinResponse(BaseModel):
ok: bool = True
hub_mode: bool = True
agent_id: str
site_id: UUID | None = None
agent_zone_id: UUID | None = None
routing_available: bool
routing_reason: str | None = None
site_name: str
message: str = "Agent check-in received."
class AgentZoneRoutingContext(BaseModel):
site_id: UUID | None = None
agent_zone_id: UUID | None = None
routing_reason: str | None = None
class MinerZoneAssociationResult(BaseModel):
agent_zone_id: UUID | None = None
routing_reason: str | None = None
outcomes: dict[UUID, str] = Field(default_factory=dict)