Files
Nexus/server/domain/models/__init__.py
T

499 lines
24 KiB
Python
Raw Normal View History

"""Nexus — Domain Models
SQLAlchemy ORM models for all database tables.
"""
import datetime
from datetime import timezone
import uuid
from sqlalchemy import (
2026-05-30 20:07:45 +08:00
Column, Integer, String, Boolean,
DateTime, Text, ForeignKey, Index, JSON
)
from sqlalchemy.dialects.mysql import MEDIUMTEXT
from sqlalchemy.orm import declarative_base, relationship
2026-05-22 22:16:50 +08:00
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="默认字符集")
2026-05-22 22:16:50 +08:00
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="排序权重")
2026-05-22 22:16:50 +08:00
created_at = Column(DateTime, default=_utcnow)
2026-05-22 22:16:50 +08:00
parent = relationship("Node", remote_side=[id], back_populates="children")
children = relationship("Node", back_populates="parent")
assets = relationship("Server", back_populates="node")
2026-05-22 22:16:50 +08:00
__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="服务器分类(旧字段,保留兼容)")
# ── 新增:资产组织字段 ──
2026-05-22 22:16:50 +08:00
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
2026-05-22 22:16:50 +08:00
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 SyncLog(Base):
"""Push operation log"""
__tablename__ = "sync_logs"
id = Column(Integer, primary_key=True, autoincrement=True)
2026-05-22 22:16:50 +08:00
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)
2026-05-22 22:16:50 +08:00
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)
2026-05-22 22:16:50 +08:00
created_at = Column(DateTime, default=_utcnow)
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
last_login = Column(DateTime, nullable=True)
2026-05-22 22:16:50 +08:00
token_version = Column(Integer, default=0, comment="令牌版本(重用时递增使旧token失效)")
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)
2026-05-22 22:16:50 +08:00
attempted_at = Column(DateTime, default=_utcnow)
success = Column(Boolean, default=False)
2026-05-22 22:16:50 +08:00
__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)
2026-05-22 22:16:50 +08:00
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="加密后的密码")
2026-05-22 22:16:50 +08:00
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)
2026-05-22 22:16:50 +08:00
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)
2026-05-22 22:16:50 +08:00
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)
2026-05-22 22:16:50 +08:00
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")
2026-05-22 22:16:50 +08:00
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="创建人")
2026-05-22 22:16:50 +08:00
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)
2026-05-22 22:16:50 +08:00
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="操作人用户名")
2026-05-22 22:16:50 +08:00
started_at = Column(DateTime, default=_utcnow)
completed_at = Column(DateTime, nullable=True)
2026-05-22 22:16:50 +08:00
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="数据库库名")
2026-05-22 22:16:50 +08:00
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")
2026-05-22 22:16:50 +08:00
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")
2026-05-22 22:16:50 +08:00
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)