Introduce agent GUID enrollment

This commit is contained in:
2025-10-08 18:51:15 -06:00
parent f4b425f609
commit 3092f528bf
3 changed files with 487 additions and 70 deletions

View File

@@ -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())

View File

@@ -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}")

View File

@@ -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
}