diff --git a/netx_api/topology_discover_common.py b/netx_api/topology_discover_common.py index b6110cc..3a0e592 100644 --- a/netx_api/topology_discover_common.py +++ b/netx_api/topology_discover_common.py @@ -7,6 +7,7 @@ from fastapi import HTTPException from sqlalchemy.orm import Session from .cli_resolve import get_default_profile, infer_device_type_vendor +from .device_types import LLDP_DISCOVERED_NE_SOURCE from .models import ManagedNE, TopoDiscoverJob, TopoDiscoverJobItem, UmeInventoryNE from .topology_common import _RAW_PREVIEW_MAX from .topology_schemas import ( @@ -24,6 +25,22 @@ def _raw_preview(raw: str, *, limit: int = _RAW_PREVIEW_MAX) -> str: return f"{text[:limit]}\n...[truncated preview {limit}/{len(text)} chars]" +def _is_lldp_placeholder_ne(ne: ManagedNE) -> bool: + """LLDP SSH placeholders have no credentials until the operator promotes them.""" + return str(ne.source or "").strip().lower() in {LLDP_DISCOVERED_NE_SOURCE, "lldp"} + + +def _managed_target_dict(ne: ManagedNE) -> dict[str, str]: + return { + "ne_id": ne.id, + "ume_ne_id": "", + "ne_name": ne.name or "", + "ne_ip": ne.ip_address or "", + "vendor": ne.vendor or "", + "device_type": ne.device_type or "", + } + + def _job_out( db: Session, job: TopoDiscoverJob, @@ -138,21 +155,15 @@ def _resolve_scan_targets( targets: list[dict[str, str]] = [] if scope == "all_inventory": # Managed inventory first; then UME NEs not already covered by the same management IP. + # Skip LLDP placeholders (no login yet); once promoted they join normal inventory. managed_ips: set[str] = set() for ne in db.query(ManagedNE).all(): + if _is_lldp_placeholder_ne(ne): + continue ip = str(ne.ip_address or "").strip() if ip: managed_ips.add(ip) - targets.append( - { - "ne_id": ne.id, - "ume_ne_id": "", - "ne_name": ne.name or "", - "ne_ip": ne.ip_address or "", - "vendor": ne.vendor or "", - "device_type": ne.device_type or "", - } - ) + targets.append(_managed_target_dict(ne)) for ume in db.query(UmeInventoryNE).all(): uid = str(ume.ne_id or "").strip() if not uid: @@ -173,19 +184,10 @@ def _resolve_scan_targets( if mid in seen: continue ne = db.get(ManagedNE, mid) - if ne is None: + if ne is None or _is_lldp_placeholder_ne(ne): continue seen.add(mid) - targets.append( - { - "ne_id": ne.id, - "ume_ne_id": "", - "ne_name": ne.name or "", - "ne_ip": ne.ip_address or "", - "vendor": ne.vendor or "", - "device_type": ne.device_type or "", - } - ) + targets.append(_managed_target_dict(ne)) for uid in ume_ids: key = f"ume:{uid}" if key in seen: @@ -206,17 +208,10 @@ def _resolve_scan_targets( for mid in list(filter_ids): ne = db.get(ManagedNE, mid) if ne is not None: - targets.append( - { - "ne_id": ne.id, - "ume_ne_id": "", - "ne_name": ne.name or "", - "ne_ip": ne.ip_address or "", - "vendor": ne.vendor or "", - "device_type": ne.device_type or "", - } - ) filter_ids.discard(mid) + if _is_lldp_placeholder_ne(ne): + continue + targets.append(_managed_target_dict(ne)) for uid in list(filter_ids): row = _ume_target_dict(db, uid, default_profile) if row is not None: diff --git a/tests/test_lldp_collect.py b/tests/test_lldp_collect.py index ee2f780..3390281 100644 --- a/tests/test_lldp_collect.py +++ b/tests/test_lldp_collect.py @@ -7,6 +7,7 @@ from datetime import datetime, timedelta from uuid import uuid4 from netx_api.db import Base, SessionLocal, engine +from netx_api.device_types import LLDP_DISCOVERED_NE_SOURCE from netx_api.lldp_collect_schemas import LldpCollectPolicyUpdate from netx_api.lldp_collect_service import ( build_discover_request, @@ -255,6 +256,14 @@ class LldpCollectTests(unittest.TestCase): device_type="cisco_ios", ip_address="10.20.30.1", ) + placeholder = ManagedNE( + id=f"ph-{suffix}", + name=f"PH-{suffix}", + vendor="Other", + device_type="generic", + ip_address="", + source=LLDP_DISCOVERED_NE_SOURCE, + ) ume_only = UmeInventoryNE( ne_id=f"u-{suffix}", ne_name=f"U-{suffix}", @@ -271,6 +280,7 @@ class LldpCollectTests(unittest.TestCase): ne_type="ZXCTN", ) self.db.add(managed) + self.db.add(placeholder) self.db.add(ume_only) self.db.add(ume_dup) self.db.commit() @@ -280,9 +290,11 @@ class LldpCollectTests(unittest.TestCase): self.assertIn((managed.id, ""), ids) self.assertIn((ume_only.ne_id, ume_only.ne_id), ids) self.assertNotIn((ume_dup.ne_id, ume_dup.ne_id), ids) + self.assertNotIn((placeholder.id, ""), ids) # Cleanup shared DB rows created by this test. self.db.delete(managed) + self.db.delete(placeholder) self.db.delete(ume_only) self.db.delete(ume_dup) self.db.commit()