diff --git a/netx_api/main.py b/netx_api/main.py index 0a881bb..3da73c5 100644 --- a/netx_api/main.py +++ b/netx_api/main.py @@ -195,6 +195,31 @@ def on_startup() -> None: conn.exec_driver_sql("ALTER TABLE alarms_norm ADD COLUMN IF NOT EXISTS me_level VARCHAR(128) DEFAULT ''") conn.exec_driver_sql("ALTER TABLE ume_token_cache ADD COLUMN IF NOT EXISTS lock_owner VARCHAR(128) DEFAULT ''") conn.exec_driver_sql("ALTER TABLE ume_token_cache ADD COLUMN IF NOT EXISTS lock_expires_at_epoch_s INTEGER DEFAULT 0") + conn.exec_driver_sql("ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS device_level VARCHAR(64) DEFAULT ''") + conn.exec_driver_sql("ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS host_name VARCHAR(256) DEFAULT ''") + conn.exec_driver_sql("ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS location VARCHAR(512) DEFAULT ''") + conn.exec_driver_sql( + "ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS hardware_version VARCHAR(128) DEFAULT ''" + ) + conn.exec_driver_sql("ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS loopback VARCHAR(128) DEFAULT ''") + conn.exec_driver_sql( + "ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS consistent_state VARCHAR(64) DEFAULT ''" + ) + conn.exec_driver_sql( + "ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS interface_version VARCHAR(128) DEFAULT ''" + ) + conn.exec_driver_sql("ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS mac VARCHAR(128) DEFAULT ''") + conn.exec_driver_sql("ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS admin_status VARCHAR(64) DEFAULT ''") + conn.exec_driver_sql("ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS address_type VARCHAR(64) DEFAULT ''") + conn.exec_driver_sql( + "ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS connection_status VARCHAR(64) DEFAULT ''" + ) + conn.exec_driver_sql( + "ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS maintain_status VARCHAR(64) DEFAULT ''" + ) + conn.exec_driver_sql("ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS net_mask VARCHAR(128) DEFAULT ''") + conn.exec_driver_sql("ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS create_time VARCHAR(64) DEFAULT ''") + conn.exec_driver_sql("ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS creator VARCHAR(128) DEFAULT ''") except Exception: pass try: @@ -379,6 +404,21 @@ def ume_list_ne( "user_label": str(x.user_label or ""), "ip_address": str(x.ip_address or ""), "ne_type": str(x.ne_type or ""), + "device_level": str(x.device_level or ""), + "host_name": str(x.host_name or ""), + "location": str(x.location or ""), + "hardware_version": str(x.hardware_version or ""), + "loopback": str(x.loopback or ""), + "consistent_state": str(x.consistent_state or ""), + "interface_version": str(x.interface_version or ""), + "mac": str(x.mac or ""), + "admin_status": str(x.admin_status or ""), + "address_type": str(x.address_type or ""), + "connection_status": str(x.connection_status or ""), + "maintain_status": str(x.maintain_status or ""), + "net_mask": str(x.net_mask or ""), + "create_time": str(x.create_time or ""), + "creator": str(x.creator or ""), "last_seen_at": (_ensure_utc(x.last_seen_at) or datetime.now(timezone.utc)).isoformat(), } for x in rows @@ -397,6 +437,21 @@ def ume_get_ne(ne_id: str, db: Session = Depends(get_db)) -> dict[str, Any]: "user_label": str(row.user_label or ""), "ip_address": str(row.ip_address or ""), "ne_type": str(row.ne_type or ""), + "device_level": str(row.device_level or ""), + "host_name": str(row.host_name or ""), + "location": str(row.location or ""), + "hardware_version": str(row.hardware_version or ""), + "loopback": str(row.loopback or ""), + "consistent_state": str(row.consistent_state or ""), + "interface_version": str(row.interface_version or ""), + "mac": str(row.mac or ""), + "admin_status": str(row.admin_status or ""), + "address_type": str(row.address_type or ""), + "connection_status": str(row.connection_status or ""), + "maintain_status": str(row.maintain_status or ""), + "net_mask": str(row.net_mask or ""), + "create_time": str(row.create_time or ""), + "creator": str(row.creator or ""), "vendor": str(row.vendor or ""), "source_type": str(row.source_type or ""), "first_seen_at": (_ensure_utc(row.first_seen_at) or datetime.now(timezone.utc)).isoformat(), @@ -415,7 +470,9 @@ def ume_list_alarms( page_size: int = Query(default=50, ge=1, le=500), db: Session = Depends(get_db), ) -> dict[str, Any]: - stmt = db.query(UmeAlarmCurrent) + stmt = db.query(UmeAlarmCurrent, UmeInventoryNE).outerjoin( + UmeInventoryNE, UmeAlarmCurrent.ne_id == UmeInventoryNE.ne_id + ) if severity and str(severity).strip(): stmt = stmt.filter(UmeAlarmCurrent.perceived_severity == str(severity).strip()) if is_cleared and str(is_cleared).strip(): @@ -430,24 +487,27 @@ def ume_list_alarms( | UmeAlarmCurrent.ne_name.contains(kw) | UmeAlarmCurrent.user_label.contains(kw) | UmeAlarmCurrent.native_probable_cause.contains(kw) + | UmeInventoryNE.ne_name.contains(kw) + | UmeInventoryNE.user_label.contains(kw) + | UmeInventoryNE.ip_address.contains(kw) ) total = int(stmt.count()) rows = stmt.order_by(UmeAlarmCurrent.last_seen_at.desc()).offset((page - 1) * page_size).limit(page_size).all() items = [ { - "alarm_key": str(x.alarm_key or ""), - "ne_id": str(x.ne_id or ""), - "ne_name": str(x.ne_name or ""), - "user_label": str(x.user_label or ""), - "object_name": str(x.object_name or ""), - "event_type": str(x.event_type or ""), - "native_probable_cause": str(x.native_probable_cause or ""), - "perceived_severity": str(x.perceived_severity or ""), - "is_cleared": str(x.is_cleared or ""), - "time_created": str(x.time_created or ""), - "last_seen_at": (_ensure_utc(x.last_seen_at) or datetime.now(timezone.utc)).isoformat(), + "alarm_key": str(alarm.alarm_key or ""), + "ne_id": str(alarm.ne_id or ""), + "ne_name": str(alarm.ne_name or (ne.ne_name if ne else "") or ""), + "user_label": str(alarm.user_label or (ne.user_label if ne else "") or ""), + "object_name": str(alarm.object_name or ""), + "event_type": str(alarm.event_type or ""), + "native_probable_cause": str(alarm.native_probable_cause or ""), + "perceived_severity": str(alarm.perceived_severity or ""), + "is_cleared": str(alarm.is_cleared or ""), + "time_created": str(alarm.time_created or ""), + "last_seen_at": (_ensure_utc(alarm.last_seen_at) or datetime.now(timezone.utc)).isoformat(), } - for x in rows + for alarm, ne in rows ] return {"total": total, "page": page, "page_size": page_size, "items": items} @@ -471,7 +531,9 @@ def ume_list_alarms_history( page_size: int = Query(default=50, ge=1, le=500), db: Session = Depends(get_db), ) -> dict[str, Any]: - stmt = db.query(UmeAlarmHistory) + stmt = db.query(UmeAlarmHistory, UmeInventoryNE).outerjoin( + UmeInventoryNE, UmeAlarmHistory.ne_id == UmeInventoryNE.ne_id + ) if severity and str(severity).strip(): stmt = stmt.filter(UmeAlarmHistory.perceived_severity == str(severity).strip()) if ne_id and str(ne_id).strip(): @@ -484,6 +546,9 @@ def ume_list_alarms_history( | UmeAlarmHistory.ne_name.contains(kw) | UmeAlarmHistory.user_label.contains(kw) | UmeAlarmHistory.native_probable_cause.contains(kw) + | UmeInventoryNE.ne_name.contains(kw) + | UmeInventoryNE.user_label.contains(kw) + | UmeInventoryNE.ip_address.contains(kw) ) dt_from = _parse_time(time_from) dt_to = _parse_time(time_to) @@ -495,19 +560,19 @@ def ume_list_alarms_history( rows = stmt.order_by(UmeAlarmHistory.last_seen_at.desc()).offset((page - 1) * page_size).limit(page_size).all() items = [ { - "alarm_key": str(x.alarm_key or ""), - "ne_id": str(x.ne_id or ""), - "ne_name": str(x.ne_name or ""), - "user_label": str(x.user_label or ""), - "object_name": str(x.object_name or ""), - "event_type": str(x.event_type or ""), - "native_probable_cause": str(x.native_probable_cause or ""), - "perceived_severity": str(x.perceived_severity or ""), - "is_cleared": str(x.is_cleared or ""), - "time_created": str(x.time_created or ""), - "last_seen_at": (_ensure_utc(x.last_seen_at) or datetime.now(timezone.utc)).isoformat(), + "alarm_key": str(alarm.alarm_key or ""), + "ne_id": str(alarm.ne_id or ""), + "ne_name": str(alarm.ne_name or (ne.ne_name if ne else "") or ""), + "user_label": str(alarm.user_label or (ne.user_label if ne else "") or ""), + "object_name": str(alarm.object_name or ""), + "event_type": str(alarm.event_type or ""), + "native_probable_cause": str(alarm.native_probable_cause or ""), + "perceived_severity": str(alarm.perceived_severity or ""), + "is_cleared": str(alarm.is_cleared or ""), + "time_created": str(alarm.time_created or ""), + "last_seen_at": (_ensure_utc(alarm.last_seen_at) or datetime.now(timezone.utc)).isoformat(), } - for x in rows + for alarm, ne in rows ] return {"total": total, "page": page, "page_size": page_size, "items": items} diff --git a/netx_api/mcp_server.py b/netx_api/mcp_server.py index cf2d6f4..5c36a6c 100644 --- a/netx_api/mcp_server.py +++ b/netx_api/mcp_server.py @@ -432,6 +432,32 @@ def _call_tool(name: str, args: dict[str, Any]) -> dict[str, Any]: def main() -> None: try: Base.metadata.create_all(bind=engine) + with engine.begin() as conn: + conn.exec_driver_sql("ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS device_level VARCHAR(64) DEFAULT ''") + conn.exec_driver_sql("ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS host_name VARCHAR(256) DEFAULT ''") + conn.exec_driver_sql("ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS location VARCHAR(512) DEFAULT ''") + conn.exec_driver_sql( + "ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS hardware_version VARCHAR(128) DEFAULT ''" + ) + conn.exec_driver_sql("ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS loopback VARCHAR(128) DEFAULT ''") + conn.exec_driver_sql( + "ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS consistent_state VARCHAR(64) DEFAULT ''" + ) + conn.exec_driver_sql( + "ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS interface_version VARCHAR(128) DEFAULT ''" + ) + conn.exec_driver_sql("ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS mac VARCHAR(128) DEFAULT ''") + conn.exec_driver_sql("ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS admin_status VARCHAR(64) DEFAULT ''") + conn.exec_driver_sql("ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS address_type VARCHAR(64) DEFAULT ''") + conn.exec_driver_sql( + "ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS connection_status VARCHAR(64) DEFAULT ''" + ) + conn.exec_driver_sql( + "ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS maintain_status VARCHAR(64) DEFAULT ''" + ) + conn.exec_driver_sql("ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS net_mask VARCHAR(128) DEFAULT ''") + conn.exec_driver_sql("ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS create_time VARCHAR(64) DEFAULT ''") + conn.exec_driver_sql("ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS creator VARCHAR(128) DEFAULT ''") except Exception: pass for line in sys.stdin: diff --git a/netx_api/models.py b/netx_api/models.py index ba8db9e..263d9b3 100644 --- a/netx_api/models.py +++ b/netx_api/models.py @@ -140,6 +140,21 @@ class UmeInventoryNE(Base): user_label: Mapped[str] = mapped_column(String(256), default="", index=True) ip_address: Mapped[str] = mapped_column(String(128), default="", index=True) ne_type: Mapped[str] = mapped_column(String(128), default="") + device_level: Mapped[str] = mapped_column(String(64), default="") + host_name: Mapped[str] = mapped_column(String(256), default="") + location: Mapped[str] = mapped_column(String(512), default="") + hardware_version: Mapped[str] = mapped_column(String(128), default="") + loopback: Mapped[str] = mapped_column(String(128), default="") + consistent_state: Mapped[str] = mapped_column(String(64), default="") + interface_version: Mapped[str] = mapped_column(String(128), default="") + mac: Mapped[str] = mapped_column(String(128), default="") + admin_status: Mapped[str] = mapped_column(String(64), default="") + address_type: Mapped[str] = mapped_column(String(64), default="") + connection_status: Mapped[str] = mapped_column(String(64), default="") + maintain_status: Mapped[str] = mapped_column(String(64), default="") + net_mask: Mapped[str] = mapped_column(String(128), default="") + create_time: Mapped[str] = mapped_column(String(64), default="") + creator: Mapped[str] = mapped_column(String(128), default="") vendor: Mapped[str] = mapped_column(String(64), default="ZTE") source_type: Mapped[str] = mapped_column(String(64), default="ume_restconf") first_seen_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) diff --git a/netx_api/ume_sync_service.py b/netx_api/ume_sync_service.py index c1e0e2c..97ef456 100644 --- a/netx_api/ume_sync_service.py +++ b/netx_api/ume_sync_service.py @@ -39,7 +39,7 @@ def _pick(d: dict[str, Any], *keys: str) -> Any: def _alarm_key(alarm: dict[str, Any]) -> str: - key = _s(_pick(alarm, "alarmKey", "alarm-key", "id")) + key = _s(_pick(alarm, "alarmKey", "alarm-key","alarmkey","id")) if key: return key parts = [ @@ -52,6 +52,25 @@ def _alarm_key(alarm: dict[str, Any]) -> str: return merged or f"fallback-{datetime.utcnow().timestamp()}" +def _derive_ne_id_from_alarm(alarm: dict[str, Any]) -> str: + ne_id = _s(_pick(alarm, "ne-id", "neId", "ne_id")) + if ne_id: + return ne_id + + alarm_key = _s(_pick(alarm, "alarmKey", "alarm-key", "alarmkey")) + if not alarm_key: + return "" + + # Common UME formats observed: + # 1) "#" + # 2) ", , " + if "#" in alarm_key: + return _s(alarm_key.split("#", 1)[0]) + if "," in alarm_key: + return _s(alarm_key.split(",", 1)[0]) + return "" + + def _build_sync_job(domain: str, trigger_mode: str) -> UmeSyncJob: return UmeSyncJob( domain=domain, @@ -88,6 +107,22 @@ def sync_inventory_full(db: Session, client: UMEClient, *, trigger_mode: str = " existing.user_label = _s(_pick(row, "user-label", "user_label")) existing.ip_address = _s(_pick(row, "ip-Address", "ip-address", "ip")) existing.ne_type = _s(_pick(row, "type", "ne-type")) + existing.device_level = _s(_pick(row, "device-level")) + existing.host_name = _s(_pick(row, "host-name")) + existing.location = _s(_pick(row, "location")) + existing.hardware_version = _s(_pick(row, "hardware-version")) + existing.loopback = _s(_pick(row, "loopback")) + existing.consistent_state = _s(_pick(row, "consistent-state")) + existing.interface_version = _s(_pick(row, "interface-version")) + existing.mac = _s(_pick(row, "mac")) + existing.admin_status = _s(_pick(row, "admin-status")) + existing.address_type = _s(_pick(row, "address-type")) + existing.connection_status = _s(_pick(row, "connection-status")) + existing.maintain_status = _s(_pick(row, "maintain-status")) + existing.net_mask = _s(_pick(row, "net-mask")) + existing.create_time = _s(_pick(row, "create-time")) + existing.creator = _s(_pick(row, "creator")) + existing.vendor = _s(_pick(row, "vendor-name")) or "ZTE" existing.last_seen_at = now existing.raw_json = json.dumps(row, ensure_ascii=False, default=str) @@ -192,7 +227,7 @@ def _sync_alarms_common( inserted += 1 else: updated += 1 - existing.ne_id = _s(_pick(alarm, "ne-id", "neId", "ne_id")) + existing.ne_id = _derive_ne_id_from_alarm(alarm) existing.ne_name = _s(_pick(alarm, "ne-name", "neName", "ne_name")) existing.user_label = _s(_pick(alarm, "user-label", "userLabel", "user_label")) existing.object_name = _s(_pick(alarm, "objectName", "object-name")) diff --git a/tests/test_ume_sync.py b/tests/test_ume_sync.py index 18c41c8..9674232 100644 --- a/tests/test_ume_sync.py +++ b/tests/test_ume_sync.py @@ -9,7 +9,7 @@ from sqlalchemy.orm import sessionmaker from netx_api.db import Base from netx_api.models import UmeAlarmCurrent, UmeInventoryNE from netx_api.ume_client import UMEClient -from netx_api.ume_sync_service import sync_alarms_current, sync_inventory_full +from netx_api.ume_sync_service import _derive_ne_id_from_alarm, sync_alarms_current, sync_inventory_full class _FakeResponse: @@ -139,7 +139,17 @@ class UmeSyncServiceTests(unittest.TestCase): class _C: def get_network_elements(self): rows = [ - {"ne-id": "NE-1", "name": "ne1", "user-label": "网元1", "ip-Address": "10.0.0.1", "type": "A"}, + { + "ne-id": "NE-1", + "name": "ne1", + "user-label": "网元1", + "ip-Address": "10.0.0.1", + "type": "A", + "device-level": "Access", + "host-name": "host-1", + "hardware-version": "V1", + "vendor-name": "ZTE", + }, ] return rows, None @@ -154,6 +164,9 @@ class UmeSyncServiceTests(unittest.TestCase): ne = self.db.get(UmeInventoryNE, "NE-1") self.assertIsNotNone(ne) self.assertEqual(ne.user_label, "网元1") + self.assertEqual(ne.device_level, "Access") + self.assertEqual(ne.host_name, "host-1") + self.assertEqual(ne.hardware_version, "V1") def test_sync_current_alarms_upsert(self): class _C: @@ -250,6 +263,19 @@ class UmeSyncServiceTests(unittest.TestCase): self.assertEqual(job.pulled_count, 1) self.assertIsNotNone(self.db.get(UmeAlarmCurrent, "AK-1")) + def test_derive_ne_id_from_alarmkey_formats(self): + alarm_hash = {"alarmkey": "00ceb960-1b62-478e-8303-0935ffea1d28#99010"} + alarm_csv = {"alarmkey": "00ceb960-1b62-478e-8303-0935ffea1d28, 4237, 79"} + + self.assertEqual( + _derive_ne_id_from_alarm(alarm_hash), + "00ceb960-1b62-478e-8303-0935ffea1d28", + ) + self.assertEqual( + _derive_ne_id_from_alarm(alarm_csv), + "00ceb960-1b62-478e-8303-0935ffea1d28", + ) + if __name__ == "__main__": unittest.main() diff --git a/web/src/pages/UmePage.tsx b/web/src/pages/UmePage.tsx index 1878542..2dcdec4 100644 --- a/web/src/pages/UmePage.tsx +++ b/web/src/pages/UmePage.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { Fragment, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { apiPost, @@ -23,6 +23,7 @@ export function UmePage() { const [neKeyword, setNeKeyword] = useState(""); const [nePage, setNePage] = useState(1); const [nePageSize, setNePageSize] = useState(50); + const [expandedNeId, setExpandedNeId] = useState(""); const [curSeverity, setCurSeverity] = useState(""); const [curCleared, setCurCleared] = useState(""); @@ -265,19 +266,55 @@ export function UmePage() { ne_name ip type + device_level + host_name + hw_ver last_seen {(neQuery.data?.items || []).map((x) => ( - - {x.ne_id} - {x.user_label} - {x.ne_name} - {x.ip_address} - {x.ne_type} - {x.last_seen_at ? formatSystemTime(x.last_seen_at) : "-"} - + + + + + + {x.user_label} + {x.ne_name} + {x.ip_address} + {x.ne_type} + {x.device_level || "-"} + {x.host_name || "-"} + {x.hardware_version || "-"} + {x.last_seen_at ? formatSystemTime(x.last_seen_at) : "-"} + + {expandedNeId === x.ne_id ? ( + + +
+
consistent_state: {x.consistent_state || "-"}
+
admin_status: {x.admin_status || "-"}
+
connection_status: {x.connection_status || "-"}
+
maintain_status: {x.maintain_status || "-"}
+
address_type: {x.address_type || "-"}
+
location: {x.location || "-"}
+
loopback: {x.loopback || "-"}
+
net_mask: {x.net_mask || "-"}
+
mac: {x.mac || "-"}
+
interface_version: {x.interface_version || "-"}
+
create_time: {x.create_time || "-"}
+
creator: {x.creator || "-"}
+
+ + + ) : null} +
))} @@ -368,12 +405,19 @@ export function UmePage() {
-
共 {currentQuery.data?.total || 0} 条 · 第 {curPage} 页
+
+ 共 {currentQuery.data?.total || 0} 条 · 第 {curPage}/ + {Math.max(1, Math.ceil(Math.max(0, Number(currentQuery.data?.total || 0)) / Math.max(1, curPageSize)))} 页 +
-