166 lines
6.5 KiB
Python
166 lines
6.5 KiB
Python
"""Business rules for typed, Agent-routed miner controls."""
|
|
|
|
from typing import Any
|
|
from uuid import UUID, uuid4
|
|
|
|
from app.miner_control.exceptions import (
|
|
ControlledMinerNotFound,
|
|
MinerControlActorRequired,
|
|
MinerControlUnavailable,
|
|
)
|
|
from app.miner_control.models import (
|
|
ControlAgentAvailability,
|
|
ControlCapability,
|
|
MinerControlAccepted,
|
|
MinerControlOperation,
|
|
MinerControlProjection,
|
|
)
|
|
from app.miner_control.repository import MinerControlRepository
|
|
|
|
|
|
KNOWN_MODES = {"low", "normal", "high"}
|
|
RUNNING_STATES = {"online", "running"}
|
|
STOPPED_STATES = {"suspended", "stopped"}
|
|
|
|
|
|
class MinerControlService:
|
|
def __init__(self, repository: MinerControlRepository):
|
|
self.repository = repository
|
|
|
|
@staticmethod
|
|
def _capability(context: dict) -> ControlCapability:
|
|
if not context.get("site_id"):
|
|
return ControlCapability(state="unavailable", reason="site_unassigned")
|
|
if not context.get("agent_id"):
|
|
return ControlCapability(state="unavailable", reason="agent_unassigned")
|
|
if not context.get("current_ip"):
|
|
return ControlCapability(state="unavailable", reason="current_ip_missing")
|
|
if context.get("api3_supported") is False:
|
|
return ControlCapability(state="unavailable", reason="api3_unsupported")
|
|
if context.get("api3_supported") is not True:
|
|
return ControlCapability(state="unknown", reason="api3_support_unknown")
|
|
|
|
payload = context.get("last_payload") or {}
|
|
if payload.get("control_available") is False:
|
|
reason = payload.get("control_unavailable_reason")
|
|
return ControlCapability(
|
|
state="unavailable",
|
|
reason=reason if reason == "credentials_missing" else "agent_unavailable",
|
|
)
|
|
if payload.get("control_available") is not True:
|
|
return ControlCapability(
|
|
state="unknown", reason="agent_capability_unknown"
|
|
)
|
|
return ControlCapability(state="supported")
|
|
|
|
@staticmethod
|
|
def _operation(miner_id: UUID, row: dict) -> MinerControlOperation:
|
|
request = row.get("request_payload") or {}
|
|
result = row.get("result_payload") or {}
|
|
mode = result.get("observed_performance_mode")
|
|
return MinerControlOperation(
|
|
operation_id=row["operation_id"],
|
|
miner_id=miner_id,
|
|
action=request.get("action"),
|
|
requested_value=request.get("requested_value"),
|
|
status=row["status"],
|
|
verification_status=result.get("verification_status"),
|
|
command_accepted=result.get("command_accepted"),
|
|
observed_performance_mode=mode if mode in KNOWN_MODES else None,
|
|
failure_code=result.get("failure_code"),
|
|
created_at=row["created_at"],
|
|
claimed_at=row.get("claimed_at"),
|
|
completed_at=row.get("completed_at"),
|
|
)
|
|
|
|
def get_controls(self, miner_id: UUID) -> MinerControlProjection:
|
|
context = self.repository.get_control_context(miner_id)
|
|
if not context:
|
|
raise ControlledMinerNotFound("Miner not found")
|
|
capability = self._capability(context)
|
|
runtime = str(context.get("runtime_state") or "").strip().lower() or None
|
|
service_action = None
|
|
if capability.state == "supported":
|
|
if runtime in RUNNING_STATES:
|
|
service_action = "suspend"
|
|
elif runtime in STOPPED_STATES:
|
|
service_action = "resume"
|
|
configured_mode = str(context.get("configured_performance_mode") or "").lower()
|
|
return MinerControlProjection(
|
|
miner_id=miner_id,
|
|
runtime_state=runtime,
|
|
performance_mode=(configured_mode if configured_mode in KNOWN_MODES else None),
|
|
agent=ControlAgentAvailability(
|
|
state=capability.state,
|
|
reason=capability.reason,
|
|
last_checkin_at=context.get("last_checkin_at"),
|
|
version=context.get("agent_version"),
|
|
),
|
|
service_control=capability,
|
|
performance_mode_control=capability,
|
|
available_service_action=service_action,
|
|
recent_operations=[
|
|
self._operation(miner_id, row)
|
|
for row in self.repository.list_recent(miner_id, limit=5)
|
|
],
|
|
)
|
|
|
|
def queue_control(
|
|
self,
|
|
miner_id: UUID,
|
|
action: str,
|
|
*,
|
|
requested_value: str | None,
|
|
actor_user_id: UUID,
|
|
actor_username: str,
|
|
) -> MinerControlAccepted:
|
|
if not actor_user_id:
|
|
raise MinerControlActorRequired("Authenticated actor UUID is required")
|
|
context = self.repository.get_control_context(miner_id)
|
|
if not context:
|
|
raise ControlledMinerNotFound("Miner not found")
|
|
capability = self._capability(context)
|
|
if capability.state != "supported":
|
|
raise MinerControlUnavailable(
|
|
capability.reason or "control_capability_not_supported"
|
|
)
|
|
|
|
runtime = str(context.get("runtime_state") or "").strip().lower()
|
|
if action == "suspend" and runtime not in RUNNING_STATES:
|
|
raise MinerControlUnavailable("miner_not_factually_running")
|
|
if action == "resume" and runtime not in STOPPED_STATES:
|
|
raise MinerControlUnavailable("miner_not_factually_stopped")
|
|
if action == "performance_mode" and requested_value not in KNOWN_MODES:
|
|
raise MinerControlUnavailable("invalid_performance_mode")
|
|
|
|
operation_id = str(uuid4())
|
|
payload: dict[str, Any] = {
|
|
"schema_version": 1,
|
|
"miner_id": str(miner_id),
|
|
"action": action,
|
|
"requested_value": requested_value,
|
|
"ip_address": context["current_ip"],
|
|
}
|
|
operation = self.repository.enqueue_with_audit(
|
|
operation_id=operation_id,
|
|
miner_id=miner_id,
|
|
agent_id=context["agent_id"],
|
|
actor_user_id=actor_user_id,
|
|
actor_username=actor_username,
|
|
request_payload=payload,
|
|
audit_payload={
|
|
"operation_id": operation_id,
|
|
"miner_id": str(miner_id),
|
|
"action": action,
|
|
"requested_value": requested_value,
|
|
},
|
|
)
|
|
return MinerControlAccepted(
|
|
operation_id=operation["operation_id"],
|
|
miner_id=miner_id,
|
|
action=action,
|
|
requested_value=requested_value,
|
|
status=operation["status"],
|
|
created_at=operation["created_at"],
|
|
)
|