Files
Nexus/server/infrastructure/database/migrations.py
T
Your Name 8530f0e0d5 安全补强: 6项P0/P1漏洞修复
P0-1: PHP配置注入防护 — install.py添加_escape_php_string()转义单引号和反斜杠
P0-2: WebSocket JWT校验 — _verify_ws_token()要求exp+sub字段
P1-1: 删除SHA256密码fallback — auth_service.py和auth.py直接import bcrypt
P1-3: LIKE通配符转义 — search.py添加_escape_like()并对所有ilike()加escape参数
P1-2: 安全响应头中间件 — main.py添加SecurityHeadersMiddleware注入4个安全头
P0-3: Refresh Token重用检测 — Admin模型添加token_version字段,token格式改为token:admin_id:version

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 22:16:50 +08:00

123 lines
4.8 KiB
Python

"""Nexus — Data Migration: category → Node, backfill platform_id + schema migrations
D8: Migrate existing server category field to the new Node tree structure,
and create default Platform entries for existing server types.
Also handles safe schema migrations for existing databases (adding new columns).
"""
import logging
from sqlalchemy import select, update, text
from sqlalchemy.ext.asyncio import AsyncSession
from server.domain.models import Server, Node, Platform, Base
from server.infrastructure.database.session import AsyncSessionLocal
logger = logging.getLogger("nexus.migration")
# Default platforms to create
DEFAULT_PLATFORMS = [
{"name": "Linux 服务器", "category": "host", "type": "linux",
"default_protocols": [{"name": "ssh", "port": 22, "primary": True}]},
{"name": "Windows 服务器", "category": "host", "type": "windows",
"default_protocols": [{"name": "rdp", "port": 3389, "primary": True}, {"name": "ssh", "port": 22}]},
{"name": "MySQL", "category": "database", "type": "mysql",
"default_protocols": [{"name": "mysql", "port": 3306, "primary": True}]},
{"name": "网络设备", "category": "device", "type": "switch",
"default_protocols": [{"name": "ssh", "port": 22, "primary": True}]},
]
async def migrate_category_to_node():
"""Migrate server.category values to Node tree + backfill platform_id.
Steps:
1. Create default Platform entries (if not exist)
2. Create Node entries from distinct category values (if not exist)
3. Update servers: set node_id and platform_id based on category
"""
async with AsyncSessionLocal() as session:
# ── 1. Create default platforms ──
for plat_data in DEFAULT_PLATFORMS:
existing = await session.execute(
select(Platform).where(Platform.name == plat_data["name"])
)
if not existing.scalar_one_or_none():
platform = Platform(**plat_data)
session.add(platform)
logger.info(f"Created platform: {plat_data['name']}")
await session.commit()
# ── 2. Create nodes from existing categories ──
result = await session.execute(
select(Server.category).distinct().where(Server.category.isnot(None))
)
categories = [row[0] for row in result.all() if row[0]]
category_to_node_id = {}
for cat in categories:
existing = await session.execute(
select(Node).where(Node.name == cat)
)
node = existing.scalar_one_or_none()
if not node:
node = Node(name=cat, sort_order=0)
session.add(node)
await session.flush()
logger.info(f"Created node: {cat}")
category_to_node_id[cat] = node.id
await session.commit()
# ── 3. Backfill server node_id ──
linux_platform = await session.execute(
select(Platform).where(Platform.type == "linux")
)
linux_plat = linux_platform.scalar_one_or_none()
linux_platform_id = linux_plat.id if linux_plat else None
for cat, node_id in category_to_node_id.items():
await session.execute(
update(Server)
.where(Server.category == cat)
.values(node_id=node_id, platform_id=linux_platform_id)
)
logger.info(f"Migrated category '{cat}' → node_id={node_id}, platform_id={linux_platform_id}")
await session.commit()
logger.info(f"Migration complete: {len(category_to_node_id)} categories migrated")
async def run_migrations():
"""Run all data migrations (called during startup after init_db)"""
try:
await migrate_category_to_node()
except Exception as e:
logger.error(f"Data migration failed: {e}")
# Non-fatal — tables exist, migration can be retried
try:
await run_schema_migrations()
except Exception as e:
logger.error(f"Schema migration failed: {e}")
async def run_schema_migrations():
"""Safe schema migrations — add new columns to existing tables.
Uses ALTER TABLE with try/except so it's a no-op if the column already exists.
"""
migrations = [
"ALTER TABLE push_schedules ADD COLUMN sync_mode VARCHAR(20) DEFAULT 'incremental' COMMENT '同步模式'",
"ALTER TABLE admins ADD COLUMN token_version INT DEFAULT 0 COMMENT '令牌版本(重用时递增使旧token失效)'",
]
async with AsyncSessionLocal() as session:
for sql in migrations:
try:
await session.execute(text(sql))
await session.commit()
logger.info(f"Schema migration applied: {sql[:60]}...")
except Exception:
await session.rollback()
# Column already exists — expected for non-first-run