feat: add safe miner control foundation
This commit is contained in:
1 parent
6cd876b4b3
commit
f5965af914
39 files changed
+2952
-491
No files matched your search
+20
-165
@@ -11,6 +11,8 @@ from fastapi import Request, Depends, FastAPI, Header, HTTPException, Query, sta
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr, Field, ValidationError
|
||||
|
||||
from app.config import settings
|
||||
from app.agent_commands.router import router as agent_commands_router
|
||||
from app.miner_control.router import router as miner_control_router
|
||||
from app.dashboard.router import router as dashboard_router
|
||||
from app.identity import (
|
||||
COOKIE_TOKEN_HINT,
|
||||
@@ -63,6 +65,8 @@ app = FastAPI(title=settings.app_name)
|
||||
app.include_router(identity_router)
|
||||
app.include_router(frontend_router)
|
||||
app.include_router(inventory_router)
|
||||
app.include_router(miner_control_router)
|
||||
app.include_router(agent_commands_router)
|
||||
app.include_router(components_router)
|
||||
app.include_router(locations_router)
|
||||
app.include_router(dashboard_router)
|
||||
@@ -4010,6 +4014,8 @@ def minerops_process_due_scheduled_bulk_actions():
|
||||
conn.close()
|
||||
|
||||
|
||||
app.state.agent_command_before_claim = minerops_process_due_scheduled_bulk_actions
|
||||
|
||||
|
||||
def minerops_reconcile_scheduled_bulk_action_statuses():
|
||||
minerops_ensure_scheduled_bulk_actions_table()
|
||||
@@ -24985,8 +24991,7 @@ async def minerops_hub_mode_js():
|
||||
# -----------------------------
|
||||
|
||||
def minerops_agent_secret_expected() -> str:
|
||||
import os
|
||||
return str(os.environ.get("MINEROPS_AGENT_SHARED_SECRET", "") or "").strip()
|
||||
return settings.minerops_agent_shared_secret.strip()
|
||||
|
||||
|
||||
def minerops_require_agent_secret(request: Request):
|
||||
@@ -25089,6 +25094,7 @@ async def minerops_agent_checkin(request: Request):
|
||||
body = {}
|
||||
|
||||
agent_id = str(body.get("agent_id") or "plexus-windows-agent").strip()
|
||||
site_id = str(body.get("site_id") or "").strip()
|
||||
site_name = str(body.get("site_name") or "Plexus").strip()
|
||||
hostname = str(body.get("hostname") or "").strip()
|
||||
version = str(body.get("version") or "").strip()
|
||||
@@ -25101,6 +25107,7 @@ async def minerops_agent_checkin(request: Request):
|
||||
"""
|
||||
INSERT INTO minerops_site_agents (
|
||||
agent_id,
|
||||
site_id,
|
||||
site_name,
|
||||
hostname,
|
||||
version,
|
||||
@@ -25108,9 +25115,10 @@ async def minerops_agent_checkin(request: Request):
|
||||
last_checkin_at,
|
||||
last_payload
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, now(), $6::jsonb)
|
||||
VALUES ($1, NULLIF($2, '')::uuid, $3, $4, $5, $6, now(), $7::jsonb)
|
||||
ON CONFLICT (agent_id)
|
||||
DO UPDATE SET
|
||||
site_id = COALESCE(EXCLUDED.site_id, minerops_site_agents.site_id),
|
||||
site_name = EXCLUDED.site_name,
|
||||
hostname = EXCLUDED.hostname,
|
||||
version = EXCLUDED.version,
|
||||
@@ -25119,6 +25127,7 @@ async def minerops_agent_checkin(request: Request):
|
||||
last_payload = EXCLUDED.last_payload
|
||||
""",
|
||||
agent_id,
|
||||
site_id,
|
||||
site_name,
|
||||
hostname,
|
||||
version,
|
||||
@@ -25301,12 +25310,11 @@ async def minerops_agent_setup_info(current_user: dict = Depends(get_current_use
|
||||
return {
|
||||
"ok": True,
|
||||
"hub_url": "https://plexus.lussier.cc",
|
||||
"agent_id": "plexus-windows-agent",
|
||||
"agent_id": settings.minerops_agent_id,
|
||||
"site_name": "Plexus",
|
||||
"agent_secret": secret,
|
||||
"secret_length": len(secret),
|
||||
"secret_configured": bool(secret),
|
||||
"config_path": "C:\\MinerOpsAgent\\agent-config.json",
|
||||
"message": "Keep this secret private. It allows the Windows Agent to push updates to the hub.",
|
||||
"message": "Agent credentials are provisioned locally and are never returned to browsers.",
|
||||
}
|
||||
|
||||
|
||||
@@ -25353,7 +25361,7 @@ async def minerops_agent_setup_page():
|
||||
<div class="card">
|
||||
<h2>Agent Connection Info</h2>
|
||||
<div class="warn">
|
||||
Keep this secret private. Anyone with the secret can submit agent data to this hub.
|
||||
Agent credentials must be provisioned directly on the Windows host.
|
||||
</div>
|
||||
|
||||
<label>Hub URL</label>
|
||||
@@ -25365,20 +25373,7 @@ async def minerops_agent_setup_page():
|
||||
<label>Site Name</label>
|
||||
<input id="siteName" readonly>
|
||||
|
||||
<label>Agent Shared Secret</label>
|
||||
<input id="agentSecret" type="password" readonly>
|
||||
|
||||
<p>
|
||||
<button onclick="toggleSecret()">Show / Hide Secret</button>
|
||||
<button onclick="copySecret()">Copy Secret</button>
|
||||
<button onclick="copyConfig()">Copy Full Config JSON</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Windows Agent Config JSON</h2>
|
||||
<p class="small">Paste this into <strong>C:\\MinerOpsAgent\\agent-config.json</strong> if you need to recreate the agent config.</p>
|
||||
<textarea id="configJson" readonly></textarea>
|
||||
<p id="secretStatus" class="small"></p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
@@ -25427,42 +25422,14 @@ async function loadSetup() {
|
||||
document.getElementById("hubUrl").value = data.hub_url || "";
|
||||
document.getElementById("agentId").value = data.agent_id || "";
|
||||
document.getElementById("siteName").value = data.site_name || "";
|
||||
document.getElementById("agentSecret").value = data.agent_secret || "";
|
||||
|
||||
const config = {
|
||||
agent_id: data.agent_id,
|
||||
site_name: data.site_name,
|
||||
hub_url: data.hub_url,
|
||||
agent_secret: data.agent_secret,
|
||||
poll_interval_seconds: 120,
|
||||
ip_ranges: [
|
||||
{ name: "Unit 4", cidr: "10.30.0.0/22", enabled: true, api_port: 4028, api3_port: 4433 },
|
||||
{ name: "Unit 6", cidr: "10.80.0.0/22", enabled: true, api_port: 4028, api3_port: 4433 },
|
||||
{ name: "Unit 12", cidr: "10.90.0.0/22", enabled: false, api_port: 4028, api3_port: 4433 }
|
||||
]
|
||||
};
|
||||
|
||||
document.getElementById("configJson").value = JSON.stringify(config, null, 2);
|
||||
document.getElementById("secretStatus").textContent = data.secret_configured
|
||||
? "A shared Agent secret is configured on the Hub. Its value is not exposed here."
|
||||
: "The Hub does not currently have an Agent secret configured.";
|
||||
|
||||
status.className = "ok";
|
||||
status.textContent = "Agent setup info loaded.";
|
||||
}
|
||||
|
||||
function toggleSecret() {
|
||||
const input = document.getElementById("agentSecret");
|
||||
input.type = input.type === "password" ? "text" : "password";
|
||||
}
|
||||
|
||||
async function copySecret() {
|
||||
await navigator.clipboard.writeText(document.getElementById("agentSecret").value);
|
||||
alert("Secret copied.");
|
||||
}
|
||||
|
||||
async function copyConfig() {
|
||||
await navigator.clipboard.writeText(document.getElementById("configJson").value);
|
||||
alert("Config JSON copied.");
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", loadSetup);
|
||||
</script>
|
||||
<script src="/assets/minerops-miner-detail-extra.js?v=1"></script>
|
||||
@@ -25939,118 +25906,6 @@ async def minerops_agent_command_status(current_user: dict = Depends(get_current
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/agent/commands/pending")
|
||||
async def minerops_agent_commands_pending_v2(request: Request):
|
||||
# __SCHEDULE_DUE_HOOK_V2__
|
||||
try:
|
||||
minerops_process_due_scheduled_bulk_actions()
|
||||
except NameError:
|
||||
pass
|
||||
except Exception as exc:
|
||||
print(f"Scheduled bulk action due processing error: {exc}")
|
||||
|
||||
minerops_require_agent_secret(request)
|
||||
|
||||
await minerops_ensure_agent_command_queue_table()
|
||||
|
||||
agent_id = request.query_params.get("agent_id") or "plexus-windows-agent"
|
||||
|
||||
conn = await minerops_command_db_connect()
|
||||
|
||||
try:
|
||||
rows = await conn.fetch(
|
||||
"""
|
||||
UPDATE minerops_agent_command_queue
|
||||
SET
|
||||
command_status = 'claimed',
|
||||
claimed_at = now()
|
||||
WHERE id IN (
|
||||
SELECT id
|
||||
FROM minerops_agent_command_queue
|
||||
WHERE agent_id = $1
|
||||
AND command_status = 'pending'
|
||||
ORDER BY created_at ASC
|
||||
LIMIT 5
|
||||
)
|
||||
RETURNING
|
||||
id,
|
||||
command_type,
|
||||
request_payload,
|
||||
created_at
|
||||
""",
|
||||
agent_id,
|
||||
)
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
commands = []
|
||||
|
||||
for row in rows:
|
||||
commands.append(
|
||||
{
|
||||
"command_id": row["id"],
|
||||
"command_type": row["command_type"],
|
||||
"request_payload": row["request_payload"],
|
||||
"created_at": row["created_at"].isoformat() if row["created_at"] else None,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"commands": commands,
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/agent/commands/result")
|
||||
async def minerops_agent_commands_result_v2(request: Request):
|
||||
import json
|
||||
|
||||
minerops_require_agent_secret(request)
|
||||
|
||||
body = await request.json()
|
||||
|
||||
command_id = str(body.get("command_id") or "").strip()
|
||||
status = str(body.get("status") or "").strip().lower()
|
||||
result_payload = body.get("result_payload") or {}
|
||||
error_message = body.get("error_message")
|
||||
|
||||
if not command_id:
|
||||
raise HTTPException(status_code=400, detail="Missing command_id.")
|
||||
|
||||
if status not in {"completed", "failed"}:
|
||||
status = "completed"
|
||||
|
||||
await minerops_ensure_agent_command_queue_table()
|
||||
|
||||
conn = await minerops_command_db_connect()
|
||||
|
||||
try:
|
||||
await conn.execute(
|
||||
"""
|
||||
UPDATE minerops_agent_command_queue
|
||||
SET
|
||||
command_status = $2,
|
||||
result_payload = $3::jsonb,
|
||||
error_message = $4,
|
||||
completed_at = now()
|
||||
WHERE id = $1
|
||||
""",
|
||||
command_id,
|
||||
status,
|
||||
json.dumps(result_payload),
|
||||
error_message,
|
||||
)
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"command_id": command_id,
|
||||
"status": status,
|
||||
"message": "Agent command result received.",
|
||||
}
|
||||
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# Phase 5K.5: Agent control web page
|
||||
|
||||
Reference in new issue
Block a user