From 475250eb6f84ecee249d775bf99713ffa3d635c2 Mon Sep 17 00:00:00 2001 From: dv Date: Wed, 8 Apr 2026 12:02:25 +0300 Subject: [PATCH] =?UTF-8?q?haiku=20=D0=92=D0=B0=D0=B9=D0=B1=205?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/services/device_discovery.py | 125 +++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/backend/app/services/device_discovery.py b/backend/app/services/device_discovery.py index 7887e84..51b80c2 100644 --- a/backend/app/services/device_discovery.py +++ b/backend/app/services/device_discovery.py @@ -339,12 +339,24 @@ async def _fetch_rack_hosts(dsn: str, table: str) -> list[tuple[str, str, str]]: # ── Main entry point ────────────────────────────────────────────────────────── +@dataclass +class DeviceAction: + """Track each device discovery action for summary reporting.""" + ip: str + hostname: str + action: str # "ADDED", "UPDATED", "SKIPPED", "IGNORED", "ERROR" + reason: str + category: str = "" + role: str = "" + + @dataclass class DiscoveryResult: created: int = 0 updated: int = 0 skipped: int = 0 errors: list[str] = field(default_factory=list) + actions: list[DeviceAction] = field(default_factory=list) async def discover_via_ssh( @@ -514,9 +526,25 @@ async def discover_via_ssh( if changes: logger.info(f"[ssh_discovery] {found.ip} ({found.plugin_name}) - UPDATED: {', '.join(changes)}") result.updated += 1 + result.actions.append(DeviceAction( + ip=found.ip, + hostname=hostname, + action="UPDATED", + reason="; ".join(changes), + category=found.category, + role=found.role, + )) else: logger.info(f"[ssh_discovery] {found.ip} ({found.plugin_name}) - SKIPPED: already exists with same values (source={existing.source}, category={existing.category}, role={existing.role}, hostname={existing.hostname})") result.skipped += 1 + result.actions.append(DeviceAction( + ip=found.ip, + hostname=hostname, + action="SKIPPED", + reason=f"already exists with same values (source={existing.source}, category={existing.category}, role={existing.role}, hostname={existing.hostname})", + category=existing.category, + role=existing.role, + )) else: db.add(Device( object_id=obj.id, @@ -529,9 +557,25 @@ async def discover_via_ssh( )) logger.info(f"[ssh_discovery] {found.ip} ({found.plugin_name}) - ADDED: category={found.category}, role={found.role}, hostname={hostname}, source=auto_ssh") result.created += 1 + result.actions.append(DeviceAction( + ip=found.ip, + hostname=hostname, + action="ADDED", + reason=f"new device from plugin {found.plugin_name}", + category=found.category, + role=found.role, + )) except Exception as exc: logger.error(f"[ssh_discovery] {found.ip} ({found.plugin_name}) - ERROR: {exc}") result.errors.append(f"Error upserting {found.ip}: {exc}") + result.actions.append(DeviceAction( + ip=found.ip, + hostname=hostname, + action="ERROR", + reason=str(exc), + category=found.category, + role=found.role, + )) # Flush after each rack so that subsequent SELECTs (for other racks # that may share the same device IPs) see the just-added rows. @@ -563,6 +607,14 @@ async def discover_via_ssh( if existing is not None: logger.info(f"[ssh_discovery] {mikrotik_ip} (mikrotik) - SKIPPED: already exists (source={existing.source}, category={existing.category}, hostname={existing.hostname})") result.skipped += 1 + result.actions.append(DeviceAction( + ip=mikrotik_ip, + hostname="mikrotik", + action="SKIPPED", + reason=f"already exists (source={existing.source}, category={existing.category}, hostname={existing.hostname})", + category=existing.category, + role=existing.role, + )) else: db.add(Device( object_id=obj.id, @@ -574,9 +626,25 @@ async def discover_via_ssh( )) logger.info(f"[ssh_discovery] {mikrotik_ip} (mikrotik) - ADDED: category=router, role=other, hostname=mikrotik, source=auto_ssh") result.created += 1 + result.actions.append(DeviceAction( + ip=mikrotik_ip, + hostname="mikrotik", + action="ADDED", + reason="discovered via SSH_CLIENT", + category="router", + role="other", + )) except Exception as exc: logger.error(f"[ssh_discovery] {mikrotik_ip} (mikrotik) - ERROR: {exc}") result.errors.append(f"Error upserting {mikrotik_ip}: {exc}") + result.actions.append(DeviceAction( + ip=mikrotik_ip, + hostname="mikrotik", + action="ERROR", + reason=str(exc), + category="router", + role="other", + )) await db.flush() else: @@ -605,6 +673,14 @@ async def discover_via_ssh( if existing is not None: logger.info(f"[ssh_discovery] {proxmox_ip} ({hostname}) - SKIPPED: already exists (source={existing.source}, category={existing.category}, hostname={existing.hostname})") result.skipped += 1 + result.actions.append(DeviceAction( + ip=proxmox_ip, + hostname=hostname, + action="SKIPPED", + reason=f"already exists (source={existing.source}, category={existing.category}, hostname={existing.hostname})", + category=existing.category, + role=existing.role, + )) else: db.add(Device( object_id=obj.id, @@ -616,12 +692,61 @@ async def discover_via_ssh( )) logger.info(f"[ssh_discovery] {proxmox_ip} ({hostname}) - ADDED: category=vm, role=other, hostname={hostname}, source=auto_ssh") result.created += 1 + result.actions.append(DeviceAction( + ip=proxmox_ip, + hostname=hostname, + action="ADDED", + reason="discovered via port 8006 scan", + category="vm", + role="other", + )) except Exception as exc: logger.error(f"[ssh_discovery] {proxmox_ip} ({hostname}) - ERROR: {exc}") result.errors.append(f"Error upserting {proxmox_ip}: {exc}") + result.actions.append(DeviceAction( + ip=proxmox_ip, + hostname=hostname, + action="ERROR", + reason=str(exc), + category="vm", + role="other", + )) await db.flush() else: logger.info(f"[ssh_discovery] Proxmox: No servers found via port 8006 scan (reason: no hosts responding on port 8006)") + # ── Summary Report ───────────────────────────────────────────────────────────── + logger.info("=" * 100) + logger.info(f"[ssh_discovery] SUMMARY: ADDED={result.created}, UPDATED={result.updated}, SKIPPED={result.skipped}, ERRORS={len(result.errors)}") + logger.info("=" * 100) + + # Group actions by status + added = [a for a in result.actions if a.action == "ADDED"] + updated = [a for a in result.actions if a.action == "UPDATED"] + skipped = [a for a in result.actions if a.action == "SKIPPED"] + errors = [a for a in result.actions if a.action == "ERROR"] + + if added: + logger.info("[ssh_discovery] ADDED devices:") + for action in added: + logger.info(f" {action.ip:20} ({action.hostname:30}) | category={action.category:15} role={action.role:10} | Reason: {action.reason}") + + if updated: + logger.info("[ssh_discovery] UPDATED devices:") + for action in updated: + logger.info(f" {action.ip:20} ({action.hostname:30}) | category={action.category:15} role={action.role:10} | Reason: {action.reason}") + + if skipped: + logger.info("[ssh_discovery] SKIPPED devices:") + for action in skipped: + logger.info(f" {action.ip:20} ({action.hostname:30}) | category={action.category:15} role={action.role:10} | Reason: {action.reason}") + + if errors: + logger.error("[ssh_discovery] ERROR devices:") + for action in errors: + logger.error(f" {action.ip:20} ({action.hostname:30}) | category={action.category:15} role={action.role:10} | Reason: {action.reason}") + + logger.info("=" * 100) + return result