Unify UME World as hex nav with conditional flat world map.

Seed a unique L2 World canvas under the UME World container, show the flat map only when nested regions have NEs, pack blocks without overlap, allow drag overrides on the world map while forbidding direct NE adds, and hide the world-map truncation banner.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-07 00:51:08 +08:00
parent 5d705fa7bb
commit 1ef43fdcd4
30 changed files with 4389 additions and 371 deletions

View file

@ -0,0 +1,247 @@
"""Apply docked UME topology into Fabric (local attrs + edges, union with LLDP)."""
from __future__ import annotations
import logging
from typing import Any
from uuid import uuid4
from sqlalchemy.orm import Session
from .models import TopoFabricEdge, TopoFabricNode, UmeInventoryNE, UmeTopoLink, UmeTopoNode
from .topology_common import (
_edge_attrs,
_purge_edge_if_due,
_set_edge_missing,
_utcnow,
)
from .topology_fabric_links import (
_primary_source,
_sources_from_attrs,
find_fabric_edge_compatible,
upsert_fabric_edge,
)
from .topology_fabric_nodes import ensure_fabric_node_for_ume, refresh_fabric_stats
from .topology_lldp import normalize_ifname
from .ume_port_normalize import resolve_link_ifnames
_log = logging.getLogger("netx.ume.topo_apply")
def apply_ume_topology_to_fabric(db: Session) -> dict[str, Any]:
"""Upsert UME ME local coords/links into Fabric; reconcile ume provenance only.
Local UME xPos/yPos are stored in attrs (ume_local_*), not as global world_*.
Flat-world packing runs via ``recompute_flat_world_coords`` after apply.
Edge presence = UME ∪ LLDP ∪ manual. Removing ume from dump only drops the
``ume`` source mark; if no sources remain, mark missing (未发现).
"""
now = _utcnow()
stats: dict[str, Any] = {
"nodes_seen": 0,
"nodes_ensured": 0,
"nodes_coords": 0,
"links_seen": 0,
"edges_upserted": 0,
"edges_merged": 0,
"edges_skipped": 0,
"ume_source_cleared": 0,
"edges_missing": 0,
"edges_purged": 0,
"flat_coords": {},
}
inv_by_id = {
str(r.ne_id): r
for r in db.query(UmeInventoryNE).all()
if str(r.ne_id or "").strip()
}
fabric_by_ume: dict[str, TopoFabricNode] = {
str(n.ume_ne_id): n
for n in db.query(TopoFabricNode).filter(TopoFabricNode.ume_ne_id.isnot(None)).all()
if str(n.ume_ne_id or "").strip()
}
me_nodes = (
db.query(UmeTopoNode)
.filter(UmeTopoNode.node_type == "TOPO_NODE_ME")
.all()
)
for tn in me_nodes:
uid = str(tn.ume_ne_id or tn.node_id or "").strip()
if not uid:
continue
stats["nodes_seen"] += 1
inv = inv_by_id.get(uid)
if inv is not None:
fn = ensure_fabric_node_for_ume(db, inv)
else:
fn = fabric_by_ume.get(uid)
if fn is None:
fn = TopoFabricNode(
id=uuid4().hex,
managed_ne_id=None,
ume_ne_id=uid,
name=(tn.user_label or uid)[:256],
ip="",
vendor="ZTE",
device_type="zte_zxros",
attrs={},
last_seen_at=now,
created_at=now,
updated_at=now,
)
db.add(fn)
db.flush()
else:
if tn.user_label:
fn.name = str(tn.user_label)[:256]
fn.last_seen_at = now
fn.updated_at = now
stats["nodes_ensured"] += 1
attrs = dict(fn.attrs or {})
sources = set(attrs.get("sources") or [])
if not isinstance(sources, set):
sources = {str(x) for x in (sources or []) if str(x).strip()}
sources.add("ume")
attrs["sources"] = sorted(sources)
if tn.x_pos is not None:
attrs["ume_local_x"] = float(tn.x_pos)
stats["nodes_coords"] += 1
if tn.y_pos is not None:
attrs["ume_local_y"] = float(tn.y_pos)
if tn.x_pos is None:
stats["nodes_coords"] += 1
if tn.parent_node:
attrs["ume_parent_node"] = str(tn.parent_node)[:512]
attrs["ume_sbn_id"] = str(tn.parent_node)[:128]
fn.attrs = attrs
fabric_by_ume[uid] = fn
db.flush()
seen_edge_ids: set[str] = set()
links = db.query(UmeTopoLink).all()
for link in links:
stats["links_seen"] += 1
a_uid = str(link.a_ume_ne_id or "").strip()
z_uid = str(link.z_ume_ne_id or "").strip()
if not a_uid or not z_uid or a_uid == z_uid:
stats["edges_skipped"] += 1
continue
a_fn = fabric_by_ume.get(a_uid)
z_fn = fabric_by_ume.get(z_uid)
if a_fn is None or z_fn is None:
stats["edges_skipped"] += 1
continue
a_if = str(link.a_ifname or "").strip()
z_if = str(link.z_ifname or "").strip()
if not a_if or not z_if:
a_if, z_if = resolve_link_ifnames(
a_end_tp_ref=link.a_end_tp_ref or "",
z_end_tp_ref=link.z_end_tp_ref or "",
user_label=link.user_label or "",
)
a_if = normalize_ifname(a_if)
z_if = normalize_ifname(z_if)
if not a_if or not z_if:
stats["edges_skipped"] += 1
continue
existing = find_fabric_edge_compatible(
db,
a_node_id=a_fn.id,
b_node_id=z_fn.id,
a_port=a_if,
b_port=z_if,
)
if existing is not None:
# Merge onto existing LLDP/manual edge (keep its port strings).
attrs = _edge_attrs(existing)
sources = _sources_from_attrs(attrs, fallback=existing.source or "")
sources.add("ume")
attrs["sources"] = sorted(sources)
attrs["ume_link_id"] = str(link.link_id or "")[:128]
existing.attrs = _clear_and_keep(attrs)
if (existing.source or "") != "manual":
existing.source = _primary_source(sources)
existing.status = "active"
existing.last_seen_at = now
existing.updated_at = now
if existing.discovered_at is None:
existing.discovered_at = now
seen_edge_ids.add(existing.id)
stats["edges_merged"] += 1
continue
edge, action = upsert_fabric_edge(
db,
a_node_id=a_fn.id,
b_node_id=z_fn.id,
a_port=a_if,
b_port=z_if,
source="ume",
now=now,
)
if edge is None:
stats["edges_skipped"] += 1
continue
attrs = _edge_attrs(edge)
attrs["ume_link_id"] = str(link.link_id or "")[:128]
edge.attrs = attrs
seen_edge_ids.add(edge.id)
stats["edges_upserted"] += 1
if action == "kept_manual":
stats["edges_merged"] += 1
# Drop ume provenance from edges not seen this round.
for edge in db.query(TopoFabricEdge).all():
attrs = _edge_attrs(edge)
sources = _sources_from_attrs(attrs, fallback=edge.source or "")
if "ume" not in sources and str(edge.source or "").lower() != "ume":
continue
if edge.id in seen_edge_ids:
continue
sources.discard("ume")
stats["ume_source_cleared"] += 1
if "manual" in sources:
attrs["sources"] = sorted(sources)
edge.attrs = attrs
edge.source = "manual"
edge.updated_at = now
continue
if "lldp" in sources:
attrs["sources"] = sorted(sources)
edge.attrs = attrs
edge.source = "lldp"
# Keep active — LLDP still claims the edge until discover marks missing.
edge.updated_at = now
continue
# No remaining discovery source → 未发现
attrs["sources"] = []
edge.attrs = attrs
if _set_edge_missing(edge, now):
stats["edges_missing"] += 1
if _purge_edge_if_due(db, edge):
stats["edges_purged"] += 1
db.commit()
try:
from .ume_topology_flat_coords import recompute_flat_world_coords
stats["flat_coords"] = recompute_flat_world_coords(db)
except Exception:
_log.exception("recompute_flat_world_coords after ume apply failed")
try:
refresh_fabric_stats(db)
except Exception:
_log.exception("refresh_fabric_stats after ume apply failed")
_log.info("ume topology apply done: %s", stats)
return stats
def _clear_and_keep(attrs: dict[str, Any]) -> dict[str, Any]:
from .topology_common import _clear_miss_attrs
return _clear_miss_attrs(attrs)