"""Transactional discovery authority and append-only evidence, using existing schema. All writers acquire bounded table locks in the same order. The miners lock also serializes INSERT phantoms with legacy writers which do not take our advisory lock. This deliberately favors correctness over fleet-scale write concurrency. No miner I/O occurs here. No discovered address authorizes a physical command. """ from contextlib import contextmanager from datetime import datetime, timezone, timedelta from hashlib import sha256 import ipaddress import json import re import math from uuid import UUID, uuid4, uuid5, NAMESPACE_URL import jwt import psycopg from psycopg.rows import dict_row from psycopg.types.json import Jsonb from fastapi import HTTPException from app.config import settings from app.agent_zones.repository import default_connection_factory TTL = timedelta(minutes=15) MAX_BATCH = 128 def reject(reason, code=409): raise HTTPException(status_code=code, detail=reason) def object_body(body): if not isinstance(body, dict): reject("discovery_object_required", 422) return body def address(value): try: text = str(value) if "%" in text or "/" in text: raise ValueError() return str(ipaddress.ip_address(text)) except (ValueError, TypeError): reject("discovery_invalid_ip", 422) def mac(value): if value in (None, ""): return None if not isinstance(value, str) or not re.fullmatch( r"(?:[0-9a-fA-F]{12}|(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}|(?:[0-9a-fA-F]{2}-){5}[0-9a-fA-F]{2})", value, ): reject("discovery_invalid_mac", 422) key = value.replace(":", "").replace("-", "").lower() if key in {"000000000000", "ffffffffffff"} or int(key[:2], 16) & 1: reject("discovery_invalid_mac", 422) return ":".join(key[i : i + 2] for i in range(0, 12, 2)).upper() def digest(value): return sha256( json.dumps(value, sort_keys=True, default=str, separators=(",", ":")).encode() ).hexdigest() def text(value, limit=255): if value is None: return None if not isinstance(value, str) or len(value) > limit: reject("discovery_invalid_text", 422) return value.strip() or None def number(value, integer=False): if value is None or value == "": return None try: if isinstance(value, bool): raise ValueError() parsed = float(value) if not math.isfinite(parsed): raise ValueError() return int(parsed) if integer else parsed except (ValueError, TypeError, OverflowError): reject("discovery_invalid_measurement", 422) def insert(cur, table, values): # Identifiers originate exclusively from fixed mappings in this module. cur.execute( "INSERT INTO " + table + "(" + ",".join(values) + ") VALUES (" + ",".join(["%s"] * len(values)) + ")", tuple(values.values()), ) def status_snapshot(cur, row, raw, api3=False): raw = object_body(raw or {}) if api3: msg = object_body(object_body(raw.get("msg") or {}).get("summary") or {}) names = { "hash-average": "mhs_av", "power-realtime": "power_w", "power-rate": "power_rate_w_per_th", "chip-temp-avg": "chip_temp_avg_c", "chip-temp-max": "chip_temp_max_c", "environment-temperature": "env_temp_c", } else: msg = object_body(raw.get("Msg") or raw.get("msg") or {}) names = { "MHS av": "mhs_av", "MHS 1m": "mhs_1m", "MHS 15m": "mhs_15m", "HS RT": "hs_rt", "Power": "power_w", "Power Rate": "power_rate_w_per_th", "Env Temp": "env_temp_c", "Chip Temp Min": "chip_temp_min_c", "Chip Temp Max": "chip_temp_max_c", "Chip Temp Avg": "chip_temp_avg_c", } values = { "miner_id": row["id"], "observed_ip": row["current_ip"], "raw_summary_json": Jsonb(raw), } for key, column in names.items(): if key in msg: values[column] = number(msg[key]) if api3 and values.get("mhs_av") is not None: values["mhs_av"] *= 1000000 for key, column in ( { "fan-speed-in": "fan_speed_in_rpm", "fan-speed-out": "fan_speed_out_rpm", "bootup-time": "uptime_seconds", } if api3 else { "Fan Speed In": "fan_speed_in_rpm", "Fan Speed Out": "fan_speed_out_rpm", "Uptime": "uptime_seconds", } ).items(): if key in msg: values[column] = number(msg[key], True) insert(cur, "miner_status_snapshots", values) def pool_snapshot(cur, row, pool, agent_id): object_body(pool) values = { "miner_id": row["id"], "ip_address": row["current_ip"], "mac_address": row["current_mac"], "raw_pool_json": Jsonb(pool), "agent_id": agent_id, } for keys, column in [ (("URL", "url"), "url"), (("User", "account", "user"), "worker_user"), (("Status", "status"), "pool_status"), ]: v = next((pool[k] for k in keys if k in pool), None) if v is not None: values[column] = text(v, 2048) for keys, column in [ (("POOL", "id"), "pool_index"), (("Accepted", "accepted"), "accepted"), (("Rejected", "rejected"), "rejected"), ]: v = next((pool[k] for k in keys if k in pool), None) if v is not None: values[column] = number(v, True) insert(cur, "miner_pool_snapshots", values) class DiscoveryRepository: def __init__(self, connection_factory=default_connection_factory): self.connection_factory = connection_factory @contextmanager def transaction(self): with self.connection_factory() as conn: with conn.cursor(row_factory=dict_row) as cur: self.lock(cur) yield cur @staticmethod def lock(cur): cur.execute("SET LOCAL lock_timeout = '5s'") cur.execute("SET LOCAL statement_timeout = '15s'") cur.execute("SELECT pg_advisory_xact_lock(743190031)") cur.execute( "LOCK TABLE minerops_site_agents, agent_zones, agent_zone_network_ranges IN SHARE MODE" ) cur.execute("LOCK TABLE miners IN SHARE ROW EXCLUSIVE MODE") @staticmethod def audit(cur, action, entity, value, actor=None, old=None): cur.execute( """INSERT INTO audit_logs (actor_user_id, action, entity_type, entity_id, old_value_json, new_value_json) VALUES (%s, %s, 'discovery', %s, %s, %s)""", ( actor, action, entity, Jsonb(old) if old is not None else None, Jsonb(value), ), ) @staticmethod def context(cur, agent_id): cur.execute( """SELECT a.agent_id, a.site_id, a.agent_zone_id, a.credential_created_at, a.credential_revoked_at, z.updated_at AS zone_version, z.is_enabled, s.name AS site_name FROM minerops_site_agents a JOIN agent_zones z ON z.id=a.agent_zone_id AND z.site_id=a.site_id JOIN sites s ON s.id=a.site_id WHERE a.agent_id=%s FOR SHARE OF a,z""", (agent_id,), ) row = cur.fetchone() if not row or not row["is_enabled"] or row["credential_revoked_at"] is not None: reject("discovery_routing_unavailable") cur.execute( "SELECT network::text FROM agent_zone_network_ranges WHERE agent_zone_id=%s ORDER BY network,id FOR SHARE", (row["agent_zone_id"],), ) row["networks"] = [v["network"] for v in cur.fetchall()] if not row["networks"]: reject("discovery_network_unassigned") row["authority"] = digest( { k: row[k] for k in ( "agent_id", "site_id", "agent_zone_id", "credential_created_at", "zone_version", "networks", ) } ) return row @staticmethod def in_network(ctx, ip): ip = address(ip) if not any( ipaddress.ip_address(ip) in ipaddress.ip_network(n) for n in ctx["networks"] ): reject("discovery_address_outside_zone") @staticmethod def identity(body, agent_id): object_body(body) if body.get("agent_id") not in (None, "", agent_id): reject("discovery_agent_identity_conflict", 403) @staticmethod def fingerprint(row): return ( digest( { k: row.get(k) for k in ( "id", "site_id", "agent_zone_id", "current_mac", "current_ip", "updated_at", ) } ) if row else None ) def match(self, cur, ctx, item): """All supplied identifiers must agree. No global IP search or ownership claim.""" if ( item.get("ip_address") and item.get("ip") and address(item["ip_address"]) != address(item["ip"]) ): reject("discovery_identity_conflict") if ( item.get("mac_address") and item.get("mac") and mac(item["mac_address"]) != mac(item["mac"]) ): reject("discovery_identity_conflict") ip = address(item.get("ip_address") or item.get("ip")) self.in_network(ctx, ip) supplied_mac = mac(item.get("mac_address") or item.get("mac")) candidates = [] if item.get("miner_id"): try: mid = UUID(str(item["miner_id"])) except ValueError: reject("discovery_invalid_miner_id", 422) cur.execute( "SELECT * FROM miners WHERE id=%s AND agent_zone_id=%s FOR UPDATE", (mid, ctx["agent_zone_id"]), ) row = cur.fetchone() if not row: reject("discovery_identity_conflict") candidates.append(row) if supplied_mac: key = supplied_mac.replace(":", "").lower() cur.execute( """SELECT m.* FROM miner_mac_ownerships o JOIN miners m ON m.id=o.miner_id WHERE o.normalized_mac=%s FOR UPDATE OF m""", (key,), ) row = cur.fetchone() if row: if ( row["agent_zone_id"] != ctx["agent_zone_id"] or mac(row["current_mac"]) != supplied_mac ): reject("discovery_identity_conflict") candidates.append(row) else: cur.execute( """SELECT * FROM miners WHERE agent_zone_id=%s AND regexp_replace(lower(current_mac),'[^0-9a-f]','','g')=%s FOR UPDATE""", (ctx["agent_zone_id"], key), ) rows = cur.fetchall() if len(rows) > 1: reject("discovery_identity_ambiguous") candidates.extend(rows) serial = text(item.get("miner_sn")) if serial: cur.execute( "SELECT * FROM miners WHERE miner_sn=%s ORDER BY id LIMIT 2 FOR UPDATE", (serial,), ) serial_rows = cur.fetchall() if len(serial_rows) > 1 or any( r["agent_zone_id"] != ctx["agent_zone_id"] for r in serial_rows ): reject("discovery_identity_conflict") candidates.extend(serial_rows) cur.execute( "SELECT * FROM miners WHERE agent_zone_id=%s AND host(current_ip)=%s ORDER BY id LIMIT 2 FOR UPDATE", (ctx["agent_zone_id"], ip), ) rows = cur.fetchall() if len(rows) > 1: reject("discovery_identity_ambiguous") candidates.extend(rows) if len({r["id"] for r in candidates}) > 1: reject("discovery_identity_conflict") row = candidates[0] if candidates else None if row and ( str(row["current_ip"]) != ip or (supplied_mac and mac(row["current_mac"]) != supplied_mac) or (serial and row["miner_sn"] != serial) ): reject("discovery_identity_conflict") return row @staticmethod def observation_version(row): return row["updated_at"].isoformat() def observed(self, cur, ctx, item, parent=None): ip = address(item.get("ip_address")) self.in_network(ctx, ip) canonical_mac = mac(item.get("mac_address")) if item.get("discovered_at"): try: reported = datetime.fromisoformat( item["discovered_at"].replace("Z", "+00:00") ) if ( reported.tzinfo is None or not -timedelta(minutes=5) <= datetime.now(timezone.utc) - reported <= TTL ): raise ValueError() except (ValueError, AttributeError, TypeError): reject("discovery_stale_report", 422) # Input is kept verbatim; authoritative provenance exists only in audit_logs. oid = str( uuid5( NAMESPACE_URL, "minerops-discovery:" + ctx["authority"] + ":" + digest(item), ) ) cur.execute( "SELECT * FROM minerops_agent_discoveries WHERE id=%s FOR UPDATE", (oid,) ) existing = cur.fetchone() if existing: return existing status = "new" reason = None matched = None try: matched = self.match( cur, ctx, { **item, "ip_address": ip, "mac_address": canonical_mac, "miner_sn": item.get("miner_sn"), }, ) except HTTPException as exc: if exc.status_code != 409: raise status = "zone_conflict" reason = exc.detail if matched: status = "known" cur.execute( """INSERT INTO minerops_agent_discoveries (id,agent_id,agent_zone_id,site_name,room_name,ip_address,mac_address,model,hostname,miner_sn, raw_payload,discovery_status,matched_miner_id) VALUES (%s,%s,%s,%s,%s,%s::inet,%s,%s,%s,%s,%s,%s,%s) RETURNING *""", ( oid, ctx["agent_id"], ctx["agent_zone_id"], ctx["site_name"], text(item.get("room_name")), ip, canonical_mac, text(item.get("model")), text(item.get("hostname")), text(item.get("miner_sn")), Jsonb(item), status, matched["id"] if matched else None, ), ) row = cur.fetchone() self.audit( cur, "discovery.observed", oid, { "agent_id": ctx["agent_id"], "authority": ctx["authority"], "payload_hash": digest(item), "miner_fingerprint": self.fingerprint(matched), "reason": reason, "parent_observation_id": parent, "version": self.observation_version(row), }, ) return row def ingest(self, agent_id, body): self.identity(body, agent_id) items = body.get("discoveries") if not isinstance(items, list) or len(items) > MAX_BATCH: reject("discovery_invalid_batch", 422) if len({address(object_body(i).get("ip_address")) for i in items}) != len( items ): reject("discovery_duplicate_batch_address", 422) with self.transaction() as cur: ctx = self.context(cur, agent_id) cur.execute( """SELECT count(*) FROM audit_logs WHERE action='discovery.observed' AND new_value_json->>'agent_id'=%s AND created_at>clock_timestamp()-interval '1 minute'""", (agent_id,), ) if cur.fetchone()["count"] + len(items) > 1024: reject("discovery_rate_limit", 429) rows = [self.observed(cur, ctx, object_body(i)) for i in items] return { "ok": True, "received": len(items), "inserted": len(rows), "matched_existing": sum(r["discovery_status"] == "known" for r in rows), "staged_new": sum(r["discovery_status"] == "new" for r in rows), "conflicts": sum( r["discovery_status"] == "zone_conflict" for r in rows ), "agent_zone_id": str(ctx["agent_zone_id"]), "routing_reason": None, "errors": [], "discoveries": [self.present(r) for r in rows], } @staticmethod def present(row): return { **{ k: ( str(v) if isinstance( v, (UUID, datetime, ipaddress.IPv4Address, ipaddress.IPv6Address), ) else v ) for k, v in row.items() if k != "raw_payload" }, "version": row["updated_at"].isoformat(), } def listing(self): with self.connection_factory() as conn, conn.cursor( row_factory=dict_row ) as cur: cur.execute( "SELECT * FROM minerops_agent_discoveries ORDER BY created_at DESC,id LIMIT 100" ) return { "ok": True, "discoveries": [self.present(r) for r in cur.fetchall()], } def observation(self, cur, oid, version): try: UUID(str(oid)) except (ValueError, TypeError): reject("discovery_id_required", 422) cur.execute( "SELECT * FROM minerops_agent_discoveries WHERE id=%s FOR UPDATE", (str(oid),), ) row = cur.fetchone() if not row: reject("discovery_not_found", 404) ctx = self.context(cur, row["agent_id"]) if row["agent_zone_id"] != ctx["agent_zone_id"]: reject("discovery_stale_authority") cur.execute( "SELECT new_value_json,created_at FROM audit_logs WHERE entity_type='discovery' AND entity_id=%s AND action='discovery.observed' ORDER BY created_at LIMIT 2", (str(oid),), ) proofs = cur.fetchall() if len(proofs) != 1: reject("discovery_unverified_history") proof = proofs[0] if proof["new_value_json"].get("authority") != ctx["authority"] or proof[ "new_value_json" ].get("payload_hash") != digest(row["raw_payload"]): reject("discovery_stale_authority") if self.observation_version(row) != proof["new_value_json"].get("version"): cur.execute( "SELECT new_value_json FROM audit_logs WHERE entity_id=%s AND action='discovery.reviewed' ORDER BY created_at DESC LIMIT 1", (str(oid),), ) decision = cur.fetchone() if not decision: reject("discovery_unverified_history") saved = decision["new_value_json"] response = saved.get("response", {}) expected_state = {"ignore": "ignored", "non_miner": "non_miner"}.get( saved.get("action") ) if expected_state is None: expected_state = ( "known" if proof["new_value_json"].get("miner_fingerprint") else "added" ) if ( response.get("version") != self.observation_version(row) or response.get("miner_id") != (str(row["matched_miner_id"]) if row["matched_miner_id"] else None) or row["discovery_status"] != expected_state ): reject("discovery_unverified_history") self.in_network(ctx, str(row["ip_address"])) if datetime.now(timezone.utc) - row["created_at"] > TTL: reject("discovery_stale_observation") cur.execute( """SELECT 1 FROM minerops_agent_discoveries WHERE agent_id=%s AND agent_zone_id=%s AND ip_address=%s AND id<>%s AND created_at>=%s LIMIT 1""", ( row["agent_id"], row["agent_zone_id"], row["ip_address"], row["id"], row["created_at"], ), ) if cur.fetchone(): reject("discovery_superseded_observation") return row, ctx, proof["new_value_json"] def review(self, body, user): object_body(body) oid = body.get("discovery_id") version = body.get("version") action = body.get("action") if not isinstance(action, str) or action not in { "add_to_inventory", "ignore", "non_miner", }: reject("discovery_invalid_action", 422) with self.transaction() as cur: row, ctx, proof = self.observation(cur, oid, version) cur.execute( "SELECT new_value_json FROM audit_logs WHERE entity_id=%s AND action='discovery.reviewed' ORDER BY created_at DESC LIMIT 1", (oid,), ) prior = cur.fetchone() if prior: decision = prior["new_value_json"] if ( decision["request_version"] == version and decision["action"] == action ): return decision["response"] reject("discovery_already_reviewed") if self.observation_version(row) != version: reject("discovery_version_conflict") if row["discovery_status"] not in {"new", "known"}: reject("discovery_not_reviewable") matched = self.match( cur, ctx, { "ip_address": str(row["ip_address"]), "mac_address": row["mac_address"], "miner_sn": row["miner_sn"], }, ) if self.fingerprint(matched) != proof["miner_fingerprint"]: reject("discovery_inventory_changed") mid = matched["id"] if matched else None state = {"ignore": "ignored", "non_miner": "non_miner"}.get( action, "known" if mid else "added" ) if action == "add_to_inventory" and not mid: if not row["mac_address"]: reject("discovery_enrichment_required") cur.execute( """INSERT INTO miners (site_id,agent_zone_id,current_mac,current_ip,hostname,miner_sn,model,miner_type,status,notes) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,'discovered','Added by audited observation approval') RETURNING id""", ( ctx["site_id"], ctx["agent_zone_id"], row["mac_address"], row["ip_address"], row["hostname"], row["miner_sn"], row["model"], row["model"], ), ) mid = cur.fetchone()["id"] cur.execute( "UPDATE minerops_agent_discoveries SET discovery_status=%s,matched_miner_id=%s,updated_at=clock_timestamp() WHERE id=%s RETURNING *", (state, mid, oid), ) updated = cur.fetchone() response = { "ok": True, "action": action, "discovery_id": oid, "miner_id": str(mid) if mid else None, "version": self.observation_version(updated), "message": "Discovery decision saved.", } self.audit( cur, "discovery.reviewed", oid, { "agent_id": ctx["agent_id"], "agent_zone_id": str(ctx["agent_zone_id"]), "action": action, "request_version": version, "response": response, }, user["id"], self.present(row), ) return response def token(self, ctx, row): return jwt.encode( { "purpose": "minerops.discovery.miner", "agent_id": ctx["agent_id"], "authority": ctx["authority"], "miner_id": str(row["id"]), "fingerprint": self.fingerprint(row), "exp": datetime.now(timezone.utc) + TTL, }, settings.jwt_secret_key, algorithm="HS256", ) def known(self, agent_id): with self.transaction() as cur: ctx = self.context(cur, agent_id) cur.execute( """SELECT m.*,r.name AS room_name FROM miners m LEFT JOIN rooms r ON r.id=m.room_id AND r.site_id=m.site_id WHERE m.agent_zone_id=%s AND m.current_ip IS NOT NULL ORDER BY m.id FOR SHARE OF m""", (ctx["agent_zone_id"],), ) rows = cur.fetchall() result = [] for r in rows: self.in_network(ctx, str(r["current_ip"])) result.append( { "miner_id": str(r["id"]), "miner_version": self.token(ctx, r), "ip_address": str(r["current_ip"]), "mac_address": r["current_mac"], "room_name": r["room_name"] or ctx["site_name"], "model": r["model"], "status": r["status"], } ) return {"ok": True, "count": len(result), "miners": result} def bound_miner(self, cur, ctx, item): object_body(item) try: proof = jwt.decode( item.get("miner_version", ""), settings.jwt_secret_key, algorithms=["HS256"], options={"require": ["exp"]}, ) except jwt.PyJWTError: reject("discovery_miner_version_required") if ( proof.get("purpose") != "minerops.discovery.miner" or proof.get("agent_id") != ctx["agent_id"] or proof.get("authority") != ctx["authority"] or proof.get("miner_id") != str(item.get("miner_id")) ): reject("discovery_stale_authority") row = self.match(cur, ctx, item) if not row or proof.get("fingerprint") != self.fingerprint(row): reject("discovery_inventory_changed") return row def queue(self, body, user, kind="enrich_match_discovery"): object_body(body) with self.transaction() as cur: if kind == "enrich_match_discovery": row, ctx, proof = self.observation( cur, body.get("discovery_id"), body.get("version") ) if ( self.observation_version(row) != body.get("version") or row["discovery_status"] != "new" ): reject("discovery_not_enrichable") item = { "ip_address": str(row["ip_address"]), "mac_address": row["mac_address"], "miner_sn": row["miner_sn"], } matched = self.match(cur, ctx, item) if self.fingerprint(matched) != proof["miner_fingerprint"]: reject("discovery_inventory_changed") payload = { "discovery_id": row["id"], "version": body["version"], "ip_address": str(row["ip_address"]), } entity = row["id"] else: if not isinstance(body.get("agent_id"), str): reject("discovery_agent_id_required", 422) ctx = self.context(cur, body["agent_id"]) payload = {} entity = None if kind != "enrich_match_discovery": try: payload["request_id"] = str( UUID(str(body.get("request_id") or uuid4())) ) except (ValueError, TypeError): reject("discovery_request_id_invalid", 422) payload.update( { "schema_version": 1, "agent_zone_id": str(ctx["agent_zone_id"]), "authority": ctx["authority"], } ) # Queue requests are idempotent for the same principal/type/observation version. qid = str( uuid5( NAMESPACE_URL, "minerops-discovery-command:" + kind + ":" + digest(payload) + ":" + str(user["id"]), ) ) cur.execute( """INSERT INTO minerops_agent_command_queue (id,agent_id,command_type,command_status, requested_by_user_id,requested_by_username,request_payload) VALUES (%s,%s,%s,'pending',%s,%s,%s) ON CONFLICT(id) DO NOTHING RETURNING id""", ( qid, ctx["agent_id"], kind, str(user["id"]), user["username"], Jsonb(payload), ), ) if cur.fetchone(): self.audit( cur, "discovery.command_queued", entity, { "command_id": qid, "agent_id": ctx["agent_id"], "type": kind, "payload": payload, }, user["id"], ) return { "ok": True, "command_id": qid, "message": "Scoped read command queued.", } def validate_command(self, cur, agent_id, command): ctx = self.context(cur, agent_id) p = command["request_payload"] if ( not isinstance(p, dict) or p.get("schema_version") != 1 or p.get("agent_zone_id") != str(ctx["agent_zone_id"]) or p.get("authority") != ctx["authority"] ): reject("discovery_command_stale_authority") if command["command_type"] == "enrich_match_discovery": row, _, proof = self.observation( cur, p.get("discovery_id"), p.get("version") ) if ( row["agent_id"] != agent_id or self.observation_version(row) != p["version"] or row["discovery_status"] != "new" or str(row["ip_address"]) != p.get("ip_address") ): reject("discovery_command_stale_observation") if ( self.fingerprint( self.match( cur, ctx, { "ip_address": p["ip_address"], "mac_address": row["mac_address"], "miner_sn": row["miner_sn"], }, ) ) != proof["miner_fingerprint"] ): reject("discovery_inventory_changed") return ctx def target(self, agent_id, body): self.identity(body, agent_id) try: UUID(str(body.get("command_id"))) except (ValueError, TypeError): reject("discovery_command_id_required", 422) with self.transaction() as cur: cur.execute( "SELECT * FROM minerops_agent_command_queue WHERE id=%s AND agent_id=%s FOR UPDATE", (body.get("command_id"), agent_id), ) command = cur.fetchone() if ( not command or command["command_status"] != "claimed" or command["command_type"] != "enrich_match_discovery" ): reject("discovery_enrichment_command_required") self.validate_command(cur, agent_id, command) p = command["request_payload"] if p["discovery_id"] != body.get("discovery_id") or p[ "version" ] != body.get("version"): reject("discovery_command_observation_conflict") return {"ok": True, "ip_address": p["ip_address"]} def enrich(self, agent_id, body): self.identity(body, agent_id) with self.transaction() as cur: ctx = self.context(cur, agent_id) if body.get("discovery_id"): try: UUID(str(body.get("command_id"))) except (ValueError, TypeError): reject("discovery_command_id_required", 422) row, _, proof = self.observation( cur, body["discovery_id"], body.get("version") ) if ( row["agent_id"] != agent_id or row["discovery_status"] != "new" or body.get("version") != self.observation_version(row) ): reject("discovery_not_enrichable") cur.execute( "SELECT * FROM minerops_agent_command_queue WHERE id=%s AND agent_id=%s FOR UPDATE", (body.get("command_id"), agent_id), ) command = cur.fetchone() if ( not command or command["command_status"] != "claimed" or command["command_type"] != "enrich_match_discovery" ): reject("discovery_enrichment_command_required") self.validate_command(cur, agent_id, command) if command["request_payload"]["discovery_id"] != row["id"]: reject("discovery_command_observation_conflict") if address(body.get("ip_address")) != str(row["ip_address"]): reject("discovery_identity_conflict") item = { k: body.get(k) for k in ( "ip_address", "mac_address", "model", "hostname", "miner_sn", ) } item["parent_observation_id"] = row["id"] item["command_id"] = command["id"] child = self.observed(cur, ctx, item, parent=row["id"]) return { "ok": True, "matched": bool(child["matched_miner_id"]), "discovery": self.present(child), "message": "Enrichment appended; review the new observation.", } row = self.bound_miner(cur, ctx, body) changes = { k: text(body.get(k)) for k in ( "hostname", "model", "miner_type", "api_version", "firmware_version", "platform", ) } changes = {k: v for k, v in changes.items() if v is not None} if changes: sql = ", ".join(k + "=%s" for k in changes) cur.execute( "UPDATE miners SET " + sql + ",updated_at=clock_timestamp() WHERE id=%s AND agent_zone_id=%s RETURNING *", (*changes.values(), row["id"], ctx["agent_zone_id"]), ) updated = cur.fetchone() else: updated = row self.audit( cur, "discovery.miner_enriched", row["id"], { "agent_id": agent_id, "changes": changes, "raw_version": body.get("raw_version"), "raw_miner_info": body.get("raw_miner_info"), }, old={"fingerprint": self.fingerprint(row)}, ) return { "ok": True, "matched": True, "miner": { k: ( str(v) if isinstance( v, ( UUID, datetime, ipaddress.IPv4Address, ipaddress.IPv6Address, ), ) else v ) for k, v in updated.items() }, "message": "Enrichment saved without changing ownership or history.", } def snapshots(self, agent_id, body, kind="miner"): self.identity(body, agent_id) items = ( body.get("miners") if kind == "miner" else body.get("pool_snapshots", body.get("snapshots")) ) if not isinstance(items, list) or len(items) > MAX_BATCH: reject("discovery_invalid_batch", 422) with self.transaction() as cur: ctx = self.context(cur, agent_id) for item in items: row = self.bound_miner(cur, ctx, item) if kind == "miner": if type(item.get("online")) is not bool: reject("discovery_invalid_online", 422) status = "online" if item["online"] else "offline" cur.execute( "UPDATE miners SET status=%s,last_seen_at=CASE WHEN %s THEN clock_timestamp() ELSE last_seen_at END,updated_at=clock_timestamp() WHERE id=%s AND agent_zone_id=%s", (status, item["online"], row["id"], ctx["agent_zone_id"]), ) summary = object_body( object_body(item.get("responses") or {}).get("summary") or {} ) raw = summary.get("json") if raw is None: try: raw = json.loads(summary.get("raw") or "{}") except (ValueError, TypeError): reject("discovery_invalid_summary", 422) status_snapshot(cur, row, raw) else: pools = item.get("pools", []) if not isinstance(pools, list) or len(pools) > 32: reject("discovery_invalid_pools", 422) for p in pools: object_body(p) pool_snapshot(cur, row, p, agent_id) self.audit( cur, "discovery." + kind + "_snapshot", row["id"], {"agent_id": agent_id, "payload": item}, ) if kind == "miner": cur.execute( """INSERT INTO minerops_agent_snapshots(agent_id,agent_zone_id,site_name,miner_count,payload) VALUES (%s,%s,%s,%s,%s) RETURNING id,received_at""", ( agent_id, ctx["agent_zone_id"], ctx["site_name"], len(items), Jsonb(body), ), ) receipt = cur.fetchone() else: receipt = {} return { "ok": True, "applied": len(items), "inserted": len(items), "matched": len(items), "miner_count": len(items), "snapshot_id": receipt.get("id"), "message": "Scoped telemetry appended.", } def api3(self, agent_id, body): self.identity(body, agent_id) items = body.get("items") if not isinstance(items, list) or len(items) > MAX_BATCH: reject("discovery_invalid_batch", 422) with self.transaction() as cur: ctx = self.context(cur, agent_id) counts = dict( processed=len(items), matched=len(items), api3_ok=0, api3_failed=0, miner_updates=0, status_rows=0, pool_rows=0, hashboard_rows=0, ) for item in items: object_body(item) device = object_body(item.get("device_info") or {}) message = object_body(device.get("msg") or {}) network = object_body(message.get("network") or {}) if network.get("ip") and address(network["ip"]) != address( item.get("ip") ): reject("discovery_identity_conflict") if network.get("mac") and mac(network["mac"]) != mac(item.get("mac")): reject("discovery_identity_conflict") row = self.bound_miner(cur, ctx, item) if type(item.get("api3_supported")) is not bool: reject("discovery_invalid_api3_supported", 422) miner = object_body(message.get("miner") or {}) system = object_body(message.get("system") or {}) power = object_body(message.get("power") or {}) reported_serial = text(miner.get("miner-sn")) if ( reported_serial and row["miner_sn"] and reported_serial != row["miner_sn"] ): reject("discovery_identity_conflict") metadata = { "model": text(miner.get("type")), "miner_type": text(miner.get("type")), "api_version": text(system.get("api")), "api3_api_version": text(system.get("api")), "api3_api_switch": text(system.get("apiswitch")), "firmware_version": text(system.get("fwversion")), "platform": text(system.get("platform")), } metadata = {k: v for k, v in metadata.items() if v is not None} if metadata: assignments = [ k + ( "=COALESCE(NULLIF(" + k + ",''),%s)" if k in {"model", "miner_type"} else "=%s" ) for k in metadata ] cur.execute( "UPDATE miners SET " + ",".join(assignments) + " WHERE id=%s AND agent_zone_id=%s", (*metadata.values(), row["id"], ctx["agent_zone_id"]), ) counts["api3_ok" if item["api3_supported"] else "api3_failed"] += 1 counts["miner_updates"] += 1 cur.execute( "UPDATE miners SET api3_supported=%s,api3_last_seen_at=clock_timestamp(),api3_last_error=%s,updated_at=clock_timestamp() WHERE id=%s AND agent_zone_id=%s", ( item["api3_supported"], text(item.get("api3_error")), row["id"], ctx["agent_zone_id"], ), ) cur.execute( """INSERT INTO miner_api3_enrichment_snapshots (miner_id,agent_id,site_name,ip_address,mac_address,api_supported,api_error,device_info,miner_setting,miner_status,fan_setting,custom_data,api_version,api_switch,model,miner_sn,power_sn,psu_model) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""", ( row["id"], agent_id, ctx["site_name"], row["current_ip"], row["current_mac"], item["api3_supported"], text(item.get("api3_error")), *[ Jsonb(item.get(k)) for k in ( "device_info", "miner_setting", "miner_status", "fan_setting", "custom_data", ) ], metadata.get("api_version"), metadata.get("api3_api_switch"), metadata.get("model"), reported_serial, text(power.get("sn")), text(power.get("model")), ), ) status = object_body(item.get("miner_status") or {}) status_msg = object_body(status.get("msg") or {}) summary = object_body(status_msg.get("summary") or {}) if item["api3_supported"] and summary: status_snapshot(cur, row, status, api3=True) counts["status_rows"] += 1 pools = status_msg.get("pools") or [] boards = status_msg.get("edevs") or [] if ( not isinstance(pools, list) or len(pools) > 32 or not isinstance(boards, list) or len(boards) > 32 ): reject("discovery_invalid_telemetry_batch", 422) for pool in pools: pool_snapshot(cur, row, pool, agent_id) counts["pool_rows"] += 1 for board in boards: object_body(board) values = { "miner_id": row["id"], "ip_address": row["current_ip"], "mac_address": row["current_mac"], "raw_hashboard_json": Jsonb(board), } for key, column in { "slot": "slot", "hash-average": "hash_average_ths", "factory-hash": "factory_hash_ths", "freq": "freq", "effective-chips": "effective_chips", "chip-temp-min": "chip_temp_min_c", "chip-temp-avg": "chip_temp_avg_c", "chip-temp-max": "chip_temp_max_c", }.items(): if key in board: values[column] = number( board[key], column in {"slot", "effective_chips"} ) insert(cur, "miner_api3_hashboard_snapshots", values) counts["hashboard_rows"] += 1 self.audit( cur, "discovery.api3_snapshot", row["id"], {"agent_id": agent_id, "payload": item}, ) return { "data": {"ok": True, **counts}, "meta": {}, "error": None, }