mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 06:40:45 +08:00
590 lines
28 KiB
Python
590 lines
28 KiB
Python
"""Idempotent brownfield schema patches (shared by API startup and Alembic).
|
|
|
|
Historically these lived inline in ``main.on_startup``. Prefer:
|
|
|
|
1. ``alembic upgrade head`` (applies this module via revision)
|
|
2. ``NETX_SKIP_LEGACY_STARTUP_DDL=true`` so API startup does not re-run the big ALTER block
|
|
|
|
``create_all`` still creates missing tables from ORM metadata; these patches add/alter
|
|
columns and indexes that ``create_all`` will not evolve on existing DBs.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import re
|
|
|
|
from sqlalchemy import text
|
|
from sqlalchemy.engine import Connection, Engine
|
|
|
|
_log = logging.getLogger("netx.schema_patches")
|
|
|
|
|
|
def _dialect_name(conn: Connection) -> str:
|
|
return str(getattr(conn.dialect, "name", "") or "").lower()
|
|
|
|
|
|
def _normalize_ddl_for_dialect(conn: Connection, sql: str) -> str:
|
|
"""SQLite lacks ``ADD COLUMN IF NOT EXISTS`` (PG 9.5+/11+). Strip for quiet retry."""
|
|
stmt = str(sql or "").strip()
|
|
if not stmt:
|
|
return stmt
|
|
if _dialect_name(conn) == "sqlite":
|
|
# Keep CREATE/INDEX IF NOT EXISTS; only ALTER TABLE … ADD COLUMN needs help.
|
|
upper = stmt.upper()
|
|
if "ADD COLUMN IF NOT EXISTS" in upper:
|
|
stmt = re.sub(
|
|
r"(?i)\bADD\s+COLUMN\s+IF\s+NOT\s+EXISTS\b",
|
|
"ADD COLUMN",
|
|
stmt,
|
|
count=1,
|
|
)
|
|
return stmt
|
|
|
|
|
|
def _run_sql(conn: Connection, sql: str, *, quiet: bool = True) -> None:
|
|
"""Run one DDL/DML statement.
|
|
|
|
On PostgreSQL a failed statement aborts the whole transaction. Optional
|
|
patches must use a SAVEPOINT (``begin_nested``) so Alembic can still stamp
|
|
``alembic_version`` after a best-effort ALTER fails.
|
|
"""
|
|
stmt = _normalize_ddl_for_dialect(conn, sql)
|
|
if not stmt:
|
|
return
|
|
|
|
def _exec() -> None:
|
|
if hasattr(conn, "exec_driver_sql"):
|
|
conn.exec_driver_sql(stmt)
|
|
else:
|
|
conn.execute(text(stmt))
|
|
|
|
if not quiet:
|
|
_exec()
|
|
return
|
|
|
|
nested = None
|
|
try:
|
|
nested = conn.begin_nested()
|
|
except Exception:
|
|
nested = None
|
|
try:
|
|
_exec()
|
|
if nested is not None:
|
|
nested.commit()
|
|
except Exception:
|
|
if nested is not None:
|
|
try:
|
|
nested.rollback()
|
|
except Exception:
|
|
pass
|
|
_log.debug("schema patch skipped/failed: %s", stmt[:120], exc_info=True)
|
|
|
|
|
|
def _run_optional_block(conn: Connection, label: str, fn) -> None:
|
|
"""Run a block of optional DDL inside a SAVEPOINT."""
|
|
nested = None
|
|
try:
|
|
nested = conn.begin_nested()
|
|
except Exception:
|
|
nested = None
|
|
try:
|
|
fn(conn)
|
|
if nested is not None:
|
|
nested.commit()
|
|
except Exception:
|
|
if nested is not None:
|
|
try:
|
|
nested.rollback()
|
|
except Exception:
|
|
pass
|
|
_log.debug("schema block skipped/failed: %s", label, exc_info=True)
|
|
|
|
|
|
def apply_auth_schema_patches(conn: Connection) -> None:
|
|
"""Columns required before auth bootstrap (safe to run even in Alembic mode)."""
|
|
_run_sql(
|
|
conn,
|
|
"ALTER TABLE app_user ADD COLUMN IF NOT EXISTS must_change_password BOOLEAN DEFAULT FALSE",
|
|
)
|
|
_run_sql(conn, "ALTER TABLE app_user ADD COLUMN IF NOT EXISTS scopes JSON DEFAULT '[]'")
|
|
_run_sql(conn, "ALTER TABLE api_token ADD COLUMN IF NOT EXISTS expires_at TIMESTAMP")
|
|
_run_sql(conn, "ALTER TABLE api_token ADD COLUMN IF NOT EXISTS scopes JSON DEFAULT '[]'")
|
|
_run_sql(
|
|
conn,
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS auth_session (
|
|
id VARCHAR(64) PRIMARY KEY,
|
|
user_id VARCHAR(64) NOT NULL,
|
|
created_at TIMESTAMP,
|
|
expires_at TIMESTAMP,
|
|
revoked_at TIMESTAMP,
|
|
client_ip VARCHAR(128) DEFAULT '',
|
|
user_agent VARCHAR(512) DEFAULT '',
|
|
last_seen_at TIMESTAMP
|
|
)
|
|
""",
|
|
)
|
|
_run_sql(conn, "CREATE INDEX IF NOT EXISTS ix_auth_session_user_id ON auth_session (user_id)")
|
|
_run_sql(conn, "CREATE INDEX IF NOT EXISTS ix_auth_session_expires_at ON auth_session (expires_at)")
|
|
_run_sql(conn, "CREATE INDEX IF NOT EXISTS ix_auth_session_revoked_at ON auth_session (revoked_at)")
|
|
_run_sql(
|
|
conn,
|
|
"ALTER TABLE auth_session ADD COLUMN IF NOT EXISTS refresh_token_hash VARCHAR(128) DEFAULT ''",
|
|
)
|
|
_run_sql(
|
|
conn,
|
|
"ALTER TABLE auth_session ADD COLUMN IF NOT EXISTS refresh_expires_at TIMESTAMP",
|
|
)
|
|
_run_sql(
|
|
conn,
|
|
"CREATE INDEX IF NOT EXISTS ix_auth_session_refresh_token_hash ON auth_session (refresh_token_hash)",
|
|
)
|
|
|
|
|
|
def apply_topology_schema_safety_net(conn: Connection) -> None:
|
|
"""Always-on topology columns (Alembic stamp-without-upgrade must not skip these)."""
|
|
_run_sql(
|
|
conn,
|
|
"ALTER TABLE topo_folder ADD COLUMN IF NOT EXISTS external_ref VARCHAR(128) DEFAULT ''",
|
|
)
|
|
_run_sql(
|
|
conn,
|
|
"CREATE INDEX IF NOT EXISTS ix_topo_folder_external_ref ON topo_folder (external_ref)",
|
|
)
|
|
_run_sql(conn, "ALTER TABLE topo_fabric_node ADD COLUMN IF NOT EXISTS world_x DOUBLE PRECISION")
|
|
_run_sql(conn, "ALTER TABLE topo_fabric_node ADD COLUMN IF NOT EXISTS world_y DOUBLE PRECISION")
|
|
_run_sql(conn, "ALTER TABLE topo_fabric_node ADD COLUMN IF NOT EXISTS level DOUBLE PRECISION")
|
|
_run_sql(
|
|
conn,
|
|
"CREATE INDEX IF NOT EXISTS ix_topo_fabric_node_world_x ON topo_fabric_node (world_x)",
|
|
)
|
|
_run_sql(
|
|
conn,
|
|
"CREATE INDEX IF NOT EXISTS ix_topo_fabric_node_world_y ON topo_fabric_node (world_y)",
|
|
)
|
|
_run_sql(
|
|
conn,
|
|
"CREATE INDEX IF NOT EXISTS ix_topo_fabric_node_level ON topo_fabric_node (level)",
|
|
)
|
|
# UME link ifnames — required by topology sync/apply; missing when DB predates the columns
|
|
# or Alembic was stamped head without running domain patches.
|
|
_run_sql(
|
|
conn,
|
|
"ALTER TABLE ume_topo_link ADD COLUMN IF NOT EXISTS a_ifname VARCHAR(128) DEFAULT ''",
|
|
)
|
|
_run_sql(
|
|
conn,
|
|
"ALTER TABLE ume_topo_link ADD COLUMN IF NOT EXISTS z_ifname VARCHAR(128) DEFAULT ''",
|
|
)
|
|
|
|
|
|
def apply_collection_schema_safety_net(conn: Connection) -> None:
|
|
"""Always-on NE collection columns (create_all will not ALTER existing tables)."""
|
|
_run_sql(
|
|
conn,
|
|
"ALTER TABLE ne_collection_job ADD COLUMN IF NOT EXISTS last_run_at TIMESTAMP",
|
|
)
|
|
_run_sql(
|
|
conn,
|
|
"ALTER TABLE ne_collection_job ADD COLUMN IF NOT EXISTS trigger_mode VARCHAR(32) DEFAULT 'manual'",
|
|
)
|
|
_run_sql(
|
|
conn,
|
|
"CREATE INDEX IF NOT EXISTS ix_ne_collection_job_trigger_mode ON ne_collection_job (trigger_mode)",
|
|
)
|
|
_run_sql(
|
|
conn,
|
|
"ALTER TABLE ne_collection_run ADD COLUMN IF NOT EXISTS ne_source VARCHAR(16) DEFAULT 'managed'",
|
|
)
|
|
|
|
|
|
def apply_hop_schema_safety_net(conn: Connection) -> None:
|
|
"""Always-on hop / exec_policy columns (Alembic head stamp skips legacy domain patches)."""
|
|
_run_sql(
|
|
conn,
|
|
"ALTER TABLE managed_ne ADD COLUMN IF NOT EXISTS hop_enter_system_view BOOLEAN DEFAULT FALSE",
|
|
)
|
|
_run_sql(
|
|
conn,
|
|
"ALTER TABLE cli_connect_profile ADD COLUMN IF NOT EXISTS hop_enter_system_view BOOLEAN DEFAULT FALSE",
|
|
)
|
|
_run_sql(
|
|
conn,
|
|
"ALTER TABLE managed_ne ADD COLUMN IF NOT EXISTS exec_policy VARCHAR(32) DEFAULT 'readonly'",
|
|
)
|
|
|
|
|
|
def apply_key_alert_schema_patches(
|
|
engine: Engine | None = None,
|
|
*,
|
|
conn: Connection | None = None,
|
|
) -> None:
|
|
"""Evolve ume_key_alert_rule (isolated tx when using engine; in-place when given conn)."""
|
|
from .key_alert_config import invalidate_key_alert_config_cache
|
|
|
|
steps = [
|
|
(
|
|
"add match_type",
|
|
"ALTER TABLE ume_key_alert_rule ADD COLUMN IF NOT EXISTS match_type VARCHAR(32) DEFAULT 'notification_id'",
|
|
),
|
|
(
|
|
"add match_value",
|
|
"ALTER TABLE ume_key_alert_rule ADD COLUMN IF NOT EXISTS match_value VARCHAR(256) DEFAULT ''",
|
|
),
|
|
(
|
|
"create monitor_config",
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS ume_key_alert_monitor_config (
|
|
id INTEGER PRIMARY KEY,
|
|
forward_on_clear INTEGER DEFAULT 0,
|
|
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW()
|
|
)
|
|
""",
|
|
),
|
|
(
|
|
"seed monitor_config",
|
|
"INSERT INTO ume_key_alert_monitor_config (id, forward_on_clear, updated_at) "
|
|
"VALUES (1, 0, NOW()) ON CONFLICT (id) DO NOTHING",
|
|
),
|
|
(
|
|
"backfill match_value from notification_id",
|
|
"UPDATE ume_key_alert_rule SET match_value = notification_id "
|
|
"WHERE (match_value IS NULL OR match_value = '') "
|
|
"AND NOT starts_with(notification_id, 'kw:')",
|
|
),
|
|
(
|
|
"backfill keyword rules",
|
|
"UPDATE ume_key_alert_rule SET match_type = 'keyword', match_value = SUBSTRING(notification_id FROM 4) "
|
|
"WHERE starts_with(notification_id, 'kw:') "
|
|
"AND (match_type IS NULL OR match_type = '' OR match_type = 'notification_id')",
|
|
),
|
|
(
|
|
"migrate forward_on_clear to global config",
|
|
"UPDATE ume_key_alert_monitor_config SET forward_on_clear = 1, updated_at = NOW() "
|
|
"WHERE id = 1 AND EXISTS (SELECT 1 FROM ume_key_alert_rule WHERE forward_on_clear = 1)",
|
|
),
|
|
(
|
|
"add forward_log rule_key",
|
|
"ALTER TABLE ume_key_alert_forward_log ADD COLUMN IF NOT EXISTS rule_key VARCHAR(128) DEFAULT ''",
|
|
),
|
|
(
|
|
"add rule ne_types",
|
|
"ALTER TABLE ume_key_alert_rule ADD COLUMN IF NOT EXISTS ne_types TEXT DEFAULT '[]'",
|
|
),
|
|
]
|
|
if conn is not None:
|
|
for _label, sql in steps:
|
|
_run_sql(conn, sql)
|
|
else:
|
|
if engine is None:
|
|
raise ValueError("engine or conn required")
|
|
for label, sql in steps:
|
|
try:
|
|
with engine.begin() as c:
|
|
_run_sql(c, sql, quiet=False)
|
|
except Exception:
|
|
_log.exception("ume_key_alert_rule schema migration failed at %s", label)
|
|
invalidate_key_alert_config_cache()
|
|
|
|
|
|
def apply_domain_schema_patches(conn: Connection) -> None:
|
|
"""Port traffic, topology, collection, config_sync, and historical column evolves."""
|
|
from .port_traffic_migrate import ensure_port_traffic_series_schema
|
|
from .topology_migrate import ensure_topology_schema
|
|
|
|
# Never let optional ensure_* poison the Alembic transaction.
|
|
_run_optional_block(conn, "port_traffic", ensure_port_traffic_series_schema)
|
|
_run_optional_block(conn, "topology", ensure_topology_schema)
|
|
|
|
_run_sql(
|
|
conn,
|
|
"ALTER TABLE ne_collection_run ADD COLUMN IF NOT EXISTS ne_source VARCHAR(16) DEFAULT 'managed'",
|
|
)
|
|
_run_sql(conn, "ALTER TABLE ne_collection_run ALTER COLUMN ne_id TYPE VARCHAR(128)")
|
|
_run_sql(
|
|
conn,
|
|
"ALTER TABLE config_sync_policy ADD COLUMN IF NOT EXISTS cycle_keep INTEGER DEFAULT 30",
|
|
)
|
|
|
|
pg = _dialect_name(conn).startswith("postgres")
|
|
|
|
_run_sql(conn, "DROP TABLE IF EXISTS ume_inventory_equipment_holder")
|
|
|
|
for sql in (
|
|
"ALTER TABLE alarms_norm ADD COLUMN IF NOT EXISTS relevancy VARCHAR(128) DEFAULT ''",
|
|
"ALTER TABLE alarms_norm ADD COLUMN IF NOT EXISTS l3vpn_peer_ne VARCHAR(256) DEFAULT ''",
|
|
"ALTER TABLE alarms_norm ADD COLUMN IF NOT EXISTS service VARCHAR(256) DEFAULT ''",
|
|
"ALTER TABLE alarms_norm ADD COLUMN IF NOT EXISTS affected_client_service_number INTEGER DEFAULT 0",
|
|
"ALTER TABLE alarms_norm ADD COLUMN IF NOT EXISTS intermittence_count INTEGER DEFAULT 0",
|
|
"ALTER TABLE alarms_norm ADD COLUMN IF NOT EXISTS me_level VARCHAR(128) DEFAULT ''",
|
|
"ALTER TABLE ume_token_cache ADD COLUMN IF NOT EXISTS lock_owner VARCHAR(128) DEFAULT ''",
|
|
"ALTER TABLE ume_token_cache ADD COLUMN IF NOT EXISTS lock_expires_at_epoch_s INTEGER DEFAULT 0",
|
|
"ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS device_level VARCHAR(64) DEFAULT ''",
|
|
"ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS host_name VARCHAR(256) DEFAULT ''",
|
|
"ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS location VARCHAR(512) DEFAULT ''",
|
|
"ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS ipv6_address VARCHAR(128) DEFAULT ''",
|
|
"ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS hardware_version VARCHAR(128) DEFAULT ''",
|
|
"ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS loopback VARCHAR(128) DEFAULT ''",
|
|
"ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS consistent_state VARCHAR(64) DEFAULT ''",
|
|
"ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS interface_version VARCHAR(128) DEFAULT ''",
|
|
"ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS mac VARCHAR(128) DEFAULT ''",
|
|
"ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS admin_status VARCHAR(64) DEFAULT ''",
|
|
"ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS address_type VARCHAR(64) DEFAULT ''",
|
|
"ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS connection_status VARCHAR(64) DEFAULT ''",
|
|
"ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS maintain_status VARCHAR(64) DEFAULT ''",
|
|
"ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS net_mask VARCHAR(128) DEFAULT ''",
|
|
"ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS create_time VARCHAR(64) DEFAULT ''",
|
|
"ALTER TABLE ume_inventory_ne ADD COLUMN IF NOT EXISTS creator VARCHAR(128) DEFAULT ''",
|
|
"ALTER TABLE ume_alarms_current ADD COLUMN IF NOT EXISTS host_name VARCHAR(256) DEFAULT ''",
|
|
"ALTER TABLE ume_alarms_history ADD COLUMN IF NOT EXISTS host_name VARCHAR(256) DEFAULT ''",
|
|
"ALTER TABLE ume_alarms_current ADD COLUMN IF NOT EXISTS notification_id VARCHAR(128) DEFAULT ''",
|
|
"ALTER TABLE ume_alarms_history ADD COLUMN IF NOT EXISTS notification_id VARCHAR(128) DEFAULT ''",
|
|
"ALTER TABLE ume_topo_link ADD COLUMN IF NOT EXISTS a_ifname VARCHAR(128) DEFAULT ''",
|
|
"ALTER TABLE ume_topo_link ADD COLUMN IF NOT EXISTS z_ifname VARCHAR(128) DEFAULT ''",
|
|
"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",
|
|
"ALTER TABLE topo_folder ADD COLUMN IF NOT EXISTS external_ref VARCHAR(128) DEFAULT ''",
|
|
"CREATE INDEX IF NOT EXISTS ix_topo_folder_external_ref ON topo_folder (external_ref)",
|
|
"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 INDEX IF NOT EXISTS ix_ume_alarms_current_notification_id ON ume_alarms_current (notification_id)",
|
|
"CREATE INDEX IF NOT EXISTS ix_ume_alarms_history_notification_id ON ume_alarms_history (notification_id)",
|
|
"CREATE INDEX IF NOT EXISTS ix_ume_alarms_current_host_name ON ume_alarms_current (host_name)",
|
|
"CREATE INDEX IF NOT EXISTS ix_ume_alarms_history_host_name ON ume_alarms_history (host_name)",
|
|
"ALTER TABLE api_token ADD COLUMN IF NOT EXISTS expires_at TIMESTAMP",
|
|
"ALTER TABLE app_user ADD COLUMN IF NOT EXISTS must_change_password BOOLEAN DEFAULT FALSE",
|
|
"ALTER TABLE managed_ne ADD COLUMN IF NOT EXISTS hop_enabled BOOLEAN DEFAULT FALSE",
|
|
"ALTER TABLE managed_ne ADD COLUMN IF NOT EXISTS hop_vendor VARCHAR(32) DEFAULT 'zte'",
|
|
"ALTER TABLE managed_ne ADD COLUMN IF NOT EXISTS hop_host VARCHAR(128) DEFAULT ''",
|
|
"ALTER TABLE managed_ne ADD COLUMN IF NOT EXISTS hop_port INTEGER DEFAULT 22",
|
|
"ALTER TABLE managed_ne ADD COLUMN IF NOT EXISTS hop_protocol VARCHAR(16) DEFAULT 'ssh'",
|
|
"ALTER TABLE managed_ne ADD COLUMN IF NOT EXISTS hop_username VARCHAR(128) DEFAULT ''",
|
|
"ALTER TABLE managed_ne ADD COLUMN IF NOT EXISTS hop_password_enc TEXT DEFAULT ''",
|
|
"ALTER TABLE managed_ne ADD COLUMN IF NOT EXISTS hop_command_template TEXT DEFAULT ''",
|
|
"ALTER TABLE managed_ne ADD COLUMN IF NOT EXISTS hop_vrf VARCHAR(128) DEFAULT ''",
|
|
"ALTER TABLE managed_ne ADD COLUMN IF NOT EXISTS hop_target_auth_mode VARCHAR(32) DEFAULT 'bastion_managed'",
|
|
"ALTER TABLE managed_ne ADD COLUMN IF NOT EXISTS hop_enter_system_view BOOLEAN DEFAULT FALSE",
|
|
"ALTER TABLE cli_connect_profile ADD COLUMN IF NOT EXISTS hop_enter_system_view BOOLEAN DEFAULT FALSE",
|
|
"ALTER TABLE managed_ne ADD COLUMN IF NOT EXISTS source VARCHAR(64) DEFAULT ''",
|
|
"ALTER TABLE managed_ne ADD COLUMN IF NOT EXISTS source_ref VARCHAR(128) DEFAULT ''",
|
|
"ALTER TABLE managed_ne ADD COLUMN IF NOT EXISTS connect_detail TEXT DEFAULT ''",
|
|
"ALTER TABLE ne_collection_job ADD COLUMN IF NOT EXISTS last_run_at TIMESTAMP",
|
|
"UPDATE ne_collection_job SET last_run_at = COALESCE(ended_at, started_at, created_at) "
|
|
"WHERE last_run_at IS NULL",
|
|
"ALTER TABLE ne_collection_job ADD COLUMN IF NOT EXISTS trigger_mode VARCHAR(32) DEFAULT 'manual'",
|
|
"ALTER TABLE topology_edge ADD COLUMN IF NOT EXISTS stroke_color VARCHAR(32) DEFAULT ''",
|
|
"ALTER TABLE topology_edge ADD COLUMN IF NOT EXISTS stroke_width INTEGER DEFAULT 0",
|
|
"ALTER TABLE topology_edge ADD COLUMN IF NOT EXISTS line_style VARCHAR(16) DEFAULT ''",
|
|
):
|
|
_run_sql(conn, sql)
|
|
|
|
if pg:
|
|
for sql in (
|
|
"ALTER TABLE ume_alarms_current ALTER COLUMN alarm_key TYPE TEXT",
|
|
"ALTER TABLE ume_alarms_current ALTER COLUMN object_name TYPE TEXT",
|
|
"ALTER TABLE ume_alarms_current ALTER COLUMN event_type TYPE TEXT",
|
|
"ALTER TABLE ume_alarms_current ALTER COLUMN native_probable_cause TYPE TEXT",
|
|
"ALTER TABLE ume_alarms_current ALTER COLUMN perceived_severity TYPE TEXT",
|
|
"ALTER TABLE ume_alarms_current ALTER COLUMN is_cleared TYPE TEXT",
|
|
"ALTER TABLE ume_alarms_current ALTER COLUMN time_created TYPE TEXT",
|
|
"ALTER TABLE ume_alarms_current ALTER COLUMN root_cause_alarm_indication TYPE TEXT",
|
|
"ALTER TABLE ume_alarms_history ALTER COLUMN alarm_key TYPE TEXT",
|
|
"ALTER TABLE ume_alarms_history ALTER COLUMN object_name TYPE TEXT",
|
|
"ALTER TABLE ume_alarms_history ALTER COLUMN event_type TYPE TEXT",
|
|
"ALTER TABLE ume_alarms_history ALTER COLUMN native_probable_cause TYPE TEXT",
|
|
"ALTER TABLE ume_alarms_history ALTER COLUMN perceived_severity TYPE TEXT",
|
|
"ALTER TABLE ume_alarms_history ALTER COLUMN is_cleared TYPE TEXT",
|
|
"ALTER TABLE ume_alarms_history ALTER COLUMN time_created TYPE TEXT",
|
|
"ALTER TABLE ume_alarms_history ALTER COLUMN root_cause_alarm_indication TYPE TEXT",
|
|
"ALTER TABLE ume_alarms_current DROP COLUMN IF EXISTS ne_name",
|
|
"ALTER TABLE ume_alarms_current DROP COLUMN IF EXISTS user_label",
|
|
"ALTER TABLE ume_alarms_history DROP COLUMN IF EXISTS ne_name",
|
|
"ALTER TABLE ume_alarms_history DROP COLUMN IF EXISTS user_label",
|
|
"ALTER TABLE managed_ne DROP CONSTRAINT IF EXISTS managed_ne_ip_address_key",
|
|
"DROP INDEX IF EXISTS managed_ne_ip_address_key",
|
|
"DROP INDEX IF EXISTS ix_managed_ne_ip_address",
|
|
"CREATE INDEX IF NOT EXISTS ix_managed_ne_ip_address ON managed_ne (ip_address)",
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS cli_connect_profile (
|
|
id VARCHAR(64) PRIMARY KEY,
|
|
name VARCHAR(256) DEFAULT '',
|
|
is_default BOOLEAN DEFAULT FALSE,
|
|
username VARCHAR(128) DEFAULT '',
|
|
password_enc TEXT DEFAULT '',
|
|
port INTEGER DEFAULT 22,
|
|
protocol VARCHAR(16) DEFAULT 'ssh',
|
|
device_type_default VARCHAR(128) DEFAULT 'zte_zxros',
|
|
vendor_default VARCHAR(64) DEFAULT 'ZTE',
|
|
ne_type_rules TEXT DEFAULT '',
|
|
hop_enabled BOOLEAN DEFAULT FALSE,
|
|
hop_vendor VARCHAR(32) DEFAULT 'zte',
|
|
hop_host VARCHAR(128) DEFAULT '',
|
|
hop_port INTEGER DEFAULT 22,
|
|
hop_protocol VARCHAR(16) DEFAULT 'ssh',
|
|
hop_username VARCHAR(128) DEFAULT '',
|
|
hop_password_enc TEXT DEFAULT '',
|
|
hop_command_template TEXT DEFAULT '',
|
|
hop_vrf VARCHAR(128) DEFAULT '',
|
|
hop_target_auth_mode VARCHAR(32) DEFAULT 'bastion_managed',
|
|
hop_enter_system_view BOOLEAN DEFAULT FALSE,
|
|
created_at TIMESTAMP,
|
|
updated_at TIMESTAMP
|
|
)
|
|
""",
|
|
"CREATE INDEX IF NOT EXISTS ix_cli_connect_profile_is_default ON cli_connect_profile (is_default)",
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS ume_cli_override (
|
|
ume_ne_id VARCHAR(128) PRIMARY KEY,
|
|
profile_id VARCHAR(64),
|
|
username_override VARCHAR(128) DEFAULT '',
|
|
device_type_override VARCHAR(128) DEFAULT '',
|
|
vendor_override VARCHAR(64) DEFAULT '',
|
|
connect_status VARCHAR(32) DEFAULT 'unknown',
|
|
connect_message VARCHAR(512) DEFAULT '',
|
|
connect_detail TEXT DEFAULT '',
|
|
connect_tested_at TIMESTAMP,
|
|
updated_at TIMESTAMP
|
|
)
|
|
""",
|
|
"CREATE INDEX IF NOT EXISTS ix_ume_cli_override_connect_status ON ume_cli_override (connect_status)",
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS ume_topo_node (
|
|
node_id VARCHAR(128) PRIMARY KEY,
|
|
name VARCHAR(512) DEFAULT '',
|
|
node_type VARCHAR(64) DEFAULT '',
|
|
user_label VARCHAR(512) DEFAULT '',
|
|
owner VARCHAR(64) DEFAULT '',
|
|
parent_node VARCHAR(512) DEFAULT '',
|
|
x_pos INTEGER,
|
|
y_pos INTEGER,
|
|
ume_ne_id VARCHAR(128) DEFAULT '',
|
|
first_seen_at TIMESTAMP,
|
|
last_seen_at TIMESTAMP,
|
|
raw_json TEXT DEFAULT '{}'
|
|
)
|
|
""",
|
|
"CREATE INDEX IF NOT EXISTS ix_ume_topo_node_node_type ON ume_topo_node (node_type)",
|
|
"CREATE INDEX IF NOT EXISTS ix_ume_topo_node_ume_ne_id ON ume_topo_node (ume_ne_id)",
|
|
"CREATE INDEX IF NOT EXISTS ix_ume_topo_node_last_seen_at ON ume_topo_node (last_seen_at)",
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS ume_topo_link (
|
|
link_id VARCHAR(128) PRIMARY KEY,
|
|
name VARCHAR(1024) DEFAULT '',
|
|
user_label TEXT DEFAULT '',
|
|
owner VARCHAR(64) DEFAULT '',
|
|
direction VARCHAR(32) DEFAULT '',
|
|
layer_rate INTEGER,
|
|
connection_status VARCHAR(64) DEFAULT '',
|
|
a_end_tp_ref TEXT DEFAULT '',
|
|
z_end_tp_ref TEXT DEFAULT '',
|
|
a_ume_ne_id VARCHAR(128) DEFAULT '',
|
|
z_ume_ne_id VARCHAR(128) DEFAULT '',
|
|
a_ptp VARCHAR(256) DEFAULT '',
|
|
z_ptp VARCHAR(256) DEFAULT '',
|
|
a_ifname VARCHAR(128) DEFAULT '',
|
|
z_ifname VARCHAR(128) DEFAULT '',
|
|
first_seen_at TIMESTAMP,
|
|
last_seen_at TIMESTAMP,
|
|
raw_json TEXT DEFAULT '{}'
|
|
)
|
|
""",
|
|
"CREATE INDEX IF NOT EXISTS ix_ume_topo_link_a_ume_ne_id ON ume_topo_link (a_ume_ne_id)",
|
|
"CREATE INDEX IF NOT EXISTS ix_ume_topo_link_z_ume_ne_id ON ume_topo_link (z_ume_ne_id)",
|
|
"CREATE INDEX IF NOT EXISTS ix_ume_topo_link_last_seen_at ON ume_topo_link (last_seen_at)",
|
|
"ALTER TABLE ume_topo_link ADD COLUMN IF NOT EXISTS a_ifname VARCHAR(128) DEFAULT ''",
|
|
"ALTER TABLE ume_topo_link ADD COLUMN IF NOT EXISTS z_ifname VARCHAR(128) DEFAULT ''",
|
|
"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)",
|
|
"COMMENT ON TABLE ume_inventory_ne IS '网元对象详细信息'",
|
|
"COMMENT ON COLUMN ume_inventory_ne.ne_id IS '网元uuid'",
|
|
"COMMENT ON COLUMN ume_inventory_ne.ne_name IS '资源名称'",
|
|
"COMMENT ON COLUMN ume_inventory_ne.ne_type IS '网元类型'",
|
|
"COMMENT ON COLUMN ume_inventory_ne.user_label IS '用户标签'",
|
|
"COMMENT ON COLUMN ume_inventory_ne.address_type IS '管理地址类型(1:IPv4,2:IPv6)'",
|
|
"COMMENT ON COLUMN ume_inventory_ne.ip_address IS '网元IPv4地址'",
|
|
"COMMENT ON COLUMN ume_inventory_ne.net_mask IS '管理IPv4掩码(点分十进制)'",
|
|
"COMMENT ON COLUMN ume_inventory_ne.ipv6_address IS 'IPv6地址'",
|
|
"COMMENT ON COLUMN ume_inventory_ne.admin_status IS '管理状态(0-离线,1-在线)'",
|
|
"COMMENT ON COLUMN ume_inventory_ne.connection_status IS '连接状态(0-断链,1-正常)'",
|
|
"COMMENT ON COLUMN ume_inventory_ne.consistent_state IS '数据一致性状态(1一致,2不一致,3冲突)'",
|
|
"COMMENT ON COLUMN ume_inventory_ne.maintain_status IS '工程状态(0普通,1调测,2新建)'",
|
|
"COMMENT ON COLUMN ume_inventory_ne.vendor IS '网元提供商'",
|
|
"COMMENT ON COLUMN ume_inventory_ne.interface_version IS '网元接口版本号'",
|
|
"COMMENT ON COLUMN ume_inventory_ne.hardware_version IS '硬件版本'",
|
|
"COMMENT ON COLUMN ume_inventory_ne.mac IS '设备机架MAC地址'",
|
|
"COMMENT ON COLUMN ume_inventory_ne.loopback IS '业务环回IP(IPv4)'",
|
|
"COMMENT ON COLUMN ume_inventory_ne.device_level IS '网元层次'",
|
|
"COMMENT ON COLUMN ume_inventory_ne.host_name IS '主机名称'",
|
|
):
|
|
_run_sql(conn, sql)
|
|
else:
|
|
for sql in (
|
|
"DROP INDEX IF EXISTS managed_ne_ip_address_key",
|
|
"DROP INDEX IF EXISTS ix_managed_ne_ip_address",
|
|
"DROP INDEX IF EXISTS sqlite_autoindex_managed_ne_1",
|
|
"CREATE INDEX IF NOT EXISTS ix_managed_ne_ip_address ON managed_ne (ip_address)",
|
|
"ALTER TABLE ume_alarms_current DROP COLUMN IF EXISTS ne_name",
|
|
"ALTER TABLE ume_alarms_current DROP COLUMN IF EXISTS user_label",
|
|
"ALTER TABLE ume_alarms_history DROP COLUMN IF EXISTS ne_name",
|
|
"ALTER TABLE ume_alarms_history DROP COLUMN IF EXISTS user_label",
|
|
):
|
|
_run_sql(conn, sql)
|
|
|
|
|
|
def apply_all_legacy_startup_ddl(engine: Engine) -> None:
|
|
"""Full brownfield patch set previously inlined in ``main.on_startup``."""
|
|
apply_key_alert_schema_patches(engine)
|
|
with engine.begin() as conn:
|
|
apply_auth_schema_patches(conn)
|
|
apply_domain_schema_patches(conn)
|
|
|
|
|
|
def run_alembic_upgrade_to_head() -> None:
|
|
"""Programmatic ``alembic upgrade head`` (optional on API start).
|
|
|
|
Skip the full Alembic command when already at head — avoids noisy
|
|
``Context impl`` logs and lock waits when nothing to apply.
|
|
"""
|
|
import time
|
|
from pathlib import Path
|
|
|
|
from alembic import command
|
|
from alembic.config import Config
|
|
from alembic.script import ScriptDirectory
|
|
from sqlalchemy import create_engine, text
|
|
|
|
from .config import settings
|
|
|
|
t0 = time.monotonic()
|
|
root = Path(__file__).resolve().parents[1]
|
|
cfg = Config(str(root / "alembic.ini"))
|
|
cfg.set_main_option("sqlalchemy.url", settings.database_url)
|
|
|
|
script = ScriptDirectory.from_config(cfg)
|
|
head = script.get_current_head()
|
|
engine = create_engine(settings.database_url, pool_pre_ping=True)
|
|
current: str | None = None
|
|
try:
|
|
with engine.connect() as conn:
|
|
try:
|
|
row = conn.execute(text("SELECT version_num FROM alembic_version")).fetchone()
|
|
current = str(row[0]) if row and row[0] is not None else None
|
|
except Exception:
|
|
current = None
|
|
finally:
|
|
engine.dispose()
|
|
|
|
if head and current == head:
|
|
_log.info(
|
|
"alembic already at head (%s), skip upgrade (%.2fs)",
|
|
head,
|
|
time.monotonic() - t0,
|
|
)
|
|
return
|
|
|
|
_log.info("alembic upgrading %s -> %s …", current, head)
|
|
command.upgrade(cfg, "head")
|
|
_log.info("alembic upgrade head completed (%.2fs)", time.monotonic() - t0)
|