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 typing import Any, Dict, List, Mapping, Optional, Sequence | ||||
|  | ||||
| from Data.Engine.domain.device_auth import normalize_guid | ||||
|  | ||||
| __all__ = [ | ||||
|     "DEVICE_TABLE_COLUMNS", | ||||
|     "DEVICE_TABLE", | ||||
| @@ -91,8 +93,13 @@ class DeviceSnapshot: | ||||
|     operating_system: str | ||||
|     uptime: int | ||||
|     agent_id: str | ||||
|     ansible_ee_ver: str | ||||
|     connection_type: str | ||||
|     connection_endpoint: str | ||||
|     ssl_key_fingerprint: str | ||||
|     token_version: int | ||||
|     status: str | ||||
|     key_added_at: str | ||||
|     details: Dict[str, Any] | ||||
|     summary: Dict[str, Any] | ||||
|  | ||||
| @@ -121,8 +128,13 @@ class DeviceSnapshot: | ||||
|             "operating_system": self.operating_system, | ||||
|             "uptime": self.uptime, | ||||
|             "agent_id": self.agent_id, | ||||
|             "ansible_ee_ver": self.ansible_ee_ver, | ||||
|             "connection_type": self.connection_type, | ||||
|             "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, | ||||
|             "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]: | ||||
|     summary = { | ||||
|         "hostname": record.get("hostname") or "", | ||||
|         "description": record.get("description") or "", | ||||
|         "device_type": record.get("device_type") or "", | ||||
|         "domain": record.get("domain") or "", | ||||
|         "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, | ||||
|     } | ||||
|     hostname = clean_device_str(record.get("hostname")) or "" | ||||
|     description = clean_device_str(record.get("description")) or "" | ||||
|     agent_hash = clean_device_str(record.get("agent_hash")) or "" | ||||
|     raw_guid = clean_device_str(record.get("guid")) | ||||
|     normalized_guid = normalize_guid(raw_guid) | ||||
|  | ||||
|     created_ts = coerce_int(summary.get("created_at")) or 0 | ||||
|     last_seen_ts = coerce_int(summary.get("last_seen")) or 0 | ||||
|     uptime_val = coerce_int(summary.get("uptime")) or 0 | ||||
|     created_ts = coerce_int(record.get("created_at")) or 0 | ||||
|     last_seen_ts = coerce_int(record.get("last_seen")) or 0 | ||||
|     uptime_val = coerce_int(record.get("uptime")) or 0 | ||||
|     token_version = coerce_int(record.get("token_version")) or 0 | ||||
|  | ||||
|     parsed_lists = { | ||||
|         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"]) | ||||
|  | ||||
|     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 = { | ||||
|         "memory": parsed_lists["memory"], | ||||
|         "network": parsed_lists["network"], | ||||
|         "software": parsed_lists["software"], | ||||
|         "storage": parsed_lists["storage"], | ||||
|         "cpu": cpu_obj, | ||||
|         "summary": dict(summary), | ||||
|     } | ||||
|  | ||||
|     payload: Dict[str, Any] = { | ||||
|         "hostname": summary["hostname"], | ||||
|         "description": summary.get("description", ""), | ||||
|         "hostname": hostname, | ||||
|         "description": description, | ||||
|         "created_at": 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", ""), | ||||
|         "guid": summary.get("agent_guid", ""), | ||||
|         "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", ""), | ||||
|         "uptime": uptime_val, | ||||
|         "agent_id": summary.get("agent_id", ""), | ||||
|         "ansible_ee_ver": summary.get("ansible_ee_ver", ""), | ||||
|         "connection_type": summary.get("connection_type", ""), | ||||
|         "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, | ||||
|         "summary": summary, | ||||
|     } | ||||
|   | ||||
| @@ -278,28 +278,60 @@ class SQLiteDeviceInventoryRepository: | ||||
|         for field in ("memory", "network", "software", "storage"): | ||||
|             payload[field] = serialize_device_json(details.get(field), []) | ||||
|         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["domain"] = clean_device_str(summary.get("domain")) | ||||
|         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")) | ||||
|         payload["device_type"] = clean_device_str( | ||||
|             summary.get("device_type") | ||||
|             or summary.get("type") | ||||
|             or summary.get("device_class") | ||||
|         ) | ||||
|         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( | ||||
|             summary.get("last_user") | ||||
|             or summary.get("last_user_name") | ||||
|             or summary.get("logged_in_user") | ||||
|             or summary.get("username") | ||||
|             or summary.get("user") | ||||
|         ) | ||||
|         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["ansible_ee_ver"] = clean_device_str(summary.get("ansible_ee_ver")) | ||||
|         payload["connection_type"] = clean_device_str(summary.get("connection_type")) | ||||
|         payload["connection_endpoint"] = clean_device_str( | ||||
|             summary.get("connection_endpoint") or summary.get("endpoint") | ||||
|         payload["connection_type"] = clean_device_str( | ||||
|             summary.get("connection_type") or summary.get("remote_type") | ||||
|         ) | ||||
|         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["status"] = clean_device_str(summary.get("status")) | ||||
|         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, | ||||
| ) | ||||
| 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__ = [ | ||||
|     "DeviceInventoryService", | ||||
| @@ -240,7 +240,7 @@ class DeviceInventoryService: | ||||
|         summary["hostname"] = hostname | ||||
|  | ||||
|         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: | ||||
|                 cleaned_user = clean_device_str(last_user) | ||||
|                 if cleaned_user: | ||||
| @@ -422,6 +422,8 @@ class DeviceInventoryService: | ||||
|         if created_at is None: | ||||
|             created_at = int(time.time()) | ||||
|         merged_summary["created_at"] = created_at | ||||
|         if not merged_summary.get("created"): | ||||
|             merged_summary["created"] = ts_to_human(created_at) | ||||
|  | ||||
|         if fingerprint: | ||||
|             merged_summary["ssl_key_fingerprint"] = fingerprint | ||||
| @@ -431,6 +433,14 @@ class DeviceInventoryService: | ||||
|             merged_summary["token_version"] = 1 | ||||
|         if not merged_summary.get("status") and 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")) | ||||
|         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", | ||||
|                 "operating_system": "Windows 11", | ||||
|                 "description": "Primary workstation", | ||||
|                 "last_reboot": "2025-10-01 10:00:00", | ||||
|                 "uptime": 3600, | ||||
|             }, | ||||
|             "memory": [{"slot": "DIMM0", "capacity": 17179869184}], | ||||
|             "storage": [{"model": "NVMe", "size": 512}], | ||||
|             "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(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): | ||||
|     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[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