mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-26 15:41:58 -06:00
Restore device summary fields and assembly data flow
This commit is contained in:
@@ -7,6 +7,8 @@ from dataclasses import dataclass
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any, Dict, List, Mapping, Optional, Sequence
|
from typing import Any, Dict, List, Mapping, Optional, Sequence
|
||||||
|
|
||||||
|
from Data.Engine.domain.device_auth import normalize_guid
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"DEVICE_TABLE_COLUMNS",
|
"DEVICE_TABLE_COLUMNS",
|
||||||
"DEVICE_TABLE",
|
"DEVICE_TABLE",
|
||||||
@@ -91,8 +93,13 @@ class DeviceSnapshot:
|
|||||||
operating_system: str
|
operating_system: str
|
||||||
uptime: int
|
uptime: int
|
||||||
agent_id: str
|
agent_id: str
|
||||||
|
ansible_ee_ver: str
|
||||||
connection_type: str
|
connection_type: str
|
||||||
connection_endpoint: str
|
connection_endpoint: str
|
||||||
|
ssl_key_fingerprint: str
|
||||||
|
token_version: int
|
||||||
|
status: str
|
||||||
|
key_added_at: str
|
||||||
details: Dict[str, Any]
|
details: Dict[str, Any]
|
||||||
summary: Dict[str, Any]
|
summary: Dict[str, Any]
|
||||||
|
|
||||||
@@ -121,8 +128,13 @@ class DeviceSnapshot:
|
|||||||
"operating_system": self.operating_system,
|
"operating_system": self.operating_system,
|
||||||
"uptime": self.uptime,
|
"uptime": self.uptime,
|
||||||
"agent_id": self.agent_id,
|
"agent_id": self.agent_id,
|
||||||
|
"ansible_ee_ver": self.ansible_ee_ver,
|
||||||
"connection_type": self.connection_type,
|
"connection_type": self.connection_type,
|
||||||
"connection_endpoint": self.connection_endpoint,
|
"connection_endpoint": self.connection_endpoint,
|
||||||
|
"ssl_key_fingerprint": self.ssl_key_fingerprint,
|
||||||
|
"token_version": self.token_version,
|
||||||
|
"status": self.status,
|
||||||
|
"key_added_at": self.key_added_at,
|
||||||
"details": self.details,
|
"details": self.details,
|
||||||
"summary": self.summary,
|
"summary": self.summary,
|
||||||
}
|
}
|
||||||
@@ -211,33 +223,16 @@ def row_to_device_dict(row: Sequence[Any], columns: Sequence[str]) -> Dict[str,
|
|||||||
|
|
||||||
|
|
||||||
def assemble_device_snapshot(record: Mapping[str, Any]) -> Dict[str, Any]:
|
def assemble_device_snapshot(record: Mapping[str, Any]) -> Dict[str, Any]:
|
||||||
summary = {
|
hostname = clean_device_str(record.get("hostname")) or ""
|
||||||
"hostname": record.get("hostname") or "",
|
description = clean_device_str(record.get("description")) or ""
|
||||||
"description": record.get("description") or "",
|
agent_hash = clean_device_str(record.get("agent_hash")) or ""
|
||||||
"device_type": record.get("device_type") or "",
|
raw_guid = clean_device_str(record.get("guid"))
|
||||||
"domain": record.get("domain") or "",
|
normalized_guid = normalize_guid(raw_guid)
|
||||||
"external_ip": record.get("external_ip") or "",
|
|
||||||
"internal_ip": record.get("internal_ip") or "",
|
|
||||||
"last_reboot": record.get("last_reboot") or "",
|
|
||||||
"last_seen": record.get("last_seen") or 0,
|
|
||||||
"last_user": record.get("last_user") or "",
|
|
||||||
"operating_system": record.get("operating_system") or "",
|
|
||||||
"uptime": record.get("uptime") or 0,
|
|
||||||
"agent_id": record.get("agent_id") or "",
|
|
||||||
"agent_hash": record.get("agent_hash") or "",
|
|
||||||
"agent_guid": record.get("guid") or record.get("agent_guid") or "",
|
|
||||||
"connection_type": record.get("connection_type") or "",
|
|
||||||
"connection_endpoint": record.get("connection_endpoint") or "",
|
|
||||||
"ssl_key_fingerprint": record.get("ssl_key_fingerprint") or "",
|
|
||||||
"status": record.get("status") or "",
|
|
||||||
"token_version": record.get("token_version") or 0,
|
|
||||||
"key_added_at": record.get("key_added_at") or "",
|
|
||||||
"created_at": record.get("created_at") or 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
created_ts = coerce_int(summary.get("created_at")) or 0
|
created_ts = coerce_int(record.get("created_at")) or 0
|
||||||
last_seen_ts = coerce_int(summary.get("last_seen")) or 0
|
last_seen_ts = coerce_int(record.get("last_seen")) or 0
|
||||||
uptime_val = coerce_int(summary.get("uptime")) or 0
|
uptime_val = coerce_int(record.get("uptime")) or 0
|
||||||
|
token_version = coerce_int(record.get("token_version")) or 0
|
||||||
|
|
||||||
parsed_lists = {
|
parsed_lists = {
|
||||||
key: _parse_device_json(record.get(key), default)
|
key: _parse_device_json(record.get(key), default)
|
||||||
@@ -245,20 +240,48 @@ def assemble_device_snapshot(record: Mapping[str, Any]) -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
cpu_obj = _parse_device_json(record.get("cpu"), DEVICE_JSON_OBJECT_FIELDS["cpu"])
|
cpu_obj = _parse_device_json(record.get("cpu"), DEVICE_JSON_OBJECT_FIELDS["cpu"])
|
||||||
|
|
||||||
|
summary: Dict[str, Any] = {
|
||||||
|
"hostname": hostname,
|
||||||
|
"description": description,
|
||||||
|
"agent_hash": agent_hash,
|
||||||
|
"agent_guid": normalized_guid or "",
|
||||||
|
"agent_id": clean_device_str(record.get("agent_id")) or "",
|
||||||
|
"device_type": clean_device_str(record.get("device_type")) or "",
|
||||||
|
"domain": clean_device_str(record.get("domain")) or "",
|
||||||
|
"external_ip": clean_device_str(record.get("external_ip")) or "",
|
||||||
|
"internal_ip": clean_device_str(record.get("internal_ip")) or "",
|
||||||
|
"last_reboot": clean_device_str(record.get("last_reboot")) or "",
|
||||||
|
"last_seen": last_seen_ts,
|
||||||
|
"last_user": clean_device_str(record.get("last_user")) or "",
|
||||||
|
"operating_system": clean_device_str(record.get("operating_system")) or "",
|
||||||
|
"uptime": uptime_val,
|
||||||
|
"uptime_sec": uptime_val,
|
||||||
|
"ansible_ee_ver": clean_device_str(record.get("ansible_ee_ver")) or "",
|
||||||
|
"connection_type": clean_device_str(record.get("connection_type")) or "",
|
||||||
|
"connection_endpoint": clean_device_str(record.get("connection_endpoint")) or "",
|
||||||
|
"ssl_key_fingerprint": clean_device_str(record.get("ssl_key_fingerprint")) or "",
|
||||||
|
"status": clean_device_str(record.get("status")) or "",
|
||||||
|
"token_version": token_version,
|
||||||
|
"key_added_at": clean_device_str(record.get("key_added_at")) or "",
|
||||||
|
"created_at": created_ts,
|
||||||
|
"created": ts_to_human(created_ts),
|
||||||
|
}
|
||||||
|
|
||||||
details = {
|
details = {
|
||||||
"memory": parsed_lists["memory"],
|
"memory": parsed_lists["memory"],
|
||||||
"network": parsed_lists["network"],
|
"network": parsed_lists["network"],
|
||||||
"software": parsed_lists["software"],
|
"software": parsed_lists["software"],
|
||||||
"storage": parsed_lists["storage"],
|
"storage": parsed_lists["storage"],
|
||||||
"cpu": cpu_obj,
|
"cpu": cpu_obj,
|
||||||
|
"summary": dict(summary),
|
||||||
}
|
}
|
||||||
|
|
||||||
payload: Dict[str, Any] = {
|
payload: Dict[str, Any] = {
|
||||||
"hostname": summary["hostname"],
|
"hostname": hostname,
|
||||||
"description": summary.get("description", ""),
|
"description": description,
|
||||||
"created_at": created_ts,
|
"created_at": created_ts,
|
||||||
"created_at_iso": ts_to_iso(created_ts),
|
"created_at_iso": ts_to_iso(created_ts),
|
||||||
"agent_hash": summary.get("agent_hash", ""),
|
"agent_hash": agent_hash,
|
||||||
"agent_guid": summary.get("agent_guid", ""),
|
"agent_guid": summary.get("agent_guid", ""),
|
||||||
"guid": summary.get("agent_guid", ""),
|
"guid": summary.get("agent_guid", ""),
|
||||||
"memory": parsed_lists["memory"],
|
"memory": parsed_lists["memory"],
|
||||||
@@ -277,8 +300,13 @@ def assemble_device_snapshot(record: Mapping[str, Any]) -> Dict[str, Any]:
|
|||||||
"operating_system": summary.get("operating_system", ""),
|
"operating_system": summary.get("operating_system", ""),
|
||||||
"uptime": uptime_val,
|
"uptime": uptime_val,
|
||||||
"agent_id": summary.get("agent_id", ""),
|
"agent_id": summary.get("agent_id", ""),
|
||||||
|
"ansible_ee_ver": summary.get("ansible_ee_ver", ""),
|
||||||
"connection_type": summary.get("connection_type", ""),
|
"connection_type": summary.get("connection_type", ""),
|
||||||
"connection_endpoint": summary.get("connection_endpoint", ""),
|
"connection_endpoint": summary.get("connection_endpoint", ""),
|
||||||
|
"ssl_key_fingerprint": summary.get("ssl_key_fingerprint", ""),
|
||||||
|
"token_version": summary.get("token_version", 0),
|
||||||
|
"status": summary.get("status", ""),
|
||||||
|
"key_added_at": summary.get("key_added_at", ""),
|
||||||
"details": details,
|
"details": details,
|
||||||
"summary": summary,
|
"summary": summary,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -278,28 +278,60 @@ class SQLiteDeviceInventoryRepository:
|
|||||||
for field in ("memory", "network", "software", "storage"):
|
for field in ("memory", "network", "software", "storage"):
|
||||||
payload[field] = serialize_device_json(details.get(field), [])
|
payload[field] = serialize_device_json(details.get(field), [])
|
||||||
payload["cpu"] = serialize_device_json(summary.get("cpu") or details.get("cpu"), {})
|
payload["cpu"] = serialize_device_json(summary.get("cpu") or details.get("cpu"), {})
|
||||||
payload["device_type"] = clean_device_str(summary.get("device_type") or summary.get("type"))
|
payload["device_type"] = clean_device_str(
|
||||||
payload["domain"] = clean_device_str(summary.get("domain"))
|
summary.get("device_type")
|
||||||
payload["external_ip"] = clean_device_str(summary.get("external_ip") or summary.get("public_ip"))
|
or summary.get("type")
|
||||||
payload["internal_ip"] = clean_device_str(summary.get("internal_ip") or summary.get("private_ip"))
|
or summary.get("device_class")
|
||||||
payload["last_reboot"] = clean_device_str(summary.get("last_reboot") or summary.get("last_boot"))
|
)
|
||||||
payload["last_seen"] = coerce_int(summary.get("last_seen"))
|
payload["domain"] = clean_device_str(
|
||||||
|
summary.get("domain") or summary.get("domain_name")
|
||||||
|
)
|
||||||
|
payload["external_ip"] = clean_device_str(
|
||||||
|
summary.get("external_ip") or summary.get("public_ip")
|
||||||
|
)
|
||||||
|
payload["internal_ip"] = clean_device_str(
|
||||||
|
summary.get("internal_ip") or summary.get("private_ip")
|
||||||
|
)
|
||||||
|
payload["last_reboot"] = clean_device_str(
|
||||||
|
summary.get("last_reboot") or summary.get("last_boot")
|
||||||
|
)
|
||||||
|
payload["last_seen"] = coerce_int(
|
||||||
|
summary.get("last_seen") or summary.get("last_seen_epoch")
|
||||||
|
)
|
||||||
payload["last_user"] = clean_device_str(
|
payload["last_user"] = clean_device_str(
|
||||||
summary.get("last_user")
|
summary.get("last_user")
|
||||||
or summary.get("last_user_name")
|
or summary.get("last_user_name")
|
||||||
or summary.get("logged_in_user")
|
or summary.get("logged_in_user")
|
||||||
|
or summary.get("username")
|
||||||
|
or summary.get("user")
|
||||||
)
|
)
|
||||||
payload["operating_system"] = clean_device_str(
|
payload["operating_system"] = clean_device_str(
|
||||||
summary.get("operating_system") or summary.get("os")
|
summary.get("operating_system")
|
||||||
|
or summary.get("agent_operating_system")
|
||||||
|
or summary.get("os")
|
||||||
)
|
)
|
||||||
payload["uptime"] = coerce_int(summary.get("uptime"))
|
uptime_value = (
|
||||||
|
summary.get("uptime_sec")
|
||||||
|
or summary.get("uptime_seconds")
|
||||||
|
or summary.get("uptime")
|
||||||
|
)
|
||||||
|
payload["uptime"] = coerce_int(uptime_value)
|
||||||
payload["agent_id"] = clean_device_str(summary.get("agent_id"))
|
payload["agent_id"] = clean_device_str(summary.get("agent_id"))
|
||||||
payload["ansible_ee_ver"] = clean_device_str(summary.get("ansible_ee_ver"))
|
payload["ansible_ee_ver"] = clean_device_str(summary.get("ansible_ee_ver"))
|
||||||
payload["connection_type"] = clean_device_str(summary.get("connection_type"))
|
payload["connection_type"] = clean_device_str(
|
||||||
payload["connection_endpoint"] = clean_device_str(
|
summary.get("connection_type") or summary.get("remote_type")
|
||||||
summary.get("connection_endpoint") or summary.get("endpoint")
|
)
|
||||||
|
payload["connection_endpoint"] = clean_device_str(
|
||||||
|
summary.get("connection_endpoint")
|
||||||
|
or summary.get("endpoint")
|
||||||
|
or summary.get("connection_address")
|
||||||
|
or summary.get("address")
|
||||||
|
or summary.get("external_ip")
|
||||||
|
or summary.get("internal_ip")
|
||||||
|
)
|
||||||
|
payload["ssl_key_fingerprint"] = clean_device_str(
|
||||||
|
summary.get("ssl_key_fingerprint")
|
||||||
)
|
)
|
||||||
payload["ssl_key_fingerprint"] = clean_device_str(summary.get("ssl_key_fingerprint"))
|
|
||||||
payload["token_version"] = coerce_int(summary.get("token_version")) or 0
|
payload["token_version"] = coerce_int(summary.get("token_version")) or 0
|
||||||
payload["status"] = clean_device_str(summary.get("status"))
|
payload["status"] = clean_device_str(summary.get("status"))
|
||||||
payload["key_added_at"] = clean_device_str(summary.get("key_added_at"))
|
payload["key_added_at"] = clean_device_str(summary.get("key_added_at"))
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from Data.Engine.repositories.sqlite.device_inventory_repository import (
|
|||||||
SQLiteDeviceInventoryRepository,
|
SQLiteDeviceInventoryRepository,
|
||||||
)
|
)
|
||||||
from Data.Engine.domain.device_auth import DeviceAuthContext, normalize_guid
|
from Data.Engine.domain.device_auth import DeviceAuthContext, normalize_guid
|
||||||
from Data.Engine.domain.devices import clean_device_str, coerce_int
|
from Data.Engine.domain.devices import clean_device_str, coerce_int, ts_to_human
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"DeviceInventoryService",
|
"DeviceInventoryService",
|
||||||
@@ -240,7 +240,7 @@ class DeviceInventoryService:
|
|||||||
summary["hostname"] = hostname
|
summary["hostname"] = hostname
|
||||||
|
|
||||||
if metrics:
|
if metrics:
|
||||||
last_user = metrics.get("last_user")
|
last_user = metrics.get("last_user") or metrics.get("username") or metrics.get("user")
|
||||||
if last_user:
|
if last_user:
|
||||||
cleaned_user = clean_device_str(last_user)
|
cleaned_user = clean_device_str(last_user)
|
||||||
if cleaned_user:
|
if cleaned_user:
|
||||||
@@ -422,6 +422,8 @@ class DeviceInventoryService:
|
|||||||
if created_at is None:
|
if created_at is None:
|
||||||
created_at = int(time.time())
|
created_at = int(time.time())
|
||||||
merged_summary["created_at"] = created_at
|
merged_summary["created_at"] = created_at
|
||||||
|
if not merged_summary.get("created"):
|
||||||
|
merged_summary["created"] = ts_to_human(created_at)
|
||||||
|
|
||||||
if fingerprint:
|
if fingerprint:
|
||||||
merged_summary["ssl_key_fingerprint"] = fingerprint
|
merged_summary["ssl_key_fingerprint"] = fingerprint
|
||||||
@@ -431,6 +433,14 @@ class DeviceInventoryService:
|
|||||||
merged_summary["token_version"] = 1
|
merged_summary["token_version"] = 1
|
||||||
if not merged_summary.get("status") and snapshot.get("summary", {}).get("status"):
|
if not merged_summary.get("status") and snapshot.get("summary", {}).get("status"):
|
||||||
merged_summary["status"] = snapshot.get("summary", {}).get("status")
|
merged_summary["status"] = snapshot.get("summary", {}).get("status")
|
||||||
|
uptime_val = merged_summary.get("uptime")
|
||||||
|
if merged_summary.get("uptime_sec") is None and uptime_val is not None:
|
||||||
|
coerced = coerce_int(uptime_val)
|
||||||
|
if coerced is not None:
|
||||||
|
merged_summary["uptime_sec"] = coerced
|
||||||
|
merged_summary.setdefault("uptime_seconds", coerced)
|
||||||
|
if merged_summary.get("uptime_seconds") is None and merged_summary.get("uptime_sec") is not None:
|
||||||
|
merged_summary["uptime_seconds"] = merged_summary.get("uptime_sec")
|
||||||
|
|
||||||
description = clean_device_str(merged_summary.get("description"))
|
description = clean_device_str(merged_summary.get("description"))
|
||||||
existing_description = snapshot.get("description") if snapshot else ""
|
existing_description = snapshot.get("description") if snapshot else ""
|
||||||
|
|||||||
@@ -255,10 +255,14 @@ def test_agent_details_persists_inventory(prepared_app, monkeypatch):
|
|||||||
"last_user": "BUNNY-LAB\\nicole.rappe",
|
"last_user": "BUNNY-LAB\\nicole.rappe",
|
||||||
"operating_system": "Windows 11",
|
"operating_system": "Windows 11",
|
||||||
"description": "Primary workstation",
|
"description": "Primary workstation",
|
||||||
|
"last_reboot": "2025-10-01 10:00:00",
|
||||||
|
"uptime": 3600,
|
||||||
},
|
},
|
||||||
"memory": [{"slot": "DIMM0", "capacity": 17179869184}],
|
"memory": [{"slot": "DIMM0", "capacity": 17179869184}],
|
||||||
"storage": [{"model": "NVMe", "size": 512}],
|
"storage": [{"model": "NVMe", "size": 512}],
|
||||||
"network": [{"adapter": "Ethernet", "ips": ["192.168.1.50"]}],
|
"network": [{"adapter": "Ethernet", "ips": ["192.168.1.50"]}],
|
||||||
|
"software": [{"name": "Borealis Agent", "version": "2.0"}],
|
||||||
|
"cpu": {"name": "Intel Core i7", "logical_cores": 8, "base_clock_ghz": 3.4},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,6 +295,26 @@ def test_agent_details_persists_inventory(prepared_app, monkeypatch):
|
|||||||
assert json.loads(storage_json)[0]["model"] == "NVMe"
|
assert json.loads(storage_json)[0]["model"] == "NVMe"
|
||||||
assert json.loads(network_json)[0]["ips"][0] == "192.168.1.50"
|
assert json.loads(network_json)[0]["ips"][0] == "192.168.1.50"
|
||||||
|
|
||||||
|
resp = client.get("/api/devices")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
listing = resp.get_json()
|
||||||
|
device = next((dev for dev in listing.get("devices", []) if dev["hostname"] == hostname), None)
|
||||||
|
assert device is not None
|
||||||
|
summary = device["summary"]
|
||||||
|
details = device["details"]
|
||||||
|
|
||||||
|
assert summary["device_type"] == "Laptop"
|
||||||
|
assert summary["last_user"] == "BUNNY-LAB\\nicole.rappe"
|
||||||
|
assert summary["created"]
|
||||||
|
assert summary.get("uptime_sec") == 3600
|
||||||
|
assert details["summary"]["device_type"] == "Laptop"
|
||||||
|
assert details["summary"]["last_reboot"] == "2025-10-01 10:00:00"
|
||||||
|
assert details["summary"]["created"] == summary["created"]
|
||||||
|
assert details["software"][0]["name"] == "Borealis Agent"
|
||||||
|
assert device["storage"][0]["model"] == "NVMe"
|
||||||
|
assert device["memory"][0]["capacity"] == 17179869184
|
||||||
|
assert device["cpu"]["name"] == "Intel Core i7"
|
||||||
|
|
||||||
|
|
||||||
def test_heartbeat_preserves_last_user_from_details(prepared_app, monkeypatch):
|
def test_heartbeat_preserves_last_user_from_details(prepared_app, monkeypatch):
|
||||||
client = prepared_app.test_client()
|
client = prepared_app.test_client()
|
||||||
@@ -330,3 +354,32 @@ def test_heartbeat_preserves_last_user_from_details(prepared_app, monkeypatch):
|
|||||||
assert row is not None
|
assert row is not None
|
||||||
assert row[0] == "BUNNY-LAB\\nicole.rappe"
|
assert row[0] == "BUNNY-LAB\\nicole.rappe"
|
||||||
|
|
||||||
|
|
||||||
|
def test_heartbeat_uses_username_when_last_user_missing(prepared_app, monkeypatch):
|
||||||
|
client = prepared_app.test_client()
|
||||||
|
guid = "802A4E5F-1B2C-4D5E-8F90-A1B2C3D4E5F7"
|
||||||
|
fingerprint = "55:66:77:88"
|
||||||
|
hostname = "device-username"
|
||||||
|
_insert_device(prepared_app, guid, fingerprint, hostname)
|
||||||
|
|
||||||
|
services = prepared_app.extensions["engine_services"]
|
||||||
|
context = _build_context(guid, fingerprint)
|
||||||
|
monkeypatch.setattr(services.device_auth, "authenticate", lambda request, path: context)
|
||||||
|
|
||||||
|
resp = client.post(
|
||||||
|
"/api/agent/heartbeat",
|
||||||
|
json={"hostname": hostname, "metrics": {"username": "BUNNY-LAB\\alice.smith"}},
|
||||||
|
headers={"Authorization": "Bearer token"},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
db_path = Path(prepared_app.config["ENGINE_DATABASE_PATH"])
|
||||||
|
with sqlite3.connect(db_path) as conn:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT last_user FROM devices WHERE guid = ?",
|
||||||
|
(guid,),
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
assert row is not None
|
||||||
|
assert row[0] == "BUNNY-LAB\\alice.smith"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user