feat: add agent zones
This commit is contained in:
1 parent
d8cd7ad058
commit
82309db79a
29 files changed
+2242
-255
No files matched your search
@@ -0,0 +1,194 @@
|
||||
"""Business rules for Agent Zone routing and transitional check-in."""
|
||||
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from app.agent_zones.exceptions import (
|
||||
AgentZoneNotFound,
|
||||
AgentZoneValidationError,
|
||||
)
|
||||
from app.agent_zones.models import (
|
||||
AgentCheckinRequest,
|
||||
AgentCheckinResponse,
|
||||
AgentZoneRoutingContext,
|
||||
AgentZoneCreateRequest,
|
||||
AgentZoneSummary,
|
||||
MinerZoneAssociationResult,
|
||||
)
|
||||
from app.agent_zones.repository import AgentZoneRepository
|
||||
|
||||
|
||||
SENSITIVE_KEYS = {
|
||||
"agent_secret",
|
||||
"api3_account",
|
||||
"api3_password",
|
||||
"password",
|
||||
"salt",
|
||||
"token",
|
||||
}
|
||||
|
||||
|
||||
def _sanitize(value: Any) -> Any:
|
||||
if isinstance(value, dict):
|
||||
return {
|
||||
key: _sanitize(item)
|
||||
for key, item in value.items()
|
||||
if key.lower() not in SENSITIVE_KEYS
|
||||
and not any(
|
||||
marker in key.lower()
|
||||
for marker in ("secret", "password", "token", "salt")
|
||||
)
|
||||
}
|
||||
if isinstance(value, list):
|
||||
return [_sanitize(item) for item in value]
|
||||
return value
|
||||
|
||||
|
||||
class AgentZoneService:
|
||||
@staticmethod
|
||||
def validate_agent_identity(
|
||||
authenticated_agent_id: str, claimed_agent_id: str | None
|
||||
) -> None:
|
||||
if claimed_agent_id and claimed_agent_id.strip() != authenticated_agent_id:
|
||||
raise AgentZoneValidationError(
|
||||
"Payload agent_id does not match authenticated Agent identity"
|
||||
)
|
||||
|
||||
def __init__(self, repository: AgentZoneRepository):
|
||||
self.repository = repository
|
||||
|
||||
def list_zones(self, site_id: UUID | None = None) -> list[AgentZoneSummary]:
|
||||
return [
|
||||
AgentZoneSummary.model_validate(row)
|
||||
for row in self.repository.list_zones(site_id)
|
||||
]
|
||||
|
||||
def create_zone(self, request: AgentZoneCreateRequest) -> AgentZoneSummary:
|
||||
zone_id = self.repository.create_zone(
|
||||
site_id=request.site_id,
|
||||
name=request.name,
|
||||
description=request.description,
|
||||
networks=request.networks,
|
||||
)
|
||||
for zone in self.list_zones(request.site_id):
|
||||
if zone.id == zone_id:
|
||||
return zone
|
||||
raise AgentZoneNotFound("Created Agent Zone was not found")
|
||||
|
||||
def checkin(
|
||||
self, authenticated_agent_id: str, request: AgentCheckinRequest
|
||||
) -> AgentCheckinResponse:
|
||||
self.validate_agent_identity(authenticated_agent_id, request.agent_id)
|
||||
|
||||
existing = self.repository.get_agent(authenticated_agent_id)
|
||||
requested_site = request.site_id
|
||||
requested_zone = request.agent_zone_id
|
||||
final_site = requested_site or (existing or {}).get("site_id")
|
||||
final_zone: UUID | None = None
|
||||
reason: str | None = None
|
||||
|
||||
established_zone = None
|
||||
established_zone_id = (existing or {}).get("agent_zone_id")
|
||||
if established_zone_id is not None:
|
||||
established_zone = self.repository.get_zone(established_zone_id)
|
||||
if not established_zone:
|
||||
raise AgentZoneValidationError(
|
||||
"Existing Agent Zone assignment is invalid"
|
||||
)
|
||||
if (
|
||||
requested_site is not None
|
||||
and established_zone["site_id"] != requested_site
|
||||
):
|
||||
raise AgentZoneValidationError(
|
||||
"Agent cannot change Sites while assigned to an Agent Zone"
|
||||
)
|
||||
|
||||
if requested_zone is not None:
|
||||
zone = self.repository.get_zone(requested_zone)
|
||||
if not zone:
|
||||
raise AgentZoneNotFound("Agent Zone not found")
|
||||
if zone["site_id"] != requested_site:
|
||||
raise AgentZoneValidationError(
|
||||
"Agent Zone does not belong to the supplied Site"
|
||||
)
|
||||
if not zone["is_enabled"]:
|
||||
raise AgentZoneValidationError("Agent Zone is disabled")
|
||||
if established_zone_id is not None and established_zone_id != requested_zone:
|
||||
raise AgentZoneValidationError(
|
||||
"Agent is already assigned to a different Agent Zone"
|
||||
)
|
||||
final_zone = requested_zone
|
||||
elif established_zone is not None:
|
||||
final_site = established_zone["site_id"]
|
||||
final_zone = established_zone["id"]
|
||||
if not established_zone["is_enabled"]:
|
||||
reason = "agent_zone_disabled"
|
||||
elif final_site is not None:
|
||||
usable = self.repository.list_usable_zones(final_site)
|
||||
if len(usable) == 1:
|
||||
final_zone = usable[0]["id"]
|
||||
elif not usable:
|
||||
reason = "agent_zone_unavailable"
|
||||
else:
|
||||
reason = "agent_zone_ambiguous"
|
||||
else:
|
||||
reason = "site_unassigned"
|
||||
|
||||
site_name = (request.site_name or "").strip()
|
||||
if not site_name:
|
||||
site_name = str((existing or {}).get("site_name") or "Unassigned")
|
||||
payload = _sanitize(request.model_dump(mode="json"))
|
||||
self.repository.save_checkin(
|
||||
agent_id=authenticated_agent_id,
|
||||
site_id=final_site,
|
||||
agent_zone_id=final_zone,
|
||||
site_name=site_name,
|
||||
hostname=(request.hostname or "").strip() or None,
|
||||
version=(request.version or "").strip() or None,
|
||||
ip_address=(request.ip_address or "").strip() or None,
|
||||
payload=payload,
|
||||
)
|
||||
routing_available = final_zone is not None and reason is None
|
||||
return AgentCheckinResponse(
|
||||
agent_id=authenticated_agent_id,
|
||||
site_id=final_site,
|
||||
agent_zone_id=final_zone,
|
||||
routing_available=routing_available,
|
||||
routing_reason=reason,
|
||||
site_name=site_name,
|
||||
)
|
||||
|
||||
def routing_context(
|
||||
self, agent_id: str, recorded_zone_id: UUID | None = None
|
||||
) -> AgentZoneRoutingContext:
|
||||
assignment = self.repository.get_agent_assignment(agent_id)
|
||||
if not assignment or assignment.get("agent_zone_id") is None:
|
||||
return AgentZoneRoutingContext(routing_reason="agent_zone_unassigned")
|
||||
zone_id = assignment["agent_zone_id"]
|
||||
if recorded_zone_id is not None and recorded_zone_id != zone_id:
|
||||
return AgentZoneRoutingContext(routing_reason="agent_zone_conflict")
|
||||
if assignment.get("zone_enabled") is not True:
|
||||
return AgentZoneRoutingContext(
|
||||
site_id=assignment.get("site_id"),
|
||||
agent_zone_id=zone_id,
|
||||
routing_reason="agent_zone_disabled",
|
||||
)
|
||||
return AgentZoneRoutingContext(
|
||||
site_id=assignment.get("site_id"), agent_zone_id=zone_id
|
||||
)
|
||||
|
||||
def associate_miners_from_agent(
|
||||
self, agent_id: str, miner_ids: list[UUID]
|
||||
) -> MinerZoneAssociationResult:
|
||||
routing = self.routing_context(agent_id)
|
||||
if routing.routing_reason:
|
||||
return MinerZoneAssociationResult(
|
||||
agent_zone_id=routing.agent_zone_id,
|
||||
routing_reason=routing.routing_reason,
|
||||
)
|
||||
return MinerZoneAssociationResult(
|
||||
agent_zone_id=routing.agent_zone_id,
|
||||
outcomes=self.repository.assign_miners_to_zone(
|
||||
miner_ids, routing.agent_zone_id
|
||||
),
|
||||
)
|
||||
Reference in new issue
Block a user