netx/netx_api/topology_migrate.py
oliver b81e5869a6 Ship gated eye polish, fabric levels, and collection UI refresh.
Topology MCP adds pull/compact/bundle/suggest-hubs with a no-template skill path; API gains fabric level and NE collection policy; web list pages get paging and denser collect/network workflows.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-12 16:13:05 +08:00

167 lines
8.7 KiB
Python

"""Startup helpers for final topology schema (drop legacy map tables)."""
from __future__ import annotations
import logging
from sqlalchemy.engine import Connection
from .schema_patches import _run_sql
_log = logging.getLogger("netx.topology.migrate")
_LEGACY_TABLES = ("topology_edge", "topology_node", "topology_map")
def drop_legacy_topology_tables(conn: Connection) -> None:
"""Remove document-style topology_* tables after cutover to fabric/view."""
dialect = str(getattr(conn.dialect, "name", "") or "").lower()
for table in _LEGACY_TABLES:
if dialect.startswith("postgres"):
_run_sql(conn, f'DROP TABLE IF EXISTS "{table}" CASCADE')
else:
_run_sql(conn, f"DROP TABLE IF EXISTS {table}")
def ensure_topology_schema(conn: Connection) -> None:
drop_legacy_topology_tables(conn)
dialect = str(getattr(conn.dialect, "name", "") or "").lower()
# Best-effort column add for existing DBs (create_all won't alter).
alter_stmts: list[str] = []
if dialect.startswith("postgres"):
alter_stmts.extend(
[
"ALTER TABLE topo_discover_job ADD COLUMN IF NOT EXISTS trigger_mode VARCHAR(32) DEFAULT 'manual'",
"ALTER TABLE lldp_collect_policy ADD COLUMN IF NOT EXISTS history_keep INTEGER DEFAULT 30",
"ALTER TABLE lldp_collect_policy ADD COLUMN IF NOT EXISTS interval_hours INTEGER DEFAULT 24",
"ALTER TABLE topo_view ADD COLUMN IF NOT EXISTS folder_id VARCHAR(64)",
"ALTER TABLE topo_view ADD COLUMN IF NOT EXISTS parent_view_id VARCHAR(64)",
"ALTER TABLE topo_view ADD COLUMN IF NOT EXISTS kind VARCHAR(32) DEFAULT 'custom'",
"ALTER TABLE topo_view ADD COLUMN IF NOT EXISTS role VARCHAR(32) DEFAULT 'core'",
"ALTER TABLE topo_view ADD COLUMN IF NOT EXISTS sort_order INTEGER DEFAULT 0",
"ALTER TABLE topo_fabric_node ADD COLUMN IF NOT EXISTS role VARCHAR(32) DEFAULT ''",
"ALTER TABLE topo_fabric_node ADD COLUMN IF NOT EXISTS level DOUBLE PRECISION",
"ALTER TABLE topo_fabric_node ADD COLUMN IF NOT EXISTS region_folder_id VARCHAR(64)",
"ALTER TABLE topo_fabric_node ADD COLUMN IF NOT EXISTS role_source VARCHAR(16) DEFAULT ''",
"ALTER TABLE topo_fabric_node ADD COLUMN IF NOT EXISTS region_source VARCHAR(16) DEFAULT ''",
"CREATE INDEX IF NOT EXISTS ix_topo_fabric_node_level ON topo_fabric_node (level)",
"ALTER TABLE topo_fabric_node ADD COLUMN IF NOT EXISTS world_x DOUBLE PRECISION",
"ALTER TABLE topo_fabric_node ADD COLUMN IF NOT EXISTS world_y DOUBLE PRECISION",
"CREATE INDEX IF NOT EXISTS ix_topo_fabric_node_world_x ON topo_fabric_node (world_x)",
"CREATE INDEX IF NOT EXISTS ix_topo_fabric_node_world_y ON topo_fabric_node (world_y)",
"""
CREATE TABLE IF NOT EXISTS topo_folder (
id VARCHAR(64) PRIMARY KEY,
parent_id VARCHAR(64),
kind VARCHAR(32) DEFAULT 'region',
name VARCHAR(256) DEFAULT '',
sort_order INTEGER DEFAULT 0,
is_system BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP,
updated_at TIMESTAMP
)
""",
"""
CREATE TABLE IF NOT EXISTS topo_classify_rule (
id VARCHAR(64) PRIMARY KEY,
scope VARCHAR(16) DEFAULT 'role',
name VARCHAR(256) DEFAULT '',
pattern VARCHAR(512) DEFAULT '',
match_field VARCHAR(32) DEFAULT 'name',
priority INTEGER DEFAULT 100,
enabled BOOLEAN DEFAULT TRUE,
payload JSONB DEFAULT '{}',
remark VARCHAR(512) DEFAULT '',
created_at TIMESTAMP,
updated_at TIMESTAMP
)
""",
]
)
elif dialect.startswith("sqlite"):
# SQLite: ignore if column already exists.
alter_stmts.extend(
[
"ALTER TABLE topo_discover_job ADD COLUMN trigger_mode VARCHAR(32) DEFAULT 'manual'",
"ALTER TABLE lldp_collect_policy ADD COLUMN history_keep INTEGER DEFAULT 30",
"ALTER TABLE lldp_collect_policy ADD COLUMN interval_hours INTEGER DEFAULT 24",
"ALTER TABLE topo_view ADD COLUMN folder_id VARCHAR(64)",
"ALTER TABLE topo_view ADD COLUMN parent_view_id VARCHAR(64)",
"ALTER TABLE topo_view ADD COLUMN kind VARCHAR(32) DEFAULT 'custom'",
"ALTER TABLE topo_view ADD COLUMN role VARCHAR(32) DEFAULT 'core'",
"ALTER TABLE topo_view ADD COLUMN sort_order INTEGER DEFAULT 0",
"ALTER TABLE topo_fabric_node ADD COLUMN role VARCHAR(32) DEFAULT ''",
"ALTER TABLE topo_fabric_node ADD COLUMN level REAL",
"ALTER TABLE topo_fabric_node ADD COLUMN region_folder_id VARCHAR(64)",
"ALTER TABLE topo_fabric_node ADD COLUMN role_source VARCHAR(16) DEFAULT ''",
"ALTER TABLE topo_fabric_node ADD COLUMN region_source VARCHAR(16) DEFAULT ''",
"""
CREATE TABLE IF NOT EXISTS topo_folder (
id VARCHAR(64) PRIMARY KEY,
parent_id VARCHAR(64),
kind VARCHAR(32) DEFAULT 'region',
name VARCHAR(256) DEFAULT '',
sort_order INTEGER DEFAULT 0,
is_system BOOLEAN DEFAULT 0,
created_at DATETIME,
updated_at DATETIME
)
""",
"""
CREATE TABLE IF NOT EXISTS topo_classify_rule (
id VARCHAR(64) PRIMARY KEY,
scope VARCHAR(16) DEFAULT 'role',
name VARCHAR(256) DEFAULT '',
pattern VARCHAR(512) DEFAULT '',
match_field VARCHAR(32) DEFAULT 'name',
priority INTEGER DEFAULT 100,
enabled INTEGER DEFAULT 1,
payload TEXT DEFAULT '{}',
remark VARCHAR(512) DEFAULT '',
created_at DATETIME,
updated_at DATETIME
)
""",
]
)
for sql in alter_stmts:
_run_sql(conn, sql)
# Backfill hours from legacy days when hours unset/zero.
_run_sql(
conn,
"UPDATE lldp_collect_policy SET interval_hours = "
"CASE WHEN COALESCE(interval_hours, 0) <= 0 "
"THEN GREATEST(1, COALESCE(interval_days, 1)) * 24 "
"ELSE interval_hours END",
)
# SQLite has no GREATEST in older builds — use MAX (no-op if previous applied).
_run_sql(
conn,
"UPDATE lldp_collect_policy SET interval_hours = "
"CASE WHEN COALESCE(interval_hours, 0) <= 0 "
"THEN MAX(1, COALESCE(interval_days, 1)) * 24 "
"ELSE interval_hours END",
)
if not dialect.startswith("postgres"):
return
stmts = [
"CREATE INDEX IF NOT EXISTS ix_topo_fabric_edge_layer_a ON topo_fabric_edge (layer, a_node_id)",
"CREATE INDEX IF NOT EXISTS ix_topo_fabric_edge_layer_b ON topo_fabric_edge (layer, b_node_id)",
"CREATE INDEX IF NOT EXISTS ix_topo_fabric_edge_layer_seen ON topo_fabric_edge (layer, last_seen_at)",
"CREATE INDEX IF NOT EXISTS ix_topo_fabric_edge_active ON topo_fabric_edge (layer) WHERE status = 'active'",
"CREATE INDEX IF NOT EXISTS ix_topo_view_node_view ON topo_view_node (view_id)",
"CREATE INDEX IF NOT EXISTS ix_topo_view_folder ON topo_view (folder_id)",
"CREATE INDEX IF NOT EXISTS ix_topo_view_parent ON topo_view (parent_view_id)",
"CREATE INDEX IF NOT EXISTS ix_topo_view_kind ON topo_view (kind)",
"CREATE INDEX IF NOT EXISTS ix_topo_folder_parent ON topo_folder (parent_id)",
"CREATE INDEX IF NOT EXISTS ix_topo_fabric_node_role ON topo_fabric_node (role)",
"CREATE INDEX IF NOT EXISTS ix_topo_fabric_node_region ON topo_fabric_node (region_folder_id)",
"CREATE INDEX IF NOT EXISTS ix_topo_classify_rule_scope ON topo_classify_rule (scope, enabled, priority)",
"CREATE UNIQUE INDEX IF NOT EXISTS uq_topo_fabric_node_managed_nn ON topo_fabric_node (managed_ne_id) WHERE managed_ne_id IS NOT NULL",
"CREATE UNIQUE INDEX IF NOT EXISTS uq_topo_fabric_node_ume_nn ON topo_fabric_node (ume_ne_id) WHERE ume_ne_id IS NOT NULL",
"CREATE INDEX IF NOT EXISTS ix_topo_discover_job_trigger ON topo_discover_job (trigger_mode)",
]
for sql in stmts:
_run_sql(conn, sql)