Files
Nexus/server/config.py
T

137 lines
5.0 KiB
Python
Raw Normal View History

"""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
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
2026-05-22 08:19:56 +08:00
# Deployment
DEPLOY_PATH: str = "/opt/nexus" # Base directory on the server
CORS_ORIGINS: str = "" # Comma-separated: "https://example.com,http://localhost:8600"
# Database (immutable — must be set in .env)
2026-05-21 22:54:35 +08:00
DATABASE_URL: str = "mysql+aiomysql://root:password@127.0.0.1:3306/nexus"
2026-05-22 08:19:56 +08:00
# Pool params: install.php auto-calculates from MySQL max_connections
# Formula: pool_size = max(20, maxConn * 0.4), overflow = max(20, maxConn * 0.3)
# MySQL max_connections=400 → pool=160, overflow=120, max=280
# These defaults match install.php; override via .env or MySQL settings table
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"
# Health Check / Heartbeat
HEALTH_CHECK_INTERVAL: int = 60 # seconds — Agent heartbeat interval
# 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 = ""
model_config = {"env_prefix": "NEXUS_", "env_file": ".env"}
# ── 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",
}
# Settings that require int conversion from DB string values
INT_SETTINGS: set = {
"DB_POOL_SIZE", "DB_MAX_OVERFLOW", "HEALTH_CHECK_INTERVAL",
"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
# 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
settings = Settings()