From c5973eec697f9a27948d0534deba82caca7f7180 Mon Sep 17 00:00:00 2001 From: oliver Date: Thu, 2 Jul 2026 14:24:19 +0800 Subject: [PATCH] fix(managed-ne): prevent ume-sync route conflicts and prefer host_name Ensure fixed managed-ne paths are matched before /{ne_id} so deleting UME-synced NEs no longer triggers managed_ne_not_found, and use UME host_name as the synced display name to align managed NE naming with inventory host identity. Co-authored-by: Cursor --- netx_api/managed_ne_router.py | 30 ++++++++++----------- netx_api/ne_service.py | 2 +- tests/test_managed_ne.py | 50 +++++++++++++++++++++++++++++++++-- 3 files changed, 64 insertions(+), 18 deletions(-) diff --git a/netx_api/managed_ne_router.py b/netx_api/managed_ne_router.py index a1b5629..fe91111 100644 --- a/netx_api/managed_ne_router.py +++ b/netx_api/managed_ne_router.py @@ -83,21 +83,6 @@ def api_create_managed_ne(body: ManagedNeCreate, db: Session = Depends(get_db)): return create_managed_ne(db, body).model_dump() -@router.get("/{ne_id}") -def api_get_managed_ne(ne_id: str, db: Session = Depends(get_db)): - return get_managed_ne(db, ne_id).model_dump() - - -@router.patch("/{ne_id}") -def api_update_managed_ne(ne_id: str, body: ManagedNeUpdate, db: Session = Depends(get_db)): - return update_managed_ne(db, ne_id, body).model_dump() - - -@router.delete("/{ne_id}") -def api_delete_managed_ne(ne_id: str, db: Session = Depends(get_db)): - return delete_managed_ne(db, ne_id) - - @router.get("/import/template") def api_managed_ne_import_template(format: str = Query(default="xlsx")): filename, payload, media_type = build_managed_ne_import_template(format) @@ -165,3 +150,18 @@ def api_connect_test(body: ConnectTestRequest, db: Session = Depends(get_db)): raise HTTPException(status_code=404, detail=f"managed_ne_not_found: {','.join(missing[:5])}") submitted = schedule_connect_tests(ids) return {"ok": True, "submitted": submitted} + + +@router.get("/{ne_id}") +def api_get_managed_ne(ne_id: str, db: Session = Depends(get_db)): + return get_managed_ne(db, ne_id).model_dump() + + +@router.patch("/{ne_id}") +def api_update_managed_ne(ne_id: str, body: ManagedNeUpdate, db: Session = Depends(get_db)): + return update_managed_ne(db, ne_id, body).model_dump() + + +@router.delete("/{ne_id}") +def api_delete_managed_ne(ne_id: str, db: Session = Depends(get_db)): + return delete_managed_ne(db, ne_id) diff --git a/netx_api/ne_service.py b/netx_api/ne_service.py index d2edb34..06cff18 100644 --- a/netx_api/ne_service.py +++ b/netx_api/ne_service.py @@ -564,7 +564,7 @@ def sync_ume_inventory_to_managed_ne(db: Session) -> UmeManagedSyncResult: if existing is None: existing = db.query(ManagedNE).filter(ManagedNE.ip_address == ip).first() device_type, vendor = _infer_managed_ne_type_vendor(str(inv.ne_type or ""), str(inv.vendor or "")) - display_name = str(inv.ne_name or "").strip() or ip + display_name = str(inv.host_name or "").strip() or str(inv.ne_name or "").strip() or ip existing_tags = str(existing.tags or "").strip() if existing is not None else "" if existing is None: existing = ManagedNE( diff --git a/tests/test_managed_ne.py b/tests/test_managed_ne.py index 0342469..93781e2 100644 --- a/tests/test_managed_ne.py +++ b/tests/test_managed_ne.py @@ -12,10 +12,10 @@ from sqlalchemy.pool import StaticPool from netx_api.config import settings from netx_api.db import Base, get_db from netx_api.main import app -from netx_api.models import ManagedNE # noqa: F401 — register table on Base +from netx_api.models import ManagedNE, UmeInventoryNE # noqa: F401 — register table on Base from netx_api.ne_connect import hostname_probe_command, parse_hostname_from_output from netx_api.ne_crypto import decrypt_secret, encrypt_secret -from netx_api.ne_service import create_managed_ne, import_managed_ne +from netx_api.ne_service import UME_SYNC_SOURCE, create_managed_ne, import_managed_ne from netx_api.ne_schemas import ManagedNeCreate @@ -68,6 +68,7 @@ class ManagedNeApiTests(unittest.TestCase): poolclass=StaticPool, ) ManagedNE.__table__.create(bind=self.engine, checkfirst=True) + UmeInventoryNE.__table__.create(bind=self.engine, checkfirst=True) self.Session = sessionmaker(bind=self.engine, autoflush=False, autocommit=False) def override_get_db(): @@ -184,6 +185,51 @@ class ManagedNeApiTests(unittest.TestCase): ) self.assertEqual(r.status_code, 503, r.text) + def test_delete_ume_sync_route_not_captured_by_ne_id(self): + """DELETE /ume-sync must not match DELETE /{ne_id} with ne_id='ume-sync'.""" + db = self.Session() + db.add( + ManagedNE( + ip_address="10.0.0.99", + source=UME_SYNC_SOURCE, + source_ref="ume-ne-1", + tags="UME", + ) + ) + db.commit() + db.close() + + r = self.client.delete("/v1/managed-ne/ume-sync") + self.assertEqual(r.status_code, 200, r.text) + self.assertEqual(r.json()["deleted"], 1) + + r2 = self.client.delete("/v1/managed-ne/ume-sync") + self.assertEqual(r2.status_code, 200, r2.text) + self.assertEqual(r2.json()["deleted"], 0) + + def test_ume_sync_prefers_host_name_for_display_name(self): + db = self.Session() + db.add( + UmeInventoryNE( + ne_id="ume-ne-100", + ip_address="10.0.0.100", + ne_name="Resource-Name-100", + host_name="Host-Name-100", + vendor="ZTE", + ne_type="ZXR10", + ) + ) + db.commit() + db.close() + + synced = self.client.post("/v1/managed-ne/ume-sync") + self.assertEqual(synced.status_code, 200, synced.text) + + listed = self.client.get("/v1/managed-ne", params={"keyword": "10.0.0.100"}) + self.assertEqual(listed.status_code, 200, listed.text) + self.assertEqual(listed.json()["total"], 1, listed.text) + self.assertEqual(listed.json()["items"][0]["name"], "Host-Name-100") + @patch("netx_api.ne_connect._probe_device", return_value=("pass", "ok", None)) def test_connect_test(self, _mock_probe): r = self.client.post(