Files
Nexus/server/domain/models/__init__.py
T
Nexus Agent 647ebcb673 feat(watch): 监测槽分钟时长档、指标进度条与负载显示
默认 30 分钟,支持 30分/1时/2时/8时/24时;槽卡展示 CPU/内存/硬盘进度条与 1 分钟负载;API 改用 ttl_minutes。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 04:34:52 +08:00

637 lines
30 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 — Domain Models
SQLAlchemy ORM models for all database tables.
"""
import datetime
from datetime import timezone
import uuid
from sqlalchemy import (
Column, Integer, BigInteger, Float, String, Boolean,
DateTime, Text, ForeignKey, Index, JSON
)
from sqlalchemy.dialects.mysql import MEDIUMTEXT
from sqlalchemy.orm import declarative_base, relationship
def _utcnow():
"""Callable default for DateTime columns — evaluated per-row, not at module import"""
return datetime.datetime.now(timezone.utc)
Base = declarative_base()
# ──────────────────────────────────────────────
# Platform & Node (资产组织层)
# ──────────────────────────────────────────────
class Platform(Base):
"""Asset type template — defines what kind of asset this is"""
__tablename__ = "platforms"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(100), unique=True, nullable=False, comment="平台名称: Linux服务器/Windows/MySQL")
category = Column(String(50), nullable=False, comment="分类: host/database/device")
type = Column(String(50), nullable=False, comment="类型: linux/windows/mysql/switch")
default_protocols = Column(JSON, nullable=True, comment="默认协议: [{name:'ssh',port:22,primary:true}]")
charset = Column(String(20), default="utf-8", comment="默认字符集")
created_at = Column(DateTime, default=_utcnow)
assets = relationship("Server", back_populates="platform")
class Node(Base):
"""Tree-structured asset grouping"""
__tablename__ = "nodes"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(100), nullable=False, comment="节点名称")
parent_id = Column(Integer, ForeignKey("nodes.id", ondelete="SET NULL"), nullable=True, comment="父节点")
sort_order = Column(Integer, default=0, comment="排序权重")
created_at = Column(DateTime, default=_utcnow)
parent = relationship("Node", remote_side=[id], back_populates="children")
children = relationship("Node", back_populates="parent")
assets = relationship("Server", back_populates="node")
__table_args__ = (
Index('idx_nodes_parent_id', 'parent_id'),
)
# ──────────────────────────────────────────────
# Server (核心资产表 — 保持原名,扩展字段)
# ──────────────────────────────────────────────
class Server(Base):
"""Sub-server configuration / Asset"""
__tablename__ = "servers"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(100), unique=True, nullable=False, comment="服务器名称")
domain = Column(String(255), nullable=False, comment="域名或IP")
port = Column(Integer, default=22, comment="SSH端口")
username = Column(String(100), default="root", comment="SSH用户名")
auth_method = Column(String(20), default="key", comment="认证方式: key/password")
password = Column(String(255), nullable=True, comment="SSH密码(加密存储)")
ssh_key_path = Column(String(500), nullable=True, comment="SSH私钥路径")
# Text columns: RSA 4096 private key PEM is ~3.2 KB; Fernet-encrypted ~4.3 KB.
# String(500) would cause DataError in MySQL strict mode for large keys.
ssh_key_private = Column(Text, nullable=True, comment="加密后的私钥内容(Fernet)")
ssh_key_public = Column(Text, nullable=True, comment="公钥内容")
ssh_key_configured = Column(Boolean, default=False, comment="是否已配置SSH Key")
agent_port = Column(Integer, default=8601, comment="Agent API端口")
agent_api_key = Column(String(255), nullable=True, comment="Agent API密钥")
preset_id = Column(Integer, nullable=True, comment="关联的密码预设ID(SSH认证)")
ssh_key_preset_id = Column(Integer, nullable=True, comment="关联的密钥预设ID(SSH认证)")
description = Column(Text, nullable=True, comment="备注说明")
target_path = Column(String(500), nullable=True, comment="推送目标路径")
category = Column(String(100), nullable=True, comment="服务器分类(旧字段,保留兼容)")
# ── 新增:资产组织字段 ──
platform_id = Column(Integer, ForeignKey("platforms.id", ondelete="SET NULL"), nullable=True, comment="平台类型ID")
node_id = Column(Integer, ForeignKey("nodes.id", ondelete="SET NULL"), nullable=True, comment="所属节点ID")
protocols = Column(JSON, nullable=True, comment="实例级协议覆盖: [{name:'ssh',port:2222}]")
extra_attrs = Column(JSON, nullable=True, comment="扩展属性: {os:'ubuntu',arch:'x86_64',cpu:8,mem:'32G'}")
# Status (from Agent heartbeat)
is_online = Column(Boolean, default=False, comment="是否在线")
last_heartbeat = Column(DateTime, nullable=True, comment="最后心跳时间")
system_info = Column(Text, nullable=True, comment="系统信息JSON(Agent上报)")
agent_version = Column(String(20), nullable=True, comment="Agent版本")
# ── 新增:连接状态 ──
connectivity = Column(String(20), default="unknown", comment="连接状态: ok/err/unknown")
last_checked_at = Column(DateTime, nullable=True, comment="上次连接测试时间")
# Metadata
created_at = Column(DateTime, default=_utcnow)
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
# Relationships
sync_logs = relationship("SyncLog", back_populates="server", cascade="all, delete-orphan")
platform = relationship("Platform", back_populates="assets")
node = relationship("Node", back_populates="assets")
__table_args__ = (
Index('idx_servers_is_online', 'is_online'),
Index('idx_servers_category', 'category'),
Index('idx_servers_platform_id', 'platform_id'),
Index('idx_servers_node_id', 'node_id'),
)
class ServerMetricSample(Base):
"""Per-server CPU/mem samples every heartbeat flush (~10min)."""
__tablename__ = "server_metric_samples"
id = Column(Integer, primary_key=True, autoincrement=True)
server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), nullable=False)
recorded_at = Column(DateTime, nullable=False, comment="采样时刻 UTC naive")
is_online = Column(Boolean, nullable=False, default=False, comment="该采样时刻是否在线")
cpu_pct = Column(Integer, nullable=True, comment="CPU %")
mem_pct = Column(Integer, nullable=True, comment="内存 %")
__table_args__ = (
Index("idx_server_metric_server_recorded", "server_id", "recorded_at"),
)
class FleetMetricSample(Base):
"""Fleet-wide CPU/mem/disk averages sampled every heartbeat flush (~10min)."""
__tablename__ = "fleet_metric_samples"
id = Column(Integer, primary_key=True, autoincrement=True)
recorded_at = Column(DateTime, nullable=False, comment="采样时刻 UTC naive")
online_count = Column(Integer, default=0, comment="在线子机数")
sample_count = Column(Integer, default=0, comment="参与均值计算的子机数")
cpu_avg = Column(Integer, nullable=True, comment="CPU 平均 %")
mem_avg = Column(Integer, nullable=True, comment="内存平均 %")
disk_avg = Column(Integer, nullable=True, comment="磁盘平均 %")
__table_args__ = (
Index("idx_fleet_metric_recorded_at", "recorded_at"),
)
class WatchPinSession(Base):
"""One 8h monitoring session when admin pins a server to a watch slot."""
__tablename__ = "watch_pin_sessions"
id = Column(Integer, primary_key=True, autoincrement=True)
admin_id = Column(Integer, ForeignKey("admins.id", ondelete="CASCADE"), nullable=False)
server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), nullable=False)
slot_index = Column(Integer, nullable=False, comment="槽位 0-3")
started_at = Column(DateTime, nullable=False, comment="UTC naive")
ended_at = Column(DateTime, nullable=True, comment="UTC naive")
end_reason = Column(String(16), nullable=True, comment="expired|manual|replaced")
__table_args__ = (
Index("idx_watch_pin_sess_admin_started", "admin_id", "started_at"),
Index("idx_watch_pin_sess_server_started", "server_id", "started_at"),
)
class WatchPin(Base):
"""Active watch slot (max 4 per admin); removed when expired or manual delete."""
__tablename__ = "watch_pins"
id = Column(Integer, primary_key=True, autoincrement=True)
admin_id = Column(Integer, ForeignKey("admins.id", ondelete="CASCADE"), nullable=False)
session_id = Column(Integer, ForeignKey("watch_pin_sessions.id", ondelete="CASCADE"), nullable=False)
slot_index = Column(Integer, nullable=False, comment="槽位 0-3")
server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), nullable=False)
pinned_at = Column(DateTime, nullable=False, comment="UTC naive")
expires_at = Column(DateTime, nullable=False, comment="UTC naive")
monitoring_enabled = Column(
Boolean,
nullable=False,
default=True,
comment="是否开启实时探针(关闭时保留槽位、暂停倒计时)",
)
ttl_hours = Column(
Integer,
nullable=False,
default=8,
comment="遗留字段(小时),新逻辑用 ttl_minutes",
)
ttl_minutes = Column(
Integer,
nullable=False,
default=30,
comment="监测窗口时长(分钟) 30|60|120|480|1440",
)
__table_args__ = (
Index("uq_watch_pins_admin_slot", "admin_id", "slot_index", unique=True),
Index("uq_watch_pins_admin_server", "admin_id", "server_id", unique=True),
Index("idx_watch_pins_expires", "expires_at"),
)
class WatchProbeRecord(Base):
"""Per-probe sample (5s) while server is pinned — success or failure."""
__tablename__ = "watch_probe_records"
id = Column(Integer, primary_key=True, autoincrement=True)
session_id = Column(Integer, ForeignKey("watch_pin_sessions.id", ondelete="CASCADE"), nullable=False)
server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), nullable=False)
admin_id = Column(Integer, ForeignKey("admins.id", ondelete="CASCADE"), nullable=False)
recorded_at = Column(DateTime, nullable=False, comment="UTC naive")
source = Column(String(16), nullable=False, comment="redis|ssh|mixed")
probe_status = Column(String(24), nullable=False, comment="ok|ssh_timeout|...")
duration_ms = Column(Integer, nullable=False, default=0)
is_online = Column(Boolean, nullable=False, default=False)
cpu_pct = Column(Integer, nullable=True)
mem_pct = Column(Integer, nullable=True)
disk_pct = Column(Integer, nullable=True)
disk_mount = Column(String(255), nullable=True, default="/")
load_1 = Column(Float, nullable=True)
load_5 = Column(Float, nullable=True)
load_15 = Column(Float, nullable=True)
net_bytes_sent = Column(BigInteger, nullable=True)
net_bytes_recv = Column(BigInteger, nullable=True)
disk_read_bytes = Column(BigInteger, nullable=True)
disk_write_bytes = Column(BigInteger, nullable=True)
net_up_bps = Column(BigInteger, nullable=True)
net_down_bps = Column(BigInteger, nullable=True)
disk_read_bps = Column(BigInteger, nullable=True)
disk_write_bps = Column(BigInteger, nullable=True)
processes_json = Column(JSON, nullable=True, comment="Top5 processes snapshot")
error = Column(String(255), nullable=True)
__table_args__ = (
Index("idx_watch_probe_sess_recorded", "session_id", "recorded_at"),
Index("idx_watch_probe_server_recorded", "server_id", "recorded_at"),
Index("idx_watch_probe_admin_recorded", "admin_id", "recorded_at"),
Index("idx_watch_probe_status_recorded", "probe_status", "recorded_at"),
)
class SyncLog(Base):
"""Push operation log"""
__tablename__ = "sync_logs"
id = Column(Integer, primary_key=True, autoincrement=True)
server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), nullable=False)
source_path = Column(String(500), nullable=False, comment="源目录")
target_path = Column(String(500), nullable=False, comment="目标目录")
trigger_type = Column(String(20), comment="触发类型: manual/schedule/batch")
operator = Column(String(100), nullable=True, comment="操作人用户名")
status = Column(String(20), comment="pending/running/success/failed")
sync_mode = Column(String(20), default="incremental", comment="同步模式: incremental/full/overwrite/checksum")
files_total = Column(Integer, default=0)
files_transferred = Column(Integer, default=0)
files_skipped = Column(Integer, default=0)
bytes_transferred = Column(Integer, default=0)
duration_seconds = Column(Integer, default=0)
diff_summary = Column(Text, nullable=True, comment="rsync输出摘要")
push_permission = Column(String(120), nullable=True, comment="推送后权限策略 chown=… chmod=…")
error_message = Column(Text, nullable=True)
started_at = Column(DateTime, default=_utcnow)
finished_at = Column(DateTime, nullable=True)
server = relationship("Server", back_populates="sync_logs")
__table_args__ = (
Index('idx_sync_logs_srv_start', 'server_id', 'started_at'),
)
class Admin(Base):
"""Admin user for web login"""
__tablename__ = "admins"
id = Column(Integer, primary_key=True, autoincrement=True)
username = Column(String(100), unique=True, nullable=False)
password_hash = Column(String(255), nullable=False)
email = Column(String(255), nullable=True)
totp_secret = Column(String(64), nullable=True)
totp_enabled = Column(Boolean, default=False)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=_utcnow)
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
last_login = Column(DateTime, nullable=True)
token_version = Column(Integer, default=0, comment="令牌版本(重用时递增使旧token失效)")
class AdminUiPreference(Base):
"""Per-admin UI state (search history, etc.) — JSON payload keyed by context."""
__tablename__ = "admin_ui_preferences"
admin_id = Column(Integer, ForeignKey("admins.id", ondelete="CASCADE"), primary_key=True)
context = Column(String(50), primary_key=True, comment="UI 上下文,如 servers_search")
payload = Column(JSON, nullable=False, comment='JSON: {"history":[],"last":null}')
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
class LoginAttempt(Base):
"""Login attempt log for brute-force protection"""
__tablename__ = "login_attempts"
id = Column(Integer, primary_key=True, autoincrement=True)
username = Column(String(100), nullable=False)
ip_address = Column(String(45), nullable=False)
attempted_at = Column(DateTime, default=_utcnow)
success = Column(Boolean, default=False)
__table_args__ = (
Index('idx_login_attempts_user_time', 'username', 'attempted_at'),
)
class AdminRefreshToken(Base):
"""Refresh token hash persisted for login sessions (Redis + MySQL dual store)."""
__tablename__ = "admin_refresh_tokens"
id = Column(Integer, primary_key=True, autoincrement=True)
admin_id = Column(Integer, nullable=False, comment="管理员 ID")
token_hash = Column(String(64), nullable=False, unique=True, comment="SHA-256(refresh token)")
expires_at = Column(DateTime, nullable=False, comment="过期时间 UTC")
created_at = Column(DateTime, default=_utcnow)
__table_args__ = (
Index("idx_admin_refresh_admin_expires", "admin_id", "expires_at"),
)
class Setting(Base):
"""Key-value system settings (including brand config)"""
__tablename__ = "settings"
key = Column(String(100), primary_key=True)
value = Column(Text, nullable=True)
updated_at = Column(DateTime, default=_utcnow)
class PasswordPreset(Base):
"""SSH password preset — encrypted storage"""
__tablename__ = "password_presets"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(100), nullable=False, comment="预设名称")
username = Column(String(100), default="root", comment="SSH用户名")
encrypted_pw = Column(String(500), nullable=False, comment="加密后的密码")
created_at = Column(DateTime, default=_utcnow)
class SshKeyPreset(Base):
"""SSH key preset — encrypted private key storage"""
__tablename__ = "ssh_key_presets"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(100), nullable=False, comment="预设名称")
username = Column(String(100), default="root", comment="SSH用户名")
encrypted_private_key = Column(Text, nullable=False, comment="加密后的私钥内容(Fernet)")
public_key = Column(Text, nullable=True, comment="公钥内容(不加密)")
created_at = Column(DateTime, default=_utcnow)
class PendingServer(Base):
"""Servers awaiting successful SSH credential polling before promotion to servers table."""
__tablename__ = "pending_servers"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(100), nullable=False, comment="显示名称")
domain = Column(String(255), nullable=False, comment="IP或域名")
port = Column(Integer, default=22, comment="SSH端口")
agent_port = Column(Integer, default=8601, comment="Agent端口")
target_path = Column(String(500), nullable=True, comment="推送目标路径")
category = Column(String(100), nullable=True, comment="分类")
platform_id = Column(Integer, nullable=True, comment="平台ID")
node_id = Column(Integer, nullable=True, comment="节点ID")
attempts = Column(Integer, default=0, comment="轮询尝试次数")
last_error = Column(Text, nullable=True, comment="最后一次失败摘要")
last_attempt_at = Column(DateTime, nullable=True, comment="最后尝试时间")
created_at = Column(DateTime, default=_utcnow)
__table_args__ = (
Index("idx_pending_servers_domain", "domain"),
)
class PushSchedule(Base):
"""Scheduled task — cron-triggered file push OR script execution."""
__tablename__ = "push_schedules"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(100), nullable=False, comment="调度名称")
# ── Schedule type ──
# 'push': rsync file sync (default, original behaviour)
# 'script': execute a shell command / script via Agent
schedule_type = Column(String(20), default="push", comment="调度类型: push | script")
# ── Push-specific fields (schedule_type = 'push') ──
source_path = Column(String(500), nullable=True, comment="推送源目录(push 类型必填)")
target_path = Column(String(500), nullable=True, comment="推送目标路径(留空则各机 target_path")
sync_mode = Column(String(20), default="incremental", comment="同步模式: incremental/full/overwrite/checksum")
# ── Script-specific fields (schedule_type = 'script') ──
script_id = Column(Integer, ForeignKey("scripts.id", ondelete="SET NULL"), nullable=True,
comment="脚本库引用(script 类型,可留空使用 script_content")
script_content = Column(Text, nullable=True, comment="临时命令内容(script 类型,不保存到脚本库)")
exec_timeout = Column(Integer, default=60, comment="执行超时(秒)")
long_task = Column(Boolean, default=False, comment="长任务 nohup 模式")
# ── Common fields ──
server_ids = Column(Text, nullable=False, comment="JSON数组: 目标服务器ID列表")
run_mode = Column(String(10), default="once", comment="运行模式: once(一次性) | cron(循环)")
cron_expr = Column(String(100), nullable=True, comment="cron表达式 (run_mode=cron时必填)")
fire_at = Column(DateTime, nullable=True, comment="一次性执行时间 (run_mode=once时必填)")
enabled = Column(Boolean, default=True)
last_run_at = Column(DateTime, nullable=True)
created_at = Column(DateTime, default=_utcnow)
__table_args__ = (
Index('idx_push_schedules_enabled', 'enabled'),
)
class PushRetryJob(Base):
"""Failed push retry queue"""
__tablename__ = "push_retry_jobs"
id = Column(Integer, primary_key=True, autoincrement=True)
server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), nullable=False)
server_name = Column(String(100), comment="冗余字段便于列表显示")
operator = Column(String(100), nullable=True, comment="操作人用户名")
source_path = Column(String(500), nullable=False)
target_path = Column(String(500), nullable=False)
status = Column(String(20), default="pending")
retry_count = Column(Integer, default=0)
max_retries = Column(Integer, default=3)
next_retry_at = Column(DateTime)
last_error = Column(Text)
created_at = Column(DateTime, default=_utcnow)
updated_at = Column(DateTime, default=_utcnow)
server = relationship("Server")
__table_args__ = (
Index('idx_push_retry_pending', 'status', 'next_retry_at'),
)
class AuditLog(Base):
"""Audit log — record all key operations"""
__tablename__ = "audit_logs"
id = Column(Integer, primary_key=True, autoincrement=True)
admin_username = Column(String(100), nullable=True, comment="操作人用户名")
action = Column(String(50), nullable=False, comment="操作类型")
target_type = Column(String(50), nullable=True, comment="目标类型")
target_id = Column(Integer, nullable=True, comment="目标ID")
detail = Column(Text, nullable=True, comment="操作详情JSON")
ip_address = Column(String(45), nullable=True, comment="操作IP")
created_at = Column(DateTime, default=_utcnow)
__table_args__ = (
Index('idx_audit_logs_action_time', 'action', 'created_at'),
Index('idx_audit_logs_created_at', 'created_at'),
)
class Script(Base):
"""Script library — reusable shell scripts"""
__tablename__ = "scripts"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(100), nullable=False, comment="脚本名称")
category = Column(String(50), default="ops", comment="分类: ops/deploy/check/cleanup")
content = Column(Text, nullable=False, comment="Shell命令内容")
description = Column(Text, nullable=True, comment="说明")
created_by = Column(String(100), nullable=True, comment="创建人")
created_at = Column(DateTime, default=_utcnow)
updated_at = Column(DateTime, default=_utcnow)
class ScriptExecution(Base):
"""Script execution log — results per server"""
__tablename__ = "script_executions"
id = Column(Integer, primary_key=True, autoincrement=True)
script_id = Column(Integer, ForeignKey("scripts.id", ondelete="SET NULL"), nullable=True, comment="关联脚本(手动输入时为null)")
command = Column(Text, nullable=False, comment="实际执行的命令")
server_ids = Column(Text, nullable=False, comment="JSON数组: 目标服务器ID列表")
credential_id = Column(Integer, ForeignKey("db_credentials.id", ondelete="SET NULL"), nullable=True, comment="DB凭据替换$DB_*变量")
status = Column(String(20), default="pending", comment="pending/running/completed/failed")
results = Column(
Text().with_variant(MEDIUMTEXT(), "mysql"),
nullable=True,
comment="JSON: 每台服务器执行结果{server_id: {stdout,stderr,exit_code}}",
)
operator = Column(String(100), nullable=True, comment="操作人用户名")
started_at = Column(DateTime, default=_utcnow)
completed_at = Column(DateTime, nullable=True)
script = relationship("Script")
__table_args__ = (
Index('idx_script_exec_script_id', 'script_id'),
)
class ServerBatchJob(Base):
"""Server batch job history — persisted after Redis live state (install Agent, health check, etc.)."""
__tablename__ = "server_batch_jobs"
id = Column(Integer, primary_key=True, autoincrement=False, comment="与 Redis job_id 一致")
op = Column(String(50), nullable=False, comment="category/health-check/install-agent/...")
label = Column(String(200), nullable=False, comment="展示标题")
server_ids = Column(Text, nullable=False, comment="JSON 数组: 目标服务器 ID")
status = Column(String(20), default="running", comment="running/completed/failed/partial")
results = Column(Text, nullable=True, comment="JSON 数组: 每台服务器结果")
operator = Column(String(100), nullable=True, comment="操作人")
params = Column(Text, nullable=True, comment="JSON: 额外参数")
started_at = Column(DateTime, default=_utcnow)
completed_at = Column(DateTime, nullable=True)
__table_args__ = (
Index('idx_server_batch_jobs_op_time', 'op', 'started_at'),
Index('idx_server_batch_jobs_started_at', 'started_at'),
)
class DbCredential(Base):
"""Database credentials — encrypted storage for $DB_* variable substitution"""
__tablename__ = "db_credentials"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(100), nullable=False, comment="凭据名称")
db_type = Column(String(20), default="mysql", comment="数据库类型: mysql/postgresql/etc")
host = Column(String(255), default="localhost", comment="数据库主机")
port = Column(Integer, default=3306, comment="数据库端口")
username = Column(String(100), nullable=False, comment="数据库用户名")
encrypted_password = Column(String(500), nullable=False, comment="加密后的密码")
database = Column(String(100), nullable=True, comment="数据库库名")
created_at = Column(DateTime, default=_utcnow)
# ──────────────────────────────────────────────
# Web SSH 会话 & 命令日志
# ──────────────────────────────────────────────
class AlertLog(Base):
"""Persistent alert & recovery history."""
__tablename__ = "alert_logs"
id = Column(Integer, primary_key=True, autoincrement=True)
server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), nullable=False)
server_name = Column(String(100), nullable=True, comment="冗余服务器名,便于列表显示")
alert_type = Column(String(20), nullable=False, comment="cpu|mem|disk|time_drift|system")
value = Column(String(50), nullable=True, comment="指标值(百分比或描述)")
is_recovery = Column(Boolean, default=False, comment="True=恢复通知")
created_at = Column(DateTime, default=_utcnow)
server = relationship("Server")
__table_args__ = (
Index("idx_alert_logs_server_id", "server_id"),
Index("idx_alert_logs_created_at", "created_at"),
Index("idx_alert_logs_type", "alert_type"),
)
class SshSession(Base):
"""Web SSH session lifecycle tracking"""
__tablename__ = "ssh_sessions"
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()), comment="UUID")
server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), nullable=False, comment="服务器ID")
admin_id = Column(Integer, ForeignKey("admins.id", ondelete="SET NULL"), nullable=True, comment="操作人ID")
remote_addr = Column(String(45), nullable=True, comment="客户端IP")
status = Column(String(20), default="active", comment="active/closed")
started_at = Column(DateTime, default=_utcnow)
closed_at = Column(DateTime, nullable=True)
server = relationship("Server")
admin = relationship("Admin")
command_logs = relationship("CommandLog", back_populates="session", cascade="all, delete-orphan")
__table_args__ = (
Index('idx_ssh_sessions_server_id', 'server_id'),
Index('idx_ssh_sessions_admin_id', 'admin_id'),
)
class CommandLog(Base):
"""SSH command log — record every command executed in terminal"""
__tablename__ = "command_logs"
id = Column(Integer, primary_key=True, autoincrement=True)
session_id = Column(String(36), ForeignKey("ssh_sessions.id", ondelete="CASCADE"), nullable=True, comment="SSH会话ID")
server_id = Column(Integer, ForeignKey("servers.id", ondelete="CASCADE"), nullable=False, comment="服务器ID")
admin_id = Column(Integer, ForeignKey("admins.id", ondelete="SET NULL"), nullable=True, comment="操作人ID")
command = Column(String(2000), nullable=False, comment="执行的命令")
remote_addr = Column(String(45), nullable=True, comment="客户端IP")
created_at = Column(DateTime, default=_utcnow)
session = relationship("SshSession", back_populates="command_logs")
server = relationship("Server")
admin = relationship("Admin")
__table_args__ = (
Index('idx_cmdlog_srv_time', 'server_id', 'created_at'),
Index('idx_cmdlog_session_id', 'session_id'),
)
# ──────────────────────────────────────────────
# Quick Commands (终端快捷命令)
# ──────────────────────────────────────────────
class QuickCommand(Base):
"""Terminal quick commands — persistent across browsers, shareable across users"""
__tablename__ = "quick_commands"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(100), nullable=False, comment="命令名称")
cmd = Column(String(500), nullable=False, comment="命令内容")
is_builtin = Column(Boolean, default=False, comment="系统内置标记(不可删除/编辑)")
sort_order = Column(Integer, default=0, comment="排序权重")
created_by = Column(String(100), nullable=True, comment="创建人")
created_at = Column(DateTime, default=_utcnow)
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)