Files
Nexus/server/config.py
T
Nexus Deploy 5a56e5a8cb fix(1panel): 安装向导脱敏、配置一致性与 nx update 门控回滚
- /state 白名单脱敏;init-db db_port 修复;connection-check 后清空 redis_pass
- install.html sessionStorage token 与无 token 友好提示
- nx update: 镜像 import 门控、健康失败自动回滚、PENDING=0 回写、卷双写
- Redis 容器改名密码迁移;备份失败默认 WARN;update.sh 管道 ROOT 回退
2026-06-06 17:40:30 +08:00

217 lines
9.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Nexus Configuration — Pydantic Settings with MySQL overrides
Settings loading priority:
1. .env file (bootstrap — required for DB connection to even work)
2. MySQL settings table (dynamic overrides — loaded at startup via load_settings_from_db)
Immutable settings (not overridable from DB):
- SECRET_KEY, API_KEY, ENCRYPTION_KEY, DATABASE_URL — must be set in .env
Mutable settings (overridable from DB):
- system_name/title, pool_size/overflow, redis_url, alert thresholds, Telegram config
"""
import logging
from typing import Optional
from pydantic_settings import BaseSettings, SettingsConfigDict
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
logger = logging.getLogger("nexus.config")
class Settings(BaseSettings):
"""Base settings loaded from .env file, overrideable via MySQL settings table"""
# Brand (configurable in system settings)
SYSTEM_NAME: str = "Nexus"
SYSTEM_TITLE: str = "Nexus — 服务器运维管理平台"
# Server
HOST: str = "0.0.0.0"
PORT: int = 8600
# Deployment
DEPLOY_PATH: str = "/opt/nexus" # Base directory on the server
CORS_ORIGINS: str = "" # Comma-separated: "https://example.com,http://localhost:8600"
API_BASE_URL: str = "" # Public site URL — used for Agent HTTPS scheme (NEXUS_API_BASE_URL)
# Database (immutable — must be set in .env)
DATABASE_URL: str = "mysql+aiomysql://root:password@127.0.0.1:3306/nexus"
# Pool params: install wizard auto-calculates from MySQL max_connections (authoritative).
# Formula: pool_size = max(20, maxConn * 0.4), overflow = max(20, maxConn * 0.3)
# Defaults below are pre-install placeholders only; production uses wizard .env or
# docker/profiles/*.env via Compose — not these class defaults after install.
DB_POOL_SIZE: Optional[int] = 160
DB_MAX_OVERFLOW: Optional[int] = 120
# Security (immutable — changing breaks encryption/auth)
SECRET_KEY: str = "" # Must be set in .env; empty = startup error
API_KEY: str = ""
ENCRYPTION_KEY: str = ""
# Redis (mutable — can be changed from settings UI)
REDIS_URL: str = "redis://127.0.0.1:6379/0"
# SSH
SSH_STRICT_HOST_CHECKING: str = "true"
# JWT (mutable via .env / settings table — not immutable secrets)
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = 60
JWT_REFRESH_TOKEN_EXPIRE_DAYS: int = 30
# Health Check / Heartbeat
HEALTH_CHECK_INTERVAL: int = 60 # seconds — Agent heartbeat interval
# Script exec — built-in batching (servers per wave; avoids long single HTTP)
SCRIPT_EXEC_BATCH_SIZE: int = 50
SCRIPT_EXEC_CONCURRENCY: int = 10 # max parallel SSH exec calls per batch
# Alert thresholds (mutable — configurable from settings UI)
CPU_ALERT_THRESHOLD: int = 80
MEM_ALERT_THRESHOLD: int = 80
DISK_ALERT_THRESHOLD: int = 80
# Telegram (mutable — configurable from settings UI)
TELEGRAM_BOT_TOKEN: str = ""
TELEGRAM_CHAT_ID: str = ""
# Login IP allowlist master switch — "true" = enforce allowlist, "false" = allow all
LOGIN_ALLOWLIST_ENABLED: str = "false"
# Subscription URL for auto-refreshing subscription IPs (every 2h)
LOGIN_SUBSCRIPTION_URL: str = ""
# IPs from subscription (replaced entirely on each refresh)
LOGIN_SUBSCRIPTION_IPS: str = ""
# Manually added IPs (never touched by auto-refresh)
LOGIN_MANUAL_IPS: str = ""
# Legacy: combined view used by auth check = subscription + manual
LOGIN_ALLOWED_IPS: str = ""
# Notification toggles — each alert type can be independently enabled/disabled
# Stored as "true"/"false" strings in MySQL settings table
NOTIFY_ALERT_CPU: str = "true" # CPU 超阈值告警
NOTIFY_ALERT_MEM: str = "true" # 内存超阈值告警
NOTIFY_ALERT_DISK: str = "true" # 磁盘超阈值告警
NOTIFY_RECOVERY: str = "true" # 资源恢复正常通知
NOTIFY_TIME_DRIFT: str = "true" # 时钟偏差严重告警(≥60s
NOTIFY_SYSTEM_REDIS: str = "true" # Redis 连接异常/恢复
NOTIFY_SYSTEM_MYSQL: str = "true" # MySQL 连接异常/恢复
NOTIFY_RESTART: str = "true" # Nexus 后端重启通知
# extra=ignore: .env 中与 MCP 共存的 MYSQL_* / NEXUS_API_BASE_URL 等不进入 Settings
model_config = SettingsConfigDict(
env_prefix="NEXUS_",
env_file=".env",
extra="ignore",
)
# ── MySQL settings table override ──
# Mapping: MySQL settings.key → Settings attribute name
# Only mutable settings are listed here; immutable ones (SECRET_KEY, API_KEY, etc.) are NOT
DB_OVERRIDE_MAP: dict = {
"system_name": "SYSTEM_NAME",
"system_title": "SYSTEM_TITLE",
"db_pool_size": "DB_POOL_SIZE",
"db_max_overflow": "DB_MAX_OVERFLOW",
"redis_url": "REDIS_URL",
"heartbeat_timeout": "HEALTH_CHECK_INTERVAL",
"cpu_alert_threshold": "CPU_ALERT_THRESHOLD",
"mem_alert_threshold": "MEM_ALERT_THRESHOLD",
"disk_alert_threshold": "DISK_ALERT_THRESHOLD",
"telegram_bot_token": "TELEGRAM_BOT_TOKEN",
"telegram_chat_id": "TELEGRAM_CHAT_ID",
"login_allowlist_enabled": "LOGIN_ALLOWLIST_ENABLED",
"login_subscription_url": "LOGIN_SUBSCRIPTION_URL",
"login_subscription_ips": "LOGIN_SUBSCRIPTION_IPS",
"login_manual_ips": "LOGIN_MANUAL_IPS",
"login_allowed_ips": "LOGIN_ALLOWED_IPS",
"api_base_url": "API_BASE_URL",
"jwt_access_token_expire_minutes": "JWT_ACCESS_TOKEN_EXPIRE_MINUTES",
"jwt_refresh_token_expire_days": "JWT_REFRESH_TOKEN_EXPIRE_DAYS",
# Notification toggles
"notify_alert_cpu": "NOTIFY_ALERT_CPU",
"notify_alert_mem": "NOTIFY_ALERT_MEM",
"notify_alert_disk": "NOTIFY_ALERT_DISK",
"notify_recovery": "NOTIFY_RECOVERY",
"notify_time_drift": "NOTIFY_TIME_DRIFT",
"notify_system_redis": "NOTIFY_SYSTEM_REDIS",
"notify_system_mysql": "NOTIFY_SYSTEM_MYSQL",
"notify_restart": "NOTIFY_RESTART",
}
# Settings that require int conversion from DB string values
INT_SETTINGS: set = {
"DB_POOL_SIZE", "DB_MAX_OVERFLOW", "HEALTH_CHECK_INTERVAL",
"JWT_ACCESS_TOKEN_EXPIRE_MINUTES", "JWT_REFRESH_TOKEN_EXPIRE_DAYS",
"CPU_ALERT_THRESHOLD", "MEM_ALERT_THRESHOLD", "DISK_ALERT_THRESHOLD",
}
async def load_settings_from_db(self, session: AsyncSession) -> int:
"""Override mutable settings from MySQL settings table.
Called during app startup (after init_db) to apply dynamic config.
Immutable settings (SECRET_KEY, API_KEY, ENCRYPTION_KEY, DATABASE_URL)
are NOT overridable — they stay as .env values.
Returns: number of settings overridden from DB
"""
from server.domain.models import Setting
try:
result = await session.execute(select(Setting))
rows = result.scalars().all()
overridden = 0
for row in rows:
attr_name = self.DB_OVERRIDE_MAP.get(row.key)
if attr_name and hasattr(self, attr_name):
value = row.value
# Skip null values — .env value takes precedence
if value is None:
continue
# Type conversion
if attr_name in self.INT_SETTINGS:
try:
value = int(value)
except (ValueError, TypeError):
logger.warning(f"Invalid int value for {row.key}: {value}, skipping")
continue
setattr(self, attr_name, value)
overridden += 1
logger.info(
f"Settings loaded from DB: {len(rows)} keys read, "
f"{overridden} overridden"
)
return overridden
except Exception as e:
logger.error(f"Failed to load settings from DB: {e}")
return 0
async def ensure_api_base_url_in_db(self, session: AsyncSession) -> None:
"""Migrate api_base_url from .env to MySQL settings table if missing.
Ensures existing installations (where .env has NEXUS_API_BASE_URL
but the MySQL settings table was created before api_base_url was
added to DB_OVERRIDE_MAP) have the value available for the settings API.
"""
from server.domain.models import Setting
from sqlalchemy import select as sa_select
try:
result = await session.execute(
sa_select(Setting).where(Setting.key == "api_base_url")
)
existing = result.scalar_one_or_none()
if existing is None and self.API_BASE_URL:
new_setting = Setting(key="api_base_url", value=self.API_BASE_URL)
session.add(new_setting)
await session.commit()
logger.info(f"Migrated api_base_url to MySQL settings table: {self.API_BASE_URL}")
except Exception as e:
logger.warning(f"Could not migrate api_base_url to DB: {e}")
settings = Settings()