115 lines
3.4 KiB
Python
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)
|