mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-26 17:41:58 -06:00
Merge pull request #87 from bunny-lab-io/codex/update-device_list.jsx-for-new-api
Add agent hash list API and update device list hashing logic
This commit is contained in:
@@ -160,12 +160,74 @@ export default function DeviceList({ onSelectDevice }) {
|
||||
if (fetched) repoSha = fetched;
|
||||
}
|
||||
try {
|
||||
const hashById = new Map();
|
||||
const hashByGuid = new Map();
|
||||
const hashByHost = new Map();
|
||||
try {
|
||||
const hashResp = await fetch('/api/agent/hash_list');
|
||||
if (hashResp.ok) {
|
||||
const hashJson = await hashResp.json();
|
||||
const list = Array.isArray(hashJson?.agents) ? hashJson.agents : [];
|
||||
list.forEach((rec) => {
|
||||
if (!rec || typeof rec !== 'object') return;
|
||||
const hash = (rec.agent_hash || '').trim();
|
||||
if (!hash) return;
|
||||
const agentId = (rec.agent_id || '').trim();
|
||||
const guidRaw = (rec.agent_guid || '').trim().toLowerCase();
|
||||
const hostKey = (rec.hostname || '').trim().toLowerCase();
|
||||
const isMemory = (rec.source || '').trim() === 'memory';
|
||||
if (agentId && (!hashById.has(agentId) || isMemory)) {
|
||||
hashById.set(agentId, hash);
|
||||
}
|
||||
if (guidRaw && (!hashByGuid.has(guidRaw) || isMemory)) {
|
||||
hashByGuid.set(guidRaw, hash);
|
||||
}
|
||||
if (hostKey && (!hashByHost.has(hostKey) || isMemory)) {
|
||||
hashByHost.set(hostKey, hash);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
const errPayload = await hashResp.json();
|
||||
if (errPayload?.error) {
|
||||
console.warn('Failed to fetch agent hash list', errPayload.error);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to fetch agent hash list', err);
|
||||
}
|
||||
|
||||
const res = await fetch("/api/agents");
|
||||
const data = await res.json();
|
||||
const arr = Object.entries(data || {}).map(([id, a]) => {
|
||||
const hostname = a.hostname || id || "unknown";
|
||||
const agentId = (id || '').trim();
|
||||
const hostname = a.hostname || agentId || "unknown";
|
||||
const normalizedHostKey = (hostname || '').trim().toLowerCase();
|
||||
const details = detailsByHost[hostname] || {};
|
||||
const agentHash = (a.agent_hash || details.agentHash || "").trim();
|
||||
const rawGuid = (a.agent_guid || a.agentGuid || a.guid || '').trim();
|
||||
const detailGuid = (details.agentGuid || details.agent_guid || '').trim();
|
||||
const guidCandidates = [rawGuid, detailGuid].filter((g) => g && g.trim());
|
||||
let agentHash = '';
|
||||
if (agentId && hashById.has(agentId)) {
|
||||
agentHash = hashById.get(agentId) || '';
|
||||
}
|
||||
if (!agentHash) {
|
||||
for (const candidate of guidCandidates) {
|
||||
const key = (candidate || '').trim().toLowerCase();
|
||||
if (key && hashByGuid.has(key)) {
|
||||
agentHash = hashByGuid.get(key) || '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!agentHash && normalizedHostKey && hashByHost.has(normalizedHostKey)) {
|
||||
agentHash = hashByHost.get(normalizedHostKey) || '';
|
||||
}
|
||||
if (!agentHash) {
|
||||
agentHash = (a.agent_hash || details.agentHash || '').trim();
|
||||
}
|
||||
const agentGuidValue = rawGuid || detailGuid || '';
|
||||
return {
|
||||
id,
|
||||
hostname,
|
||||
@@ -181,6 +243,7 @@ export default function DeviceList({ onSelectDevice }) {
|
||||
externalIp: details.externalIp || "",
|
||||
lastReboot: details.lastReboot || "",
|
||||
description: details.description || "",
|
||||
agentGuid: agentGuidValue,
|
||||
agentHash,
|
||||
agentVersion: computeAgentVersion(agentHash, repoSha),
|
||||
};
|
||||
@@ -229,6 +292,7 @@ export default function DeviceList({ onSelectDevice }) {
|
||||
const lastReboot = summary.last_reboot || "";
|
||||
const description = summary.description || "";
|
||||
const agentHashValue = (summary.agent_hash || "").trim();
|
||||
const agentGuidValue = (summary.agent_guid || summary.agentGuid || "").trim();
|
||||
const enriched = {
|
||||
lastUser,
|
||||
created: createdRaw,
|
||||
@@ -239,6 +303,7 @@ export default function DeviceList({ onSelectDevice }) {
|
||||
lastReboot,
|
||||
description,
|
||||
agentHash: agentHashValue,
|
||||
agentGuid: agentGuidValue,
|
||||
};
|
||||
setDetailsByHost((prev) => ({
|
||||
...prev,
|
||||
@@ -258,6 +323,7 @@ export default function DeviceList({ onSelectDevice }) {
|
||||
externalIp: enriched.externalIp || r.externalIp,
|
||||
lastReboot: enriched.lastReboot || r.lastReboot,
|
||||
description: enriched.description || r.description,
|
||||
agentGuid: agentGuidValue || r.agentGuid || "",
|
||||
agentHash: nextHash,
|
||||
agentVersion: computeAgentVersion(nextHash, repoSha),
|
||||
};
|
||||
|
||||
@@ -559,6 +559,148 @@ def _lookup_agent_hash_by_guid(agent_guid: str) -> Optional[Dict[str, Any]]:
|
||||
pass
|
||||
|
||||
|
||||
def _collect_agent_hash_records() -> List[Dict[str, Any]]:
|
||||
"""Aggregate known agent hash records from memory and the database."""
|
||||
|
||||
records: List[Dict[str, Any]] = []
|
||||
key_to_index: Dict[str, int] = {}
|
||||
|
||||
def _register(
|
||||
agent_id: Optional[str],
|
||||
agent_guid: Optional[str],
|
||||
hostname: Optional[str],
|
||||
agent_hash: Optional[str],
|
||||
source: Optional[str],
|
||||
) -> None:
|
||||
normalized_id = (agent_id or '').strip()
|
||||
normalized_guid = _normalize_guid(agent_guid)
|
||||
normalized_hostname = (hostname or '').strip()
|
||||
normalized_hash = (agent_hash or '').strip()
|
||||
keys: List[str] = []
|
||||
if normalized_id:
|
||||
keys.append(f'id:{normalized_id.lower()}')
|
||||
if normalized_guid:
|
||||
keys.append(f'guid:{normalized_guid.lower()}')
|
||||
if normalized_hostname:
|
||||
keys.append(f'host:{normalized_hostname.lower()}')
|
||||
|
||||
if not keys:
|
||||
records.append(
|
||||
{
|
||||
'agent_id': normalized_id or None,
|
||||
'agent_guid': normalized_guid or None,
|
||||
'hostname': normalized_hostname or None,
|
||||
'agent_hash': normalized_hash or None,
|
||||
'source': source or None,
|
||||
}
|
||||
)
|
||||
return
|
||||
|
||||
existing_idx: Optional[int] = None
|
||||
for key in keys:
|
||||
if key in key_to_index:
|
||||
existing_idx = key_to_index[key]
|
||||
break
|
||||
|
||||
if existing_idx is None:
|
||||
idx = len(records)
|
||||
records.append(
|
||||
{
|
||||
'agent_id': normalized_id or None,
|
||||
'agent_guid': normalized_guid or None,
|
||||
'hostname': normalized_hostname or None,
|
||||
'agent_hash': normalized_hash or None,
|
||||
'source': source or None,
|
||||
}
|
||||
)
|
||||
for key in keys:
|
||||
key_to_index[key] = idx
|
||||
return
|
||||
|
||||
existing = records[existing_idx]
|
||||
prev_hash = (existing.get('agent_hash') or '').strip()
|
||||
if normalized_hash and (not prev_hash or source == 'memory'):
|
||||
existing['agent_hash'] = normalized_hash
|
||||
if source:
|
||||
existing['source'] = source
|
||||
if normalized_id and not (existing.get('agent_id') or '').strip():
|
||||
existing['agent_id'] = normalized_id
|
||||
if normalized_guid and not (existing.get('agent_guid') or '').strip():
|
||||
existing['agent_guid'] = normalized_guid
|
||||
if normalized_hostname and not (existing.get('hostname') or '').strip():
|
||||
existing['hostname'] = normalized_hostname
|
||||
if source == 'memory':
|
||||
existing['source'] = 'memory'
|
||||
for key in keys:
|
||||
key_to_index[key] = existing_idx
|
||||
|
||||
conn = None
|
||||
try:
|
||||
conn = _db_conn()
|
||||
cur = conn.cursor()
|
||||
cur.execute('SELECT hostname, agent_hash, details, guid FROM device_details')
|
||||
for hostname, stored_hash, details_json, row_guid in cur.fetchall():
|
||||
try:
|
||||
details = json.loads(details_json or '{}')
|
||||
except Exception:
|
||||
details = {}
|
||||
summary = details.get('summary') or {}
|
||||
summary_hash = (summary.get('agent_hash') or '').strip()
|
||||
summary_guid = summary.get('agent_guid') or ''
|
||||
summary_agent_id = (summary.get('agent_id') or '').strip()
|
||||
normalized_hash = (stored_hash or '').strip() or summary_hash
|
||||
_register(
|
||||
summary_agent_id or None,
|
||||
summary_guid or row_guid,
|
||||
hostname,
|
||||
normalized_hash,
|
||||
'database',
|
||||
)
|
||||
except Exception as exc:
|
||||
_write_service_log('server', f'collect_agent_hash_records database error: {exc}')
|
||||
finally:
|
||||
if conn:
|
||||
try:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
for agent_id, info in (registered_agents or {}).items():
|
||||
if agent_id and isinstance(agent_id, str) and agent_id.lower().endswith('-script'):
|
||||
continue
|
||||
if info.get('is_script_agent'):
|
||||
continue
|
||||
_register(
|
||||
agent_id,
|
||||
info.get('agent_guid'),
|
||||
info.get('hostname'),
|
||||
info.get('agent_hash'),
|
||||
'memory',
|
||||
)
|
||||
except Exception as exc:
|
||||
_write_service_log('server', f'collect_agent_hash_records memory error: {exc}')
|
||||
|
||||
records.sort(
|
||||
key=lambda r: (
|
||||
(r.get('hostname') or '').lower(),
|
||||
(r.get('agent_id') or '').lower(),
|
||||
)
|
||||
)
|
||||
sanitized: List[Dict[str, Any]] = []
|
||||
for rec in records:
|
||||
sanitized.append(
|
||||
{
|
||||
'agent_id': rec.get('agent_id') or None,
|
||||
'agent_guid': rec.get('agent_guid') or None,
|
||||
'hostname': rec.get('hostname') or None,
|
||||
'agent_hash': (rec.get('agent_hash') or '').strip() or None,
|
||||
'source': rec.get('source') or None,
|
||||
}
|
||||
)
|
||||
return sanitized
|
||||
|
||||
|
||||
def _apply_agent_hash_update(agent_id: str, agent_hash: str, agent_guid: Optional[str] = None) -> Tuple[Dict[str, Any], int]:
|
||||
agent_id = (agent_id or '').strip()
|
||||
agent_hash = (agent_hash or '').strip()
|
||||
@@ -734,6 +876,16 @@ def api_agent_hash():
|
||||
return jsonify(payload), status
|
||||
|
||||
|
||||
@app.route("/api/agent/hash_list", methods=["GET"])
|
||||
def api_agent_hash_list():
|
||||
try:
|
||||
records = _collect_agent_hash_records()
|
||||
return jsonify({'agents': records})
|
||||
except Exception as exc:
|
||||
_write_service_log('server', f'/api/agent/hash_list error: {exc}')
|
||||
return jsonify({'error': 'internal error'}), 500
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
# Server Time Endpoint
|
||||
# ---------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user