Files
Nexus/server/api/schemas.py
T
Your Name 5958c21048
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
feat: 推送页面 Round 4 — 服务器搜索 / Diff对比 / 排错诊断 / 源路径输入 / 批量重试
5 项新功能:
- H2: 服务器搜索过滤 — 目标服务器下拉加搜索框实时过滤
- H3: 推送对比 Diff 视图 — 预览文件列表加 diff 按钮对比本地与远程差异
- H4: 推送排错面板 — 失败行加诊断按钮检查SSH/磁盘/权限
- H5: 源路径直接输入 — 支持直接输入服务器本地路径推送
- H6: 批量重试 — 一键重试所有失败服务器

新增后端端点:
- POST /api/sync/validate-source-path (H5)
- POST /api/sync/diagnose (H4)
- POST /api/sync/file-diff (H3)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 21:29:41 +08:00

367 lines
12 KiB
Python

"""Nexus — Pydantic Request/Response Models
Shared schemas for API validation, replacing raw `dict` parameters.
"""
from typing import Optional, List
from pydantic import BaseModel, Field, model_validator
# ── Setting ──
class SettingUpdatePayload(BaseModel):
value: str = Field(..., min_length=0)
class ApiKeyRevealRequest(BaseModel):
"""Shared re-auth payload for revealing sensitive values (API key, install cmd, etc.)."""
current_password: str = Field(..., min_length=1, max_length=255)
# ── Agent ──
class AgentHeartbeat(BaseModel):
server_id: int
is_online: bool = True
system_info: Optional[dict] = None
agent_version: Optional[str] = None
# ── Server ──
class ServerCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
domain: str = Field(..., min_length=1, max_length=255)
port: int = Field(22, ge=1, le=65535)
username: str = Field("root", max_length=100)
auth_method: str = Field("key", pattern="^(key|password)$")
password: Optional[str] = None
preset_id: Optional[int] = None
ssh_key_path: Optional[str] = None
ssh_key_private: Optional[str] = None
ssh_key_public: Optional[str] = None
ssh_key_preset_id: Optional[int] = None
agent_port: int = Field(8601, ge=1, le=65535)
description: Optional[str] = None
target_path: Optional[str] = None
category: Optional[str] = None
platform_id: Optional[int] = None
node_id: Optional[int] = None
protocols: Optional[list] = None
extra_attrs: Optional[dict] = None
class ServerUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=100)
domain: Optional[str] = Field(None, min_length=1, max_length=255)
port: Optional[int] = Field(None, ge=1, le=65535)
username: Optional[str] = None
auth_method: Optional[str] = Field(None, pattern="^(key|password)$")
password: Optional[str] = None
preset_id: Optional[int] = None
ssh_key_path: Optional[str] = None
ssh_key_private: Optional[str] = None
ssh_key_public: Optional[str] = None
ssh_key_preset_id: Optional[int] = None
agent_port: Optional[int] = Field(None, ge=1, le=65535)
agent_api_key: Optional[str] = None
description: Optional[str] = None
target_path: Optional[str] = None
category: Optional[str] = None
platform_id: Optional[int] = None
node_id: Optional[int] = None
protocols: Optional[list] = None
extra_attrs: Optional[dict] = None
class ServerCheck(BaseModel):
server_ids: List[int] = Field(..., min_length=1)
class ServerImportResult(BaseModel):
"""Result of CSV batch import."""
total: int = 0
created: int = 0
skipped: int = 0
failed: int = 0
errors: List[dict] = [] # [{row, name, domain, error}]
created_ids: List[int] = []
class BatchAgentAction(BaseModel):
"""Batch install/upgrade Agent on multiple servers."""
server_ids: List[int] = Field(..., min_length=1, max_length=50)
class BatchAgentResultItem(BaseModel):
server_id: int
server_name: str
success: bool
stdout: str = ""
error: str = ""
class BatchAgentResult(BaseModel):
results: List[BatchAgentResultItem] = []
class FileUpload(BaseModel):
"""Upload file to remote server via SFTP."""
server_id: int = Field(..., ge=1)
remote_path: str = Field(..., min_length=1)
# ── Sync ──
class SyncFiles(BaseModel):
server_ids: List[int] = Field(..., min_length=1)
source_path: str = Field(..., min_length=1)
target_path: Optional[str] = None
sync_mode: str = Field("incremental", pattern="^(incremental|full|overwrite|checksum)$")
batch_size: int = Field(50, ge=1, le=200)
concurrency: int = Field(10, ge=1, le=50)
class SyncPreview(BaseModel):
"""Dry-run: rsync --dry-run --stats against one representative server."""
server_id: int = Field(..., ge=1)
source_path: str = Field(..., min_length=1)
target_path: Optional[str] = None
sync_mode: str = Field("incremental", pattern="^(incremental|full|overwrite|checksum)$")
verbose: bool = False # If True, include per-file list (truncated to 200 lines)
class SyncBrowse(BaseModel):
server_id: int
path: str = Field("/", min_length=1)
class SyncBrowseLocal(BaseModel):
"""Browse a local directory on the Nexus server (for upload file manager)."""
path: str = Field(..., min_length=1)
class LocalFileOperation(BaseModel):
"""Delete or rename a file/directory in the local upload staging area."""
operation: str = Field(..., pattern="^(delete|rename)$")
path: str = Field(..., min_length=1)
new_name: Optional[str] = None # required for rename
class LocalFilePreview(BaseModel):
"""Preview file content in the local upload staging area."""
path: str = Field(..., min_length=1)
class ValidateSourcePath(BaseModel):
"""Validate a local directory path on the Nexus server as a push source."""
path: str = Field(..., min_length=1)
class SyncDiagnose(BaseModel):
"""Diagnose why a push failed for a specific server."""
server_id: int = Field(..., ge=1)
target_path: Optional[str] = None
class FileSyncDiff(BaseModel):
"""Compare a local file with its remote counterpart to show push diff."""
server_id: int = Field(..., ge=1)
source_path: str = Field(..., min_length=1)
relative_path: str = Field(..., min_length=1)
target_path: Optional[str] = None
class SyncCancel(BaseModel):
"""Cancel an in-progress push by batch_id."""
batch_id: str = Field(..., min_length=1, max_length=32)
class SyncVerify(BaseModel):
"""Post-push verification: compare local source with remote target via md5sum."""
server_ids: List[int] = Field(..., min_length=1)
source_path: str = Field(..., min_length=1)
target_path: Optional[str] = None
max_files: int = Field(200, ge=1, le=500)
class FileOperation(BaseModel):
"""Remote file operation via SSH exec (delete / rename / mkdir)."""
server_id: int = Field(..., ge=1)
operation: str = Field(..., pattern="^(delete|rename|mkdir)$")
path: str = Field(..., min_length=1) # source path (or new dir path for mkdir)
new_path: Optional[str] = None # required for rename
# ── Script ──
class ScriptCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
category: str = Field("ops", pattern="^(ops|deploy|check|cleanup)$")
content: str = Field(..., min_length=1)
description: Optional[str] = None
created_by: Optional[str] = None
class ScriptUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=100)
category: Optional[str] = Field(None, pattern="^(ops|deploy|check|cleanup)$")
content: Optional[str] = None
description: Optional[str] = None
class ScriptExecute(BaseModel):
script_id: Optional[int] = None
command: str = Field(..., min_length=1)
server_ids: List[int] = Field(..., min_length=1)
credential_id: Optional[int] = None
timeout: int = Field(60, ge=1, le=600)
long_task: bool = False
class ScriptExecutionRetry(BaseModel):
server_ids: Optional[List[int]] = None
credential_id: Optional[int] = None
long_task: Optional[bool] = None
timeout: int = Field(120, ge=1, le=600)
class ScriptExecutionMarkStuck(BaseModel):
detail: str = Field("", max_length=500)
class AgentScriptCallback(BaseModel):
"""Child host reports long-running script completion (curl from nohup wrapper)."""
job_id: str = Field(..., min_length=8, max_length=64)
server_id: int = Field(..., ge=1)
secret: str = Field(..., min_length=16, max_length=128)
exit_code: int = 0
message: str = Field("", max_length=500)
log_tail: Optional[str] = Field(None, max_length=2000)
# ── DB Credential ──
class DbCredentialCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
db_type: str = Field("mysql", pattern="^(mysql|postgresql|mariadb|mongodb|redis)$")
host: str = Field(..., min_length=1)
port: int = Field(3306, ge=1, le=65535)
username: str = Field(..., min_length=1)
password: str = Field(..., min_length=1)
database: Optional[str] = None
class DbCredentialUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=100)
db_type: Optional[str] = Field(None, pattern="^(mysql|postgresql|mariadb|mongodb|redis)$")
host: Optional[str] = Field(None, min_length=1)
port: Optional[int] = Field(None, ge=1, le=65535)
username: Optional[str] = Field(None, min_length=1)
password: Optional[str] = Field(None, min_length=1, description="留空则不修改密码")
database: Optional[str] = None
# ── Schedule ──
class ScheduleCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
server_ids: str = Field(..., min_length=3) # JSON array string
run_mode: str = Field("once", pattern="^(once|cron)$")
cron_expr: Optional[str] = None # required when run_mode=cron
fire_at: Optional[str] = None # ISO datetime, required when run_mode=once
enabled: bool = True
# Schedule type
schedule_type: str = Field("push", pattern="^(push|script)$")
# Push-specific (required when schedule_type == 'push')
source_path: Optional[str] = None
sync_mode: str = Field("incremental", pattern="^(incremental|full|overwrite|checksum)$")
# Script-specific (required when schedule_type == 'script')
script_id: Optional[int] = None
script_content: Optional[str] = None
exec_timeout: int = Field(60, ge=10, le=600)
long_task: bool = False
@model_validator(mode="after")
def _validate_schedule_fields(self):
if self.run_mode == "cron" and not self.cron_expr:
raise ValueError("cron 模式必须提供 cron_expr")
if self.run_mode == "once" and not self.fire_at:
raise ValueError("一次性模式必须提供 fire_at 执行时间")
if self.schedule_type == "script" and not self.script_id and not self.script_content:
raise ValueError("script 类型调度必须提供 script_id 或 script_content")
if self.schedule_type == "push" and not self.source_path:
raise ValueError("push 类型调度必须提供 source_path")
return self
class ScheduleUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=100)
source_path: Optional[str] = None
server_ids: Optional[str] = None
run_mode: Optional[str] = Field(None, pattern="^(once|cron)$")
cron_expr: Optional[str] = None
fire_at: Optional[str] = None
sync_mode: Optional[str] = Field(None, pattern="^(incremental|full|overwrite|checksum)$")
enabled: Optional[bool] = None
schedule_type: Optional[str] = Field(None, pattern="^(push|script)$")
script_id: Optional[int] = None
script_content: Optional[str] = None
exec_timeout: Optional[int] = Field(None, ge=10, le=600)
long_task: Optional[bool] = None
# ── Preset ──
class PresetCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
encrypted_pw: str = Field(..., min_length=1)
class PresetUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=100)
encrypted_pw: Optional[str] = Field(None, min_length=1, description="留空则不修改密码")
# ── SSH Key Preset ──
class SshKeyPresetCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
private_key: str = Field(..., min_length=1)
public_key: Optional[str] = None
class SshKeyPresetUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=100)
private_key: Optional[str] = Field(None, min_length=1, description="留空则不修改私钥")
public_key: Optional[str] = None
# ── Platform / Node ──
class PlatformCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
category: str = Field(..., min_length=1, max_length=50)
type: str = Field(..., min_length=1, max_length=50)
default_protocols: Optional[list] = None
charset: str = "utf-8"
class PlatformUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=100)
category: Optional[str] = None
type: Optional[str] = None
default_protocols: Optional[list] = None
charset: Optional[str] = None
class NodeCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
parent_id: Optional[int] = None
sort_order: int = 0
class NodeUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=100)
parent_id: Optional[int] = None
sort_order: Optional[int] = None