mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-26 17:41:58 -06:00
Merge pull request #86 from bunny-lab-io/codex/investigate-update.ps1-agent-hash-submission-issue
Introduce agent GUID enrollment
This commit is contained in:
@@ -535,6 +535,38 @@ def _settings_dir():
|
||||
except Exception:
|
||||
return os.path.abspath(os.path.join(os.path.dirname(__file__), 'Settings'))
|
||||
|
||||
|
||||
def _agent_guid_path() -> str:
|
||||
try:
|
||||
root = _find_project_root()
|
||||
return os.path.join(root, 'Agent', 'Borealis', 'agent_GUID')
|
||||
except Exception:
|
||||
return os.path.abspath(os.path.join(os.path.dirname(__file__), 'agent_GUID'))
|
||||
|
||||
|
||||
def _persist_agent_guid_local(guid: str):
|
||||
guid = (guid or '').strip()
|
||||
if not guid:
|
||||
return
|
||||
path = _agent_guid_path()
|
||||
try:
|
||||
directory = os.path.dirname(path)
|
||||
if directory:
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
existing = ''
|
||||
if os.path.isfile(path):
|
||||
try:
|
||||
with open(path, 'r', encoding='utf-8') as fh:
|
||||
existing = fh.read().strip()
|
||||
except Exception:
|
||||
existing = ''
|
||||
if existing != guid:
|
||||
with open(path, 'w', encoding='utf-8') as fh:
|
||||
fh.write(guid)
|
||||
except Exception as exc:
|
||||
_log_agent(f'Failed to persist agent GUID locally: {exc}', fname='agent.error.log')
|
||||
|
||||
|
||||
def get_server_url() -> str:
|
||||
"""Return the Borealis server URL from env or Agent/Borealis/Settings/server_url.txt.
|
||||
- Strips UTF-8 BOM and whitespace
|
||||
@@ -1214,7 +1246,16 @@ async def connect():
|
||||
payload = {"agent_id": AGENT_ID, "hostname": socket.gethostname(), "username": ".\\svcBorealis"}
|
||||
timeout = aiohttp.ClientTimeout(total=10)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
await session.post(url, json=payload)
|
||||
async with session.post(url, json=payload) as resp:
|
||||
if resp.status == 200:
|
||||
try:
|
||||
data = await resp.json(content_type=None)
|
||||
except Exception:
|
||||
data = None
|
||||
if isinstance(data, dict):
|
||||
guid_value = (data.get('agent_guid') or '').strip()
|
||||
if guid_value:
|
||||
_persist_agent_guid_local(guid_value)
|
||||
except Exception:
|
||||
pass
|
||||
asyncio.create_task(_svc_checkin_once())
|
||||
|
||||
@@ -401,6 +401,7 @@ def _lookup_agent_hash_record(agent_id: str) -> Optional[Dict[str, Any]]:
|
||||
|
||||
info = registered_agents.get(agent_id) or {}
|
||||
candidate = (info.get('agent_hash') or '').strip()
|
||||
candidate_guid = _normalize_guid(info.get('agent_guid')) if info.get('agent_guid') else ''
|
||||
hostname = (info.get('hostname') or '').strip()
|
||||
if candidate:
|
||||
payload: Dict[str, Any] = {
|
||||
@@ -410,6 +411,8 @@ def _lookup_agent_hash_record(agent_id: str) -> Optional[Dict[str, Any]]:
|
||||
}
|
||||
if hostname:
|
||||
payload['hostname'] = hostname
|
||||
if candidate_guid:
|
||||
payload['agent_guid'] = candidate_guid
|
||||
return payload
|
||||
|
||||
conn = None
|
||||
@@ -437,6 +440,9 @@ def _lookup_agent_hash_record(agent_id: str) -> Optional[Dict[str, Any]]:
|
||||
'hostname': row.get('hostname') or effective_hostname,
|
||||
'source': 'database',
|
||||
}
|
||||
row_guid = _normalize_guid(row.get('guid')) if row.get('guid') else ''
|
||||
if row_guid:
|
||||
payload['agent_guid'] = row_guid
|
||||
return payload
|
||||
first = rows[0]
|
||||
fallback_hash = (first.get('agent_hash') or '').strip()
|
||||
@@ -453,9 +459,12 @@ def _lookup_agent_hash_record(agent_id: str) -> Optional[Dict[str, Any]]:
|
||||
'hostname': first.get('hostname') or effective_hostname,
|
||||
'source': 'database',
|
||||
}
|
||||
row_guid = _normalize_guid(first.get('guid')) if first.get('guid') else ''
|
||||
if row_guid:
|
||||
payload['agent_guid'] = row_guid
|
||||
return payload
|
||||
cur.execute('SELECT hostname, agent_hash, details FROM device_details')
|
||||
for host, db_hash, details_json in cur.fetchall():
|
||||
cur.execute('SELECT hostname, agent_hash, details, guid FROM device_details')
|
||||
for host, db_hash, details_json, row_guid in cur.fetchall():
|
||||
try:
|
||||
data = json.loads(details_json or '{}')
|
||||
except Exception:
|
||||
@@ -467,12 +476,18 @@ def _lookup_agent_hash_record(agent_id: str) -> Optional[Dict[str, Any]]:
|
||||
summary_hash = (summary.get('agent_hash') or '').strip()
|
||||
normalized_hash = (db_hash or '').strip() or summary_hash
|
||||
if normalized_hash:
|
||||
return {
|
||||
payload = {
|
||||
'agent_id': agent_id,
|
||||
'agent_hash': normalized_hash,
|
||||
'hostname': host,
|
||||
'source': 'database',
|
||||
}
|
||||
normalized_guid = _normalize_guid(row_guid) if row_guid else ''
|
||||
if not normalized_guid:
|
||||
normalized_guid = _normalize_guid(summary.get('agent_guid')) if summary.get('agent_guid') else ''
|
||||
if normalized_guid:
|
||||
payload['agent_guid'] = normalized_guid
|
||||
return payload
|
||||
finally:
|
||||
if conn:
|
||||
try:
|
||||
@@ -482,41 +497,156 @@ def _lookup_agent_hash_record(agent_id: str) -> Optional[Dict[str, Any]]:
|
||||
return None
|
||||
|
||||
|
||||
def _apply_agent_hash_update(agent_id: str, agent_hash: str) -> Tuple[Dict[str, Any], int]:
|
||||
def _lookup_agent_hash_by_guid(agent_guid: str) -> Optional[Dict[str, Any]]:
|
||||
normalized_guid = _normalize_guid(agent_guid)
|
||||
if not normalized_guid:
|
||||
return None
|
||||
|
||||
# Prefer in-memory record when available
|
||||
for aid, rec in registered_agents.items():
|
||||
try:
|
||||
if _normalize_guid(rec.get('agent_guid')) == normalized_guid:
|
||||
candidate_hash = (rec.get('agent_hash') or '').strip()
|
||||
if candidate_hash:
|
||||
payload: Dict[str, Any] = {
|
||||
'agent_guid': normalized_guid,
|
||||
'agent_hash': candidate_hash,
|
||||
'source': 'memory',
|
||||
}
|
||||
hostname = (rec.get('hostname') or '').strip()
|
||||
if hostname:
|
||||
payload['hostname'] = hostname
|
||||
if aid:
|
||||
payload['agent_id'] = aid
|
||||
return payload
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
conn = None
|
||||
try:
|
||||
conn = _db_conn()
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
'SELECT hostname, agent_hash, details FROM device_details WHERE guid = ?',
|
||||
(normalized_guid,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
return None
|
||||
hostname, agent_hash, details_json = row
|
||||
try:
|
||||
details = json.loads(details_json or '{}')
|
||||
except Exception:
|
||||
details = {}
|
||||
summary = details.get('summary') or {}
|
||||
agent_id = (summary.get('agent_id') or '').strip()
|
||||
normalized_hash = (agent_hash or summary.get('agent_hash') or '').strip()
|
||||
payload = {
|
||||
'agent_guid': normalized_guid,
|
||||
'agent_hash': normalized_hash,
|
||||
'hostname': hostname,
|
||||
'source': 'database',
|
||||
}
|
||||
if agent_id:
|
||||
payload['agent_id'] = agent_id
|
||||
return payload
|
||||
finally:
|
||||
if conn:
|
||||
try:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
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()
|
||||
if not agent_id or not agent_hash:
|
||||
return {'error': 'agent_id and agent_hash required'}, 400
|
||||
normalized_guid = _normalize_guid(agent_guid)
|
||||
if not agent_hash or (not agent_id and not normalized_guid):
|
||||
return {'error': 'agent_hash and agent_guid or agent_id required'}, 400
|
||||
|
||||
conn = None
|
||||
hostname = None
|
||||
resolved_agent_id = agent_id
|
||||
response_payload: Optional[Dict[str, Any]] = None
|
||||
status_code = 200
|
||||
try:
|
||||
conn = _db_conn()
|
||||
cur = conn.cursor()
|
||||
rows = _device_rows_for_agent(cur, agent_id)
|
||||
target = None
|
||||
for row in rows:
|
||||
if row.get('matched'):
|
||||
target = row
|
||||
break
|
||||
if not target:
|
||||
updated_via_guid = False
|
||||
if normalized_guid:
|
||||
cur.execute(
|
||||
'SELECT hostname, details FROM device_details WHERE guid = ?',
|
||||
(normalized_guid,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
updated_via_guid = True
|
||||
hostname = row[0]
|
||||
try:
|
||||
details = json.loads(row[1] or '{}')
|
||||
except Exception:
|
||||
details = {}
|
||||
summary = details.setdefault('summary', {})
|
||||
if not resolved_agent_id:
|
||||
resolved_agent_id = (summary.get('agent_id') or '').strip()
|
||||
if resolved_agent_id:
|
||||
summary['agent_id'] = resolved_agent_id
|
||||
summary['agent_hash'] = agent_hash
|
||||
summary['agent_guid'] = normalized_guid
|
||||
cur.execute(
|
||||
'UPDATE device_details SET agent_hash=?, details=? WHERE hostname=?',
|
||||
(agent_hash, json.dumps(details), hostname),
|
||||
)
|
||||
conn.commit()
|
||||
elif not agent_id:
|
||||
response_payload = {
|
||||
'status': 'ignored',
|
||||
'agent_guid': normalized_guid,
|
||||
'agent_hash': agent_hash,
|
||||
}
|
||||
|
||||
if response_payload is None and not updated_via_guid:
|
||||
target = None
|
||||
rows = _device_rows_for_agent(cur, resolved_agent_id)
|
||||
for row in rows:
|
||||
if row.get('matched'):
|
||||
target = row
|
||||
break
|
||||
if not target and rows:
|
||||
target = rows[0]
|
||||
if not target:
|
||||
response_payload = {
|
||||
'status': 'ignored',
|
||||
'agent_id': resolved_agent_id,
|
||||
'agent_hash': agent_hash,
|
||||
}
|
||||
else:
|
||||
hostname = target.get('hostname')
|
||||
details = target.get('details') or {}
|
||||
summary = details.setdefault('summary', {})
|
||||
summary['agent_hash'] = agent_hash
|
||||
if normalized_guid:
|
||||
summary['agent_guid'] = normalized_guid
|
||||
if resolved_agent_id:
|
||||
summary['agent_id'] = resolved_agent_id
|
||||
cur.execute(
|
||||
'UPDATE device_details SET agent_hash=?, details=? WHERE hostname=?',
|
||||
(agent_hash, json.dumps(details), hostname),
|
||||
)
|
||||
conn.commit()
|
||||
if not normalized_guid:
|
||||
normalized_guid = _normalize_guid(target.get('guid')) if target.get('guid') else ''
|
||||
if not normalized_guid:
|
||||
normalized_guid = _normalize_guid((summary.get('agent_guid') or '')) if summary else ''
|
||||
elif response_payload is None and normalized_guid and not hostname:
|
||||
# GUID provided and update attempted but hostname not resolved
|
||||
response_payload = {
|
||||
'status': 'ignored',
|
||||
'agent_id': agent_id,
|
||||
'agent_guid': normalized_guid,
|
||||
'agent_hash': agent_hash,
|
||||
}
|
||||
else:
|
||||
hostname = target.get('hostname')
|
||||
details = target.get('details') or {}
|
||||
summary = details.setdefault('summary', {})
|
||||
summary['agent_hash'] = agent_hash
|
||||
cur.execute(
|
||||
'UPDATE device_details SET agent_hash=?, details=? WHERE hostname=?',
|
||||
(agent_hash, json.dumps(details), hostname),
|
||||
)
|
||||
conn.commit()
|
||||
except Exception as exc:
|
||||
if conn:
|
||||
try:
|
||||
@@ -536,36 +666,59 @@ def _apply_agent_hash_update(agent_id: str, agent_hash: str) -> Tuple[Dict[str,
|
||||
return response_payload, status_code
|
||||
|
||||
normalized_hash = agent_hash
|
||||
if agent_id in registered_agents:
|
||||
registered_agents[agent_id]['agent_hash'] = normalized_hash
|
||||
try:
|
||||
for aid, rec in registered_agents.items():
|
||||
if rec.get('hostname') and hostname and rec['hostname'] == hostname:
|
||||
rec['agent_hash'] = normalized_hash
|
||||
except Exception:
|
||||
pass
|
||||
if normalized_guid:
|
||||
try:
|
||||
for aid, rec in registered_agents.items():
|
||||
guid_candidate = _normalize_guid(rec.get('agent_guid'))
|
||||
if guid_candidate == normalized_guid or (resolved_agent_id and aid == resolved_agent_id):
|
||||
rec['agent_hash'] = normalized_hash
|
||||
rec['agent_guid'] = normalized_guid
|
||||
if not resolved_agent_id:
|
||||
resolved_agent_id = aid
|
||||
except Exception:
|
||||
pass
|
||||
if resolved_agent_id and resolved_agent_id in registered_agents:
|
||||
registered_agents[resolved_agent_id]['agent_hash'] = normalized_hash
|
||||
if normalized_guid:
|
||||
registered_agents[resolved_agent_id]['agent_guid'] = normalized_guid
|
||||
if hostname:
|
||||
try:
|
||||
for aid, rec in registered_agents.items():
|
||||
if rec.get('hostname') and rec['hostname'] == hostname:
|
||||
rec['agent_hash'] = normalized_hash
|
||||
if normalized_guid:
|
||||
rec['agent_guid'] = normalized_guid
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
payload: Dict[str, Any] = {
|
||||
'status': 'ok',
|
||||
'agent_id': agent_id,
|
||||
'agent_hash': agent_hash,
|
||||
}
|
||||
if resolved_agent_id:
|
||||
payload['agent_id'] = resolved_agent_id
|
||||
if hostname:
|
||||
payload['hostname'] = hostname
|
||||
if normalized_guid:
|
||||
payload['agent_guid'] = normalized_guid
|
||||
return payload, 200
|
||||
|
||||
|
||||
@app.route("/api/agent/hash", methods=["GET", "POST"])
|
||||
def api_agent_hash():
|
||||
if request.method == 'GET':
|
||||
agent_guid = _normalize_guid(request.args.get('agent_guid'))
|
||||
agent_id = (request.args.get('agent_id') or request.args.get('id') or '').strip()
|
||||
if not agent_id:
|
||||
if not agent_guid and not agent_id:
|
||||
data = request.get_json(silent=True) or {}
|
||||
agent_id = (data.get('agent_id') or '').strip()
|
||||
if not agent_id:
|
||||
return jsonify({'error': 'agent_id required'}), 400
|
||||
agent_guid = _normalize_guid(data.get('agent_guid')) if data else agent_guid
|
||||
agent_id = (data.get('agent_id') or '').strip() if data else agent_id
|
||||
try:
|
||||
record = _lookup_agent_hash_record(agent_id)
|
||||
record = None
|
||||
if agent_guid:
|
||||
record = _lookup_agent_hash_by_guid(agent_guid)
|
||||
if not record and agent_id:
|
||||
record = _lookup_agent_hash_record(agent_id)
|
||||
except Exception as exc:
|
||||
_write_service_log('server', f'/api/agent/hash lookup error: {exc}')
|
||||
return jsonify({'error': 'internal error'}), 500
|
||||
@@ -576,7 +729,8 @@ def api_agent_hash():
|
||||
data = request.get_json(silent=True) or {}
|
||||
agent_id = (data.get('agent_id') or '').strip()
|
||||
agent_hash = (data.get('agent_hash') or '').strip()
|
||||
payload, status = _apply_agent_hash_update(agent_id, agent_hash)
|
||||
agent_guid = _normalize_guid(data.get('agent_guid')) if data else None
|
||||
payload, status = _apply_agent_hash_update(agent_id, agent_hash, agent_guid)
|
||||
return jsonify(payload), status
|
||||
|
||||
|
||||
@@ -747,6 +901,7 @@ def api_login():
|
||||
now = _now_ts()
|
||||
cur.execute("UPDATE users SET last_login=?, updated_at=? WHERE id=?", (now, now, row[0]))
|
||||
conn.commit()
|
||||
conn.commit()
|
||||
conn.close()
|
||||
# set session cookie
|
||||
session['username'] = row[1]
|
||||
@@ -2209,7 +2364,7 @@ def init_db():
|
||||
|
||||
# Device details table
|
||||
cur.execute(
|
||||
"CREATE TABLE IF NOT EXISTS device_details (hostname TEXT PRIMARY KEY, description TEXT, details TEXT, created_at INTEGER, agent_hash TEXT)"
|
||||
"CREATE TABLE IF NOT EXISTS device_details (hostname TEXT PRIMARY KEY, description TEXT, details TEXT, created_at INTEGER, agent_hash TEXT, guid TEXT)"
|
||||
)
|
||||
# Backfill missing created_at column on existing installs
|
||||
try:
|
||||
@@ -2219,6 +2374,8 @@ def init_db():
|
||||
cur.execute("ALTER TABLE device_details ADD COLUMN created_at INTEGER")
|
||||
if 'agent_hash' not in cols:
|
||||
cur.execute("ALTER TABLE device_details ADD COLUMN agent_hash TEXT")
|
||||
if 'guid' not in cols:
|
||||
cur.execute("ALTER TABLE device_details ADD COLUMN guid TEXT")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -2980,7 +3137,7 @@ def _persist_last_seen(hostname: str, last_seen: int, agent_id: str = None):
|
||||
conn = _db_conn()
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"SELECT details, description, created_at FROM device_details WHERE hostname = ?",
|
||||
"SELECT details, description, created_at, guid FROM device_details WHERE hostname = ?",
|
||||
(hostname,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
@@ -3007,6 +3164,12 @@ def _persist_last_seen(hostname: str, last_seen: int, agent_id: str = None):
|
||||
summary["agent_id"] = str(agent_id)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
existing_guid = (row[3] or "").strip() if row and len(row) > 3 else ""
|
||||
except Exception:
|
||||
existing_guid = ""
|
||||
if existing_guid and not summary.get("agent_guid"):
|
||||
summary["agent_guid"] = _normalize_guid(existing_guid)
|
||||
details["summary"] = summary
|
||||
|
||||
now = int(time.time())
|
||||
@@ -3042,8 +3205,9 @@ def load_agents_from_db():
|
||||
try:
|
||||
conn = _db_conn()
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT hostname, details, agent_hash FROM device_details")
|
||||
for hostname, details_json, agent_hash in cur.fetchall():
|
||||
cur.execute("SELECT hostname, details, agent_hash, guid FROM device_details")
|
||||
rows = cur.fetchall()
|
||||
for hostname, details_json, agent_hash, guid in rows:
|
||||
try:
|
||||
details = json.loads(details_json or "{}")
|
||||
except Exception:
|
||||
@@ -3055,6 +3219,16 @@ def load_agents_from_db():
|
||||
stored_hash = (agent_hash or summary.get("agent_hash") or "").strip()
|
||||
except Exception:
|
||||
stored_hash = summary.get("agent_hash") or ""
|
||||
agent_guid = (summary.get("agent_guid") or guid or "").strip()
|
||||
if guid and not summary.get("agent_guid"):
|
||||
summary["agent_guid"] = _normalize_guid(guid)
|
||||
try:
|
||||
cur.execute(
|
||||
"UPDATE device_details SET details=? WHERE hostname=?",
|
||||
(json.dumps(details), hostname),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
registered_agents[agent_id] = {
|
||||
"agent_id": agent_id,
|
||||
"hostname": summary.get("hostname") or hostname,
|
||||
@@ -3067,6 +3241,8 @@ def load_agents_from_db():
|
||||
}
|
||||
if stored_hash:
|
||||
registered_agents[agent_id]["agent_hash"] = stored_hash
|
||||
if agent_guid:
|
||||
registered_agents[agent_id]["agent_guid"] = _normalize_guid(agent_guid)
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f"[WARN] Failed to load agents from DB: {e}")
|
||||
@@ -3100,13 +3276,13 @@ def _device_rows_for_agent(cur, agent_id: str) -> List[Dict[str, Any]]:
|
||||
return results
|
||||
try:
|
||||
cur.execute(
|
||||
"SELECT hostname, agent_hash, details FROM device_details WHERE LOWER(hostname) = ?",
|
||||
"SELECT hostname, agent_hash, details, guid FROM device_details WHERE LOWER(hostname) = ?",
|
||||
(base_host.lower(),),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
except Exception:
|
||||
return results
|
||||
for hostname, agent_hash, details_json in rows or []:
|
||||
for hostname, agent_hash, details_json, guid in rows or []:
|
||||
try:
|
||||
details = json.loads(details_json or "{}")
|
||||
except Exception:
|
||||
@@ -3121,10 +3297,121 @@ def _device_rows_for_agent(cur, agent_id: str) -> List[Dict[str, Any]]:
|
||||
"details": details,
|
||||
"summary_agent_id": summary_agent,
|
||||
"matched": matched,
|
||||
"guid": (guid or "").strip(),
|
||||
}
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def _normalize_guid(value: Optional[str]) -> str:
|
||||
candidate = (value or "").strip()
|
||||
if not candidate:
|
||||
return ""
|
||||
try:
|
||||
return str(uuid.UUID(candidate))
|
||||
except Exception:
|
||||
return candidate
|
||||
|
||||
|
||||
def _ensure_agent_guid_for_hostname(cur, hostname: str, agent_id: Optional[str] = None) -> Optional[str]:
|
||||
normalized_host = (hostname or "").strip()
|
||||
if not normalized_host:
|
||||
return None
|
||||
cur.execute(
|
||||
"SELECT hostname, guid, details FROM device_details WHERE LOWER(hostname) = ?",
|
||||
(normalized_host.lower(),),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
actual_host, existing_guid, details_json = row
|
||||
else:
|
||||
actual_host, existing_guid, details_json = normalized_host, "", "{}"
|
||||
try:
|
||||
details = json.loads(details_json or "{}")
|
||||
except Exception:
|
||||
details = {}
|
||||
summary = details.setdefault("summary", {})
|
||||
if agent_id and not summary.get("agent_id"):
|
||||
try:
|
||||
summary["agent_id"] = str(agent_id)
|
||||
except Exception:
|
||||
summary["agent_id"] = agent_id
|
||||
if actual_host and not summary.get("hostname"):
|
||||
summary["hostname"] = actual_host
|
||||
|
||||
if existing_guid:
|
||||
normalized = _normalize_guid(existing_guid)
|
||||
summary.setdefault("agent_guid", normalized)
|
||||
cur.execute(
|
||||
"UPDATE device_details SET guid=?, details=? WHERE hostname=?",
|
||||
(normalized, json.dumps(details), actual_host),
|
||||
)
|
||||
return summary.get("agent_guid") or normalized
|
||||
|
||||
new_guid = str(uuid.uuid4())
|
||||
summary["agent_guid"] = new_guid
|
||||
now = int(time.time())
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO device_details(hostname, description, details, created_at, guid)
|
||||
VALUES (?, COALESCE((SELECT description FROM device_details WHERE hostname = ?), ''), ?, COALESCE((SELECT created_at FROM device_details WHERE hostname = ?), ?), ?)
|
||||
ON CONFLICT(hostname) DO UPDATE SET
|
||||
guid=excluded.guid,
|
||||
details=excluded.details,
|
||||
created_at=COALESCE(device_details.created_at, excluded.created_at),
|
||||
description=COALESCE(device_details.description, excluded.description)
|
||||
""",
|
||||
(actual_host, actual_host, json.dumps(details), actual_host, now, new_guid),
|
||||
)
|
||||
return new_guid
|
||||
|
||||
|
||||
def _ensure_agent_guid(agent_id: str, hostname: Optional[str] = None) -> Optional[str]:
|
||||
agent_id = (agent_id or "").strip()
|
||||
normalized_host = (hostname or "").strip()
|
||||
conn = None
|
||||
try:
|
||||
conn = _db_conn()
|
||||
cur = conn.cursor()
|
||||
rows = _device_rows_for_agent(cur, agent_id) if agent_id else []
|
||||
for row in rows:
|
||||
candidate = _normalize_guid(row.get("guid"))
|
||||
if candidate:
|
||||
summary = row.get("details", {}).setdefault("summary", {})
|
||||
summary.setdefault("agent_guid", candidate)
|
||||
cur.execute(
|
||||
"UPDATE device_details SET guid=?, details=? WHERE hostname=?",
|
||||
(candidate, json.dumps(row.get("details", {})), row.get("hostname")),
|
||||
)
|
||||
conn.commit()
|
||||
return candidate
|
||||
|
||||
target_host = normalized_host
|
||||
if not target_host:
|
||||
for row in rows:
|
||||
if row.get("matched"):
|
||||
target_host = row.get("hostname")
|
||||
break
|
||||
if not target_host and rows:
|
||||
target_host = rows[0].get("hostname")
|
||||
if not target_host and agent_id and agent_id in registered_agents:
|
||||
target_host = registered_agents[agent_id].get("hostname")
|
||||
if not target_host:
|
||||
return None
|
||||
|
||||
guid = _ensure_agent_guid_for_hostname(cur, target_host, agent_id)
|
||||
conn.commit()
|
||||
return guid
|
||||
except Exception as exc:
|
||||
_write_service_log('server', f'ensure_agent_guid failure for {agent_id or hostname}: {exc}')
|
||||
return None
|
||||
finally:
|
||||
if conn:
|
||||
try:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@app.route("/api/agents")
|
||||
def get_agents():
|
||||
"""Return agents with collector activity indicator."""
|
||||
@@ -3188,6 +3475,11 @@ def save_agent_details():
|
||||
agent_hash = agent_hash.strip() or None
|
||||
else:
|
||||
agent_hash = None
|
||||
agent_guid = data.get("agent_guid")
|
||||
if isinstance(agent_guid, str):
|
||||
agent_guid = agent_guid.strip() or None
|
||||
else:
|
||||
agent_guid = None
|
||||
if not hostname and isinstance(details, dict):
|
||||
hostname = (details.get("summary") or {}).get("hostname")
|
||||
if not hostname or not isinstance(details, dict):
|
||||
@@ -3197,13 +3489,14 @@ def save_agent_details():
|
||||
cur = conn.cursor()
|
||||
# Load existing row to preserve description and created_at and merge fields
|
||||
cur.execute(
|
||||
"SELECT details, description, created_at FROM device_details WHERE hostname = ?",
|
||||
"SELECT details, description, created_at, guid FROM device_details WHERE hostname = ?",
|
||||
(hostname,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
prev_details = {}
|
||||
description = ""
|
||||
created_at = 0
|
||||
existing_guid = None
|
||||
if row:
|
||||
try:
|
||||
prev_details = json.loads(row[0] or '{}')
|
||||
@@ -3214,6 +3507,12 @@ def save_agent_details():
|
||||
created_at = int(row[2] or 0)
|
||||
except Exception:
|
||||
created_at = 0
|
||||
try:
|
||||
existing_guid = (row[3] or "").strip()
|
||||
except Exception:
|
||||
existing_guid = None
|
||||
else:
|
||||
existing_guid = None
|
||||
|
||||
# Ensure summary exists and attach hostname/agent_id if missing
|
||||
incoming_summary = details.setdefault("summary", {})
|
||||
@@ -3229,6 +3528,10 @@ def save_agent_details():
|
||||
incoming_summary["agent_hash"] = agent_hash
|
||||
except Exception:
|
||||
pass
|
||||
effective_guid = agent_guid or existing_guid
|
||||
normalized_effective_guid = _normalize_guid(effective_guid) if effective_guid else None
|
||||
if normalized_effective_guid:
|
||||
incoming_summary["agent_guid"] = normalized_effective_guid
|
||||
|
||||
# Preserve last_seen if incoming omitted it
|
||||
if not incoming_summary.get("last_seen"):
|
||||
@@ -3284,15 +3587,23 @@ def save_agent_details():
|
||||
# Upsert row without destroying created_at; keep previous created_at if exists
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO device_details(hostname, description, details, created_at, agent_hash)
|
||||
VALUES (?,?,?,?,?)
|
||||
INSERT INTO device_details(hostname, description, details, created_at, agent_hash, guid)
|
||||
VALUES (?,?,?,?,?,?)
|
||||
ON CONFLICT(hostname) DO UPDATE SET
|
||||
description=excluded.description,
|
||||
details=excluded.details,
|
||||
created_at=COALESCE(device_details.created_at, excluded.created_at),
|
||||
agent_hash=COALESCE(NULLIF(excluded.agent_hash, ''), device_details.agent_hash)
|
||||
agent_hash=COALESCE(NULLIF(excluded.agent_hash, ''), device_details.agent_hash),
|
||||
guid=COALESCE(NULLIF(excluded.guid, ''), device_details.guid)
|
||||
""",
|
||||
(hostname, description, json.dumps(merged), created_at, agent_hash or None),
|
||||
(
|
||||
hostname,
|
||||
description,
|
||||
json.dumps(merged),
|
||||
created_at,
|
||||
agent_hash or None,
|
||||
(normalized_effective_guid or None),
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -3302,16 +3613,20 @@ def save_agent_details():
|
||||
normalized_hash = (agent_hash or (merged.get("summary") or {}).get("agent_hash") or "").strip()
|
||||
except Exception:
|
||||
normalized_hash = agent_hash
|
||||
if normalized_hash:
|
||||
if agent_id and agent_id in registered_agents:
|
||||
if agent_id and agent_id in registered_agents:
|
||||
if normalized_hash:
|
||||
registered_agents[agent_id]["agent_hash"] = normalized_hash
|
||||
# Also update any entries keyed by hostname (duplicate agents)
|
||||
try:
|
||||
for aid, rec in registered_agents.items():
|
||||
if rec.get("hostname") == hostname and normalized_hash:
|
||||
rec["agent_hash"] = normalized_hash
|
||||
except Exception:
|
||||
pass
|
||||
if normalized_effective_guid:
|
||||
registered_agents[agent_id]["agent_guid"] = normalized_effective_guid
|
||||
# Also update any entries keyed by hostname (duplicate agents)
|
||||
try:
|
||||
for aid, rec in registered_agents.items():
|
||||
if rec.get("hostname") == hostname and normalized_hash:
|
||||
rec["agent_hash"] = normalized_hash
|
||||
if rec.get("hostname") == hostname and normalized_effective_guid:
|
||||
rec["agent_guid"] = normalized_effective_guid
|
||||
except Exception:
|
||||
pass
|
||||
return jsonify({"status": "ok"})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
@@ -3323,7 +3638,7 @@ def get_device_details(hostname: str):
|
||||
conn = _db_conn()
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"SELECT details, description, created_at, agent_hash FROM device_details WHERE hostname = ?",
|
||||
"SELECT details, description, created_at, agent_hash, guid FROM device_details WHERE hostname = ?",
|
||||
(hostname,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
@@ -3352,6 +3667,15 @@ def get_device_details(hostname: str):
|
||||
details['summary']['agent_hash'] = agent_hash
|
||||
except Exception:
|
||||
pass
|
||||
if len(row) > 4:
|
||||
agent_guid = (row[4] or "").strip()
|
||||
if agent_guid:
|
||||
try:
|
||||
details.setdefault('summary', {})
|
||||
if not details['summary'].get('agent_guid'):
|
||||
details['summary']['agent_guid'] = _normalize_guid(agent_guid)
|
||||
except Exception:
|
||||
pass
|
||||
return jsonify(details)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -3915,6 +4239,7 @@ def api_agent_checkin():
|
||||
username = raw_username or DEFAULT_SERVICE_ACCOUNT
|
||||
if username in LEGACY_SERVICE_ACCOUNTS:
|
||||
username = DEFAULT_SERVICE_ACCOUNT
|
||||
hostname = (payload.get('hostname') or '').strip()
|
||||
try:
|
||||
conn = _db_conn()
|
||||
row = _service_acct_get(conn, agent_id)
|
||||
@@ -3947,10 +4272,22 @@ def api_agent_checkin():
|
||||
}
|
||||
conn.close()
|
||||
_ansible_log_server(f"[checkin] return creds agent_id={agent_id} user={out['username']}")
|
||||
try:
|
||||
if hostname:
|
||||
_persist_last_seen(hostname, int(time.time()), agent_id)
|
||||
except Exception:
|
||||
pass
|
||||
agent_guid = _ensure_agent_guid(agent_id, hostname or None)
|
||||
if agent_guid and agent_id:
|
||||
rec = registered_agents.setdefault(agent_id, {})
|
||||
rec['agent_guid'] = agent_guid
|
||||
else:
|
||||
agent_guid = agent_guid or ''
|
||||
return jsonify({
|
||||
'username': out['username'],
|
||||
'password': out['password'],
|
||||
'policy': { 'force_rotation_minutes': 43200 }
|
||||
'policy': { 'force_rotation_minutes': 43200 },
|
||||
'agent_guid': agent_guid or None,
|
||||
})
|
||||
except Exception as e:
|
||||
_ansible_log_server(f"[checkin] error agent_id={agent_id} err={e}")
|
||||
|
||||
55
Update.ps1
55
Update.ps1
@@ -119,6 +119,7 @@ function Get-AgentServiceId {
|
||||
$settingsDir = Join-Path $AgentRoot 'Settings'
|
||||
$candidates = @(
|
||||
(Join-Path $settingsDir 'agent_settings_svc.json')
|
||||
(Join-Path $settingsDir 'agent_settings_user.json')
|
||||
(Join-Path $settingsDir 'agent_settings.json')
|
||||
)
|
||||
|
||||
@@ -137,6 +138,29 @@ function Get-AgentServiceId {
|
||||
return ''
|
||||
}
|
||||
|
||||
function Get-AgentGuid {
|
||||
param(
|
||||
[string]$AgentRoot
|
||||
)
|
||||
|
||||
$candidates = @()
|
||||
if (-not $AgentRoot) { $AgentRoot = $scriptDir }
|
||||
if ($AgentRoot) { $candidates += (Join-Path $AgentRoot 'agent_GUID') }
|
||||
$defaultPath = Join-Path $scriptDir 'Agent\Borealis\agent_GUID'
|
||||
if ($defaultPath -and ($candidates -notcontains $defaultPath)) { $candidates += $defaultPath }
|
||||
|
||||
foreach ($path in ($candidates | Select-Object -Unique)) {
|
||||
try {
|
||||
if (Test-Path $path -PathType Leaf) {
|
||||
$value = (Get-Content -Path $path -Raw -ErrorAction Stop)
|
||||
if ($value) { return $value.Trim() }
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
function Get-RepositoryCommitHash {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
@@ -310,16 +334,21 @@ function Submit-AgentHash {
|
||||
[string]$AgentId,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$AgentHash
|
||||
[string]$AgentHash,
|
||||
|
||||
[string]$AgentGuid
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($ServerBaseUrl) -or [string]::IsNullOrWhiteSpace($AgentId) -or [string]::IsNullOrWhiteSpace($AgentHash)) {
|
||||
if ([string]::IsNullOrWhiteSpace($ServerBaseUrl) -or [string]::IsNullOrWhiteSpace($AgentHash)) {
|
||||
return
|
||||
}
|
||||
|
||||
$base = $ServerBaseUrl.TrimEnd('/')
|
||||
$uri = "$base/api/agent/hash"
|
||||
$payload = @{ agent_id = $AgentId; agent_hash = $AgentHash } | ConvertTo-Json -Depth 3
|
||||
$payloadBody = @{ agent_hash = $AgentHash }
|
||||
if (-not [string]::IsNullOrWhiteSpace($AgentId)) { $payloadBody.agent_id = $AgentId }
|
||||
if (-not [string]::IsNullOrWhiteSpace($AgentGuid)) { $payloadBody.agent_guid = $AgentGuid }
|
||||
$payload = $payloadBody | ConvertTo-Json -Depth 3
|
||||
$headers = @{ 'User-Agent' = 'borealis-agent-updater' }
|
||||
|
||||
$resp = Invoke-WebRequest -Uri $uri -Method Post -Headers $headers -Body $payload -ContentType 'application/json' -UseBasicParsing -ErrorAction Stop
|
||||
@@ -338,6 +367,7 @@ function Sync-AgentHashRecord {
|
||||
[string]$AgentHash,
|
||||
[string]$ServerBaseUrl,
|
||||
[string]$AgentId,
|
||||
[string]$AgentGuid,
|
||||
[string]$BranchName = 'main'
|
||||
)
|
||||
|
||||
@@ -354,13 +384,13 @@ function Sync-AgentHashRecord {
|
||||
|
||||
Write-Host ("Submitting agent hash to server: {0}" -f $AgentHash)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($AgentId)) {
|
||||
Write-Host "Agent ID unavailable; skipping agent hash submission." -ForegroundColor DarkYellow
|
||||
if ([string]::IsNullOrWhiteSpace($AgentId) -and [string]::IsNullOrWhiteSpace($AgentGuid)) {
|
||||
Write-Host "Agent identifier unavailable; skipping agent hash submission." -ForegroundColor DarkYellow
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
$submitResult = Submit-AgentHash -ServerBaseUrl $ServerBaseUrl -AgentId $AgentId -AgentHash $AgentHash
|
||||
$submitResult = Submit-AgentHash -ServerBaseUrl $ServerBaseUrl -AgentId $AgentId -AgentHash $AgentHash -AgentGuid $AgentGuid
|
||||
if ($submitResult -and ($submitResult.status -eq 'ok')) {
|
||||
Write-Host "Server agent_hash database record updated successfully."
|
||||
} elseif ($submitResult -and ($submitResult.status -eq 'ignored')) {
|
||||
@@ -441,6 +471,15 @@ function Invoke-BorealisAgentUpdate {
|
||||
}
|
||||
}
|
||||
|
||||
$agentGuid = Get-AgentGuid -AgentRoot $agentRoot
|
||||
if ($agentGuid) {
|
||||
Write-Host ("Agent GUID: {0}" -f $agentGuid)
|
||||
} else {
|
||||
Write-Host "Warning: No agent GUID detected - Please deploy the agent, associating it with a Borealis server then try running the updater script again." -ForegroundColor Yellow
|
||||
Write-Host "⚠️ Borealis update aborted."
|
||||
return
|
||||
}
|
||||
|
||||
$currentHash = Get-RepositoryCommitHash -ProjectRoot $scriptDir -AgentRoot $agentRoot
|
||||
$serverBaseUrl = Get-BorealisServerUrl -AgentRoot $agentRoot
|
||||
$agentId = Get-AgentServiceId -AgentRoot $agentRoot
|
||||
@@ -485,7 +524,7 @@ function Invoke-BorealisAgentUpdate {
|
||||
return
|
||||
} elseif (-not $needsUpdate) {
|
||||
Write-Host "Local agent files already match the server repository hash." -ForegroundColor Green
|
||||
Sync-AgentHashRecord -ProjectRoot $scriptDir -AgentRoot $agentRoot -AgentHash $serverHash -ServerBaseUrl $serverBaseUrl -AgentId $agentId -BranchName $serverBranch
|
||||
Sync-AgentHashRecord -ProjectRoot $scriptDir -AgentRoot $agentRoot -AgentHash $serverHash -ServerBaseUrl $serverBaseUrl -AgentId $agentId -AgentGuid $agentGuid -BranchName $serverBranch
|
||||
Write-Host "✅ Borealis - Automation Platform Already Up-to-Date"
|
||||
return
|
||||
} else {
|
||||
@@ -555,7 +594,7 @@ function Invoke-BorealisAgentUpdate {
|
||||
}
|
||||
|
||||
if ($newHash) {
|
||||
Sync-AgentHashRecord -ProjectRoot $scriptDir -AgentRoot $agentRoot -AgentHash $newHash -ServerBaseUrl $serverBaseUrl -AgentId $agentId -BranchName $serverBranch
|
||||
Sync-AgentHashRecord -ProjectRoot $scriptDir -AgentRoot $agentRoot -AgentHash $newHash -ServerBaseUrl $serverBaseUrl -AgentId $agentId -AgentGuid $agentGuid -BranchName $serverBranch
|
||||
} else {
|
||||
Write-Host "Unable to determine repository hash for submission; server hash not updated." -ForegroundColor DarkYellow
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user