2026-05-21 23:18:49 +08:00
|
|
|
|
"""Nexus — Pydantic Request/Response Models
|
|
|
|
|
|
Shared schemas for API validation, replacing raw `dict` parameters.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from typing import Optional, List
|
2026-06-01 19:54:18 +08:00
|
|
|
|
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
|
|
|
|
|
|
|
|
|
|
from server.api.schema_path_validators import (
|
|
|
|
|
|
coerce_nexus_host_abs_path,
|
|
|
|
|
|
coerce_optional_remote_abs_path,
|
|
|
|
|
|
coerce_remote_abs_path,
|
|
|
|
|
|
coerce_remote_abs_path_list,
|
|
|
|
|
|
coerce_remote_relative_path,
|
|
|
|
|
|
)
|
2026-05-21 23:18:49 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-05-21 23:38:01 +08:00
|
|
|
|
# ── Setting ──
|
|
|
|
|
|
|
|
|
|
|
|
class SettingUpdatePayload(BaseModel):
|
|
|
|
|
|
value: str = Field(..., min_length=0)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-25 22:49:57 +08:00
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-21 23:38:01 +08:00
|
|
|
|
# ── Agent ──
|
|
|
|
|
|
|
|
|
|
|
|
class AgentHeartbeat(BaseModel):
|
2026-05-30 18:20:14 +08:00
|
|
|
|
server_id: Optional[int] = None # None = unregistered agent, silently discard
|
2026-05-21 23:38:01 +08:00
|
|
|
|
is_online: bool = True
|
|
|
|
|
|
system_info: Optional[dict] = None
|
|
|
|
|
|
agent_version: Optional[str] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-21 23:18:49 +08:00
|
|
|
|
# ── 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
|
2026-05-25 22:49:57 +08:00
|
|
|
|
preset_id: Optional[int] = None
|
2026-05-21 23:18:49 +08:00
|
|
|
|
ssh_key_path: Optional[str] = None
|
|
|
|
|
|
ssh_key_private: Optional[str] = None
|
|
|
|
|
|
ssh_key_public: Optional[str] = None
|
2026-05-25 22:49:57 +08:00
|
|
|
|
ssh_key_preset_id: Optional[int] = None
|
2026-05-21 23:18:49 +08:00
|
|
|
|
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
|
2026-06-01 19:54:18 +08:00
|
|
|
|
files_elevation: Optional[str] = Field(
|
|
|
|
|
|
default=None,
|
|
|
|
|
|
pattern="^(off|auto_sudo|always_sudo)$",
|
|
|
|
|
|
description="File manager sudo policy (stored in extra_attrs)",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("target_path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_target_path(cls, v: object) -> object:
|
|
|
|
|
|
return coerce_optional_remote_abs_path(v if v is None else str(v))
|
2026-05-21 23:18:49 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-05-25 22:49:57 +08:00
|
|
|
|
preset_id: Optional[int] = None
|
2026-05-21 23:18:49 +08:00
|
|
|
|
ssh_key_path: Optional[str] = None
|
|
|
|
|
|
ssh_key_private: Optional[str] = None
|
|
|
|
|
|
ssh_key_public: Optional[str] = None
|
2026-05-25 22:49:57 +08:00
|
|
|
|
ssh_key_preset_id: Optional[int] = None
|
2026-05-21 23:18:49 +08:00
|
|
|
|
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
|
2026-06-01 19:54:18 +08:00
|
|
|
|
files_elevation: Optional[str] = Field(
|
|
|
|
|
|
default=None,
|
|
|
|
|
|
pattern="^(off|auto_sudo|always_sudo)$",
|
|
|
|
|
|
description="File manager sudo policy (stored in extra_attrs)",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("target_path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_target_path(cls, v: object) -> object:
|
|
|
|
|
|
if v is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
return coerce_optional_remote_abs_path(str(v))
|
2026-05-21 23:18:49 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ServerCheck(BaseModel):
|
2026-06-06 23:57:46 +08:00
|
|
|
|
server_ids: List[int] = Field(..., min_length=1, max_length=50)
|
2026-05-21 23:18:49 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-05-26 00:17:43 +08:00
|
|
|
|
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] = []
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-08 04:30:24 +08:00
|
|
|
|
class BatchCategoryUpdate(BaseModel):
|
|
|
|
|
|
"""Batch update server category for selected IDs."""
|
|
|
|
|
|
server_ids: List[int] = Field(..., min_length=1, max_length=200)
|
|
|
|
|
|
category: str = Field("", max_length=100, description="分类名称;留空表示清除分类")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BatchCategoryResult(BaseModel):
|
|
|
|
|
|
updated: int = 0
|
|
|
|
|
|
not_found: List[int] = []
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-08 11:17:21 +08:00
|
|
|
|
class BatchJobStarted(BaseModel):
|
|
|
|
|
|
"""Immediate response when a server batch job is queued."""
|
|
|
|
|
|
job_id: int
|
|
|
|
|
|
status: str = "running"
|
|
|
|
|
|
label: str
|
|
|
|
|
|
op: str
|
|
|
|
|
|
total: int = 0
|
|
|
|
|
|
done: int = 0
|
|
|
|
|
|
remaining: int = 0
|
|
|
|
|
|
success: int = 0
|
|
|
|
|
|
failed: int = 0
|
|
|
|
|
|
progress: str = "0/0"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BatchJobStatus(BaseModel):
|
|
|
|
|
|
job_id: int
|
|
|
|
|
|
op: str
|
|
|
|
|
|
label: str
|
|
|
|
|
|
status: str
|
|
|
|
|
|
total: int = 0
|
|
|
|
|
|
done: int = 0
|
|
|
|
|
|
remaining: int = 0
|
|
|
|
|
|
success: int = 0
|
|
|
|
|
|
failed: int = 0
|
|
|
|
|
|
progress: str = "0/0"
|
|
|
|
|
|
operator: str = ""
|
|
|
|
|
|
results: List[BatchAgentResultItem] = []
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-26 00:17:43 +08:00
|
|
|
|
class FileUpload(BaseModel):
|
|
|
|
|
|
"""Upload file to remote server via SFTP."""
|
|
|
|
|
|
server_id: int = Field(..., ge=1)
|
|
|
|
|
|
remote_path: str = Field(..., min_length=1)
|
|
|
|
|
|
|
2026-06-01 19:54:18 +08:00
|
|
|
|
@field_validator("remote_path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_remote_path(cls, v: object) -> str:
|
|
|
|
|
|
return coerce_remote_abs_path(str(v))
|
|
|
|
|
|
|
2026-05-26 00:17:43 +08:00
|
|
|
|
|
2026-05-21 23:18:49 +08:00
|
|
|
|
# ── Sync ──
|
|
|
|
|
|
|
|
|
|
|
|
class SyncFiles(BaseModel):
|
2026-06-06 23:57:46 +08:00
|
|
|
|
server_ids: List[int] = Field(..., min_length=1, max_length=2000)
|
2026-05-21 23:18:49 +08:00
|
|
|
|
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)
|
2026-06-04 14:01:14 +08:00
|
|
|
|
batch_id: Optional[str] = Field(
|
|
|
|
|
|
None,
|
|
|
|
|
|
min_length=12,
|
|
|
|
|
|
max_length=12,
|
|
|
|
|
|
description="Client-supplied push batch id (12 hex); must connect /ws/sync before POST",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("batch_id", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_batch_id(cls, v: object) -> object:
|
|
|
|
|
|
if v is None or v == "":
|
|
|
|
|
|
return None
|
|
|
|
|
|
s = str(v).strip().lower()
|
|
|
|
|
|
if len(s) != 12 or not all(c in "0123456789abcdef" for c in s):
|
|
|
|
|
|
raise ValueError("batch_id must be 12 hexadecimal characters")
|
|
|
|
|
|
return s
|
2026-05-21 23:18:49 +08:00
|
|
|
|
|
2026-06-01 19:54:18 +08:00
|
|
|
|
@field_validator("source_path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_source_path(cls, v: object) -> str:
|
|
|
|
|
|
return coerce_nexus_host_abs_path(str(v))
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("target_path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_target_path(cls, v: object) -> object:
|
|
|
|
|
|
return coerce_optional_remote_abs_path(v if v is None else str(v))
|
|
|
|
|
|
|
2026-05-21 23:18:49 +08:00
|
|
|
|
|
2026-05-23 16:41:47 +08:00
|
|
|
|
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)
|
|
|
|
|
|
|
2026-06-01 19:54:18 +08:00
|
|
|
|
@field_validator("source_path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_source_path(cls, v: object) -> str:
|
|
|
|
|
|
return coerce_nexus_host_abs_path(str(v))
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("target_path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_target_path(cls, v: object) -> object:
|
|
|
|
|
|
return coerce_optional_remote_abs_path(v if v is None else str(v))
|
|
|
|
|
|
|
2026-05-23 16:41:47 +08:00
|
|
|
|
|
2026-05-21 23:18:49 +08:00
|
|
|
|
class SyncBrowse(BaseModel):
|
|
|
|
|
|
server_id: int
|
|
|
|
|
|
path: str = Field("/", min_length=1)
|
|
|
|
|
|
|
2026-06-01 19:54:18 +08:00
|
|
|
|
@field_validator("path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_path(cls, v: object) -> str:
|
|
|
|
|
|
return coerce_remote_abs_path(str(v))
|
|
|
|
|
|
|
2026-05-21 23:18:49 +08:00
|
|
|
|
|
2026-05-28 21:01:27 +08:00
|
|
|
|
class SyncBrowseLocal(BaseModel):
|
|
|
|
|
|
"""Browse a local directory on the Nexus server (for upload file manager)."""
|
|
|
|
|
|
path: str = Field(..., min_length=1)
|
|
|
|
|
|
|
2026-06-01 19:54:18 +08:00
|
|
|
|
@field_validator("path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_path(cls, v: object) -> str:
|
|
|
|
|
|
return coerce_nexus_host_abs_path(str(v))
|
|
|
|
|
|
|
2026-05-28 21:01:27 +08:00
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
2026-06-01 19:54:18 +08:00
|
|
|
|
@field_validator("path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_path(cls, v: object) -> str:
|
|
|
|
|
|
return coerce_nexus_host_abs_path(str(v))
|
|
|
|
|
|
|
2026-05-28 21:01:27 +08:00
|
|
|
|
|
2026-05-29 15:59:30 +08:00
|
|
|
|
class LocalFilePreview(BaseModel):
|
|
|
|
|
|
"""Preview file content in the local upload staging area."""
|
|
|
|
|
|
path: str = Field(..., min_length=1)
|
|
|
|
|
|
|
2026-06-01 19:54:18 +08:00
|
|
|
|
@field_validator("path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_path(cls, v: object) -> str:
|
|
|
|
|
|
return coerce_nexus_host_abs_path(str(v))
|
|
|
|
|
|
|
2026-05-29 15:59:30 +08:00
|
|
|
|
|
2026-05-29 21:29:41 +08:00
|
|
|
|
class ValidateSourcePath(BaseModel):
|
|
|
|
|
|
"""Validate a local directory path on the Nexus server as a push source."""
|
|
|
|
|
|
path: str = Field(..., min_length=1)
|
|
|
|
|
|
|
2026-06-01 19:54:18 +08:00
|
|
|
|
@field_validator("path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_path(cls, v: object) -> str:
|
|
|
|
|
|
return coerce_nexus_host_abs_path(str(v))
|
|
|
|
|
|
|
2026-05-29 21:29:41 +08:00
|
|
|
|
|
|
|
|
|
|
class SyncDiagnose(BaseModel):
|
|
|
|
|
|
"""Diagnose why a push failed for a specific server."""
|
|
|
|
|
|
server_id: int = Field(..., ge=1)
|
|
|
|
|
|
target_path: Optional[str] = None
|
|
|
|
|
|
|
2026-06-01 19:54:18 +08:00
|
|
|
|
@field_validator("target_path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_target_path(cls, v: object) -> object:
|
|
|
|
|
|
return coerce_optional_remote_abs_path(v if v is None else str(v))
|
|
|
|
|
|
|
2026-05-29 21:29:41 +08:00
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
2026-06-01 19:54:18 +08:00
|
|
|
|
@field_validator("source_path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_source_path(cls, v: object) -> str:
|
|
|
|
|
|
return coerce_nexus_host_abs_path(str(v))
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("relative_path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_relative_path(cls, v: object) -> str:
|
|
|
|
|
|
return coerce_remote_relative_path(str(v))
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("target_path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_target_path(cls, v: object) -> object:
|
|
|
|
|
|
return coerce_optional_remote_abs_path(v if v is None else str(v))
|
|
|
|
|
|
|
2026-05-29 21:29:41 +08:00
|
|
|
|
|
|
|
|
|
|
class SyncCancel(BaseModel):
|
|
|
|
|
|
"""Cancel an in-progress push by batch_id."""
|
|
|
|
|
|
batch_id: str = Field(..., min_length=1, max_length=32)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-02 08:14:09 +08:00
|
|
|
|
class ReconcileStaleLogs(BaseModel):
|
|
|
|
|
|
"""Mark long-stuck sync_logs rows (status=running) as failed."""
|
|
|
|
|
|
max_age_minutes: int = Field(120, ge=5, le=10080)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-28 21:01:27 +08:00
|
|
|
|
class SyncVerify(BaseModel):
|
|
|
|
|
|
"""Post-push verification: compare local source with remote target via md5sum."""
|
2026-06-06 23:57:46 +08:00
|
|
|
|
server_ids: List[int] = Field(..., min_length=1, max_length=2000)
|
2026-05-28 21:01:27 +08:00
|
|
|
|
source_path: str = Field(..., min_length=1)
|
|
|
|
|
|
target_path: Optional[str] = None
|
|
|
|
|
|
max_files: int = Field(200, ge=1, le=500)
|
|
|
|
|
|
|
2026-06-01 19:54:18 +08:00
|
|
|
|
@field_validator("source_path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_source_path(cls, v: object) -> str:
|
|
|
|
|
|
return coerce_nexus_host_abs_path(str(v))
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("target_path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_target_path(cls, v: object) -> object:
|
|
|
|
|
|
return coerce_optional_remote_abs_path(v if v is None else str(v))
|
|
|
|
|
|
|
2026-05-28 21:01:27 +08:00
|
|
|
|
|
2026-05-23 16:46:00 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-06-01 19:54:18 +08:00
|
|
|
|
@field_validator("path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_path(cls, v: object) -> str:
|
|
|
|
|
|
return coerce_remote_abs_path(str(v))
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("new_path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_new_path(cls, v: object) -> object:
|
|
|
|
|
|
if v is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
return coerce_remote_abs_path(str(v))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FileClipboardApply(BaseModel):
|
|
|
|
|
|
"""Batch copy or move (cut+paste) remote paths into a destination directory."""
|
|
|
|
|
|
server_id: int = Field(..., ge=1)
|
|
|
|
|
|
mode: str = Field(..., pattern="^(copy|move)$")
|
|
|
|
|
|
sources: list[str] = Field(..., min_length=1, max_length=50)
|
|
|
|
|
|
dest_dir: str = Field(..., min_length=1, max_length=4096)
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("sources", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_sources(cls, v: object) -> list[str]:
|
|
|
|
|
|
if not isinstance(v, list):
|
|
|
|
|
|
raise ValueError("sources 必须为路径列表")
|
|
|
|
|
|
return coerce_remote_abs_path_list([str(x) for x in v])
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("dest_dir", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_dest_dir(cls, v: object) -> str:
|
|
|
|
|
|
return coerce_remote_abs_path(str(v))
|
|
|
|
|
|
|
2026-05-23 16:46:00 +08:00
|
|
|
|
|
2026-05-30 23:34:24 +08:00
|
|
|
|
class FileDownload(BaseModel):
|
|
|
|
|
|
"""Download a file from a remote server."""
|
|
|
|
|
|
server_id: int = Field(..., ge=1)
|
2026-05-30 23:44:35 +08:00
|
|
|
|
path: str = Field(..., min_length=1, max_length=4096)
|
2026-05-30 23:34:24 +08:00
|
|
|
|
|
2026-06-01 19:54:18 +08:00
|
|
|
|
@field_validator("path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_path(cls, v: object) -> str:
|
|
|
|
|
|
return coerce_remote_abs_path(str(v))
|
|
|
|
|
|
|
2026-05-30 23:34:24 +08:00
|
|
|
|
|
|
|
|
|
|
class FileRead(BaseModel):
|
|
|
|
|
|
"""Read text file content from a remote server."""
|
|
|
|
|
|
server_id: int = Field(..., ge=1)
|
2026-05-30 23:44:35 +08:00
|
|
|
|
path: str = Field(..., min_length=1, max_length=4096)
|
2026-05-30 23:34:24 +08:00
|
|
|
|
max_size: int = Field(1_000_000, ge=1, le=5_000_000) # 1MB default, 5MB max
|
|
|
|
|
|
|
2026-06-01 19:54:18 +08:00
|
|
|
|
@field_validator("path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_path(cls, v: object) -> str:
|
|
|
|
|
|
return coerce_remote_abs_path(str(v))
|
|
|
|
|
|
|
2026-05-30 23:34:24 +08:00
|
|
|
|
|
|
|
|
|
|
class FileWrite(BaseModel):
|
|
|
|
|
|
"""Write text content to a file on a remote server."""
|
|
|
|
|
|
server_id: int = Field(..., ge=1)
|
2026-05-30 23:44:35 +08:00
|
|
|
|
path: str = Field(..., min_length=1, max_length=4096)
|
2026-05-30 23:34:24 +08:00
|
|
|
|
content: str = Field(..., max_length=5_000_000)
|
2026-06-02 14:46:49 +08:00
|
|
|
|
etag: str | None = Field(None, max_length=64)
|
2026-05-30 23:34:24 +08:00
|
|
|
|
|
2026-06-01 19:54:18 +08:00
|
|
|
|
@field_validator("path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_path(cls, v: object) -> str:
|
|
|
|
|
|
return coerce_remote_abs_path(str(v))
|
|
|
|
|
|
|
2026-05-30 23:34:24 +08:00
|
|
|
|
|
2026-05-31 02:55:45 +08:00
|
|
|
|
class FileChmod(BaseModel):
|
|
|
|
|
|
"""Change file permissions on a remote server."""
|
|
|
|
|
|
server_id: int = Field(..., ge=1)
|
|
|
|
|
|
path: str = Field(..., min_length=1, max_length=4096)
|
|
|
|
|
|
mode: str = Field(..., min_length=3, max_length=4, pattern=r"^[0-7]{3,4}$")
|
2026-06-01 19:54:18 +08:00
|
|
|
|
owner: Optional[str] = Field(None, max_length=64, pattern=r"^[a-zA-Z0-9_.-]+$")
|
|
|
|
|
|
group: Optional[str] = Field(None, max_length=64, pattern=r"^[a-zA-Z0-9_.-]+$")
|
|
|
|
|
|
recursive: bool = False
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_path(cls, v: object) -> str:
|
|
|
|
|
|
return coerce_remote_abs_path(str(v))
|
|
|
|
|
|
|
|
|
|
|
|
@model_validator(mode="after")
|
|
|
|
|
|
def _group_requires_owner(self):
|
|
|
|
|
|
if self.group and not self.owner:
|
|
|
|
|
|
raise ValueError("指定属组时必须同时指定属主")
|
|
|
|
|
|
return self
|
2026-05-31 02:55:45 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FileCompress(BaseModel):
|
|
|
|
|
|
"""Compress files into an archive on a remote server."""
|
|
|
|
|
|
server_id: int = Field(..., ge=1)
|
|
|
|
|
|
paths: list[str] = Field(..., min_length=1, max_length=100)
|
|
|
|
|
|
dest: str = Field(..., min_length=1, max_length=4096)
|
|
|
|
|
|
format: str = Field("tar.gz", pattern=r"^(tar\.gz|zip)$")
|
|
|
|
|
|
|
2026-06-01 19:54:18 +08:00
|
|
|
|
@field_validator("paths", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_paths(cls, v: object) -> list[str]:
|
|
|
|
|
|
if not isinstance(v, list):
|
|
|
|
|
|
raise ValueError("paths 必须为路径列表")
|
|
|
|
|
|
return coerce_remote_abs_path_list([str(x) for x in v])
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("dest", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_dest(cls, v: object) -> str:
|
|
|
|
|
|
return coerce_remote_abs_path(str(v))
|
|
|
|
|
|
|
2026-05-31 02:55:45 +08:00
|
|
|
|
|
|
|
|
|
|
class FileDecompress(BaseModel):
|
|
|
|
|
|
"""Decompress an archive on a remote server."""
|
|
|
|
|
|
server_id: int = Field(..., ge=1)
|
|
|
|
|
|
path: str = Field(..., min_length=1, max_length=4096)
|
2026-06-01 19:54:18 +08:00
|
|
|
|
dest: str = Field(..., min_length=1, max_length=4096)
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("path", "dest", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_paths(cls, v: object) -> str:
|
|
|
|
|
|
text = str(v).strip()
|
|
|
|
|
|
if text == ".":
|
|
|
|
|
|
raise ValueError("解压目标必须为绝对路径(不可使用 '.')")
|
|
|
|
|
|
return coerce_remote_abs_path(text)
|
2026-05-31 02:55:45 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-05-21 23:18:49 +08:00
|
|
|
|
# ── 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):
|
2026-06-08 02:39:18 +08:00
|
|
|
|
script_id: Optional[int] = Field(None, ge=1)
|
|
|
|
|
|
command: Optional[str] = Field(None, min_length=1)
|
2026-06-06 23:57:46 +08:00
|
|
|
|
server_ids: List[int] = Field(..., min_length=1, max_length=2000)
|
2026-05-23 15:26:56 +08:00
|
|
|
|
credential_id: Optional[int] = None
|
2026-05-21 23:18:49 +08:00
|
|
|
|
timeout: int = Field(60, ge=1, le=600)
|
2026-05-23 15:26:56 +08:00
|
|
|
|
long_task: bool = False
|
|
|
|
|
|
|
2026-06-08 02:39:18 +08:00
|
|
|
|
@model_validator(mode="after")
|
|
|
|
|
|
def require_command_or_script(self) -> "ScriptExecute":
|
|
|
|
|
|
cmd = (self.command or "").strip()
|
|
|
|
|
|
if cmd:
|
|
|
|
|
|
self.command = cmd
|
|
|
|
|
|
elif not self.script_id:
|
|
|
|
|
|
raise ValueError("须提供 command 或 script_id")
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
2026-05-23 15:26:56 +08:00
|
|
|
|
|
|
|
|
|
|
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)
|
2026-05-21 23:18:49 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-05-21 23:38:01 +08:00
|
|
|
|
# ── 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
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-25 22:49:57 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-21 23:18:49 +08:00
|
|
|
|
# ── Schedule ──
|
|
|
|
|
|
|
|
|
|
|
|
class ScheduleCreate(BaseModel):
|
|
|
|
|
|
name: str = Field(..., min_length=1, max_length=100)
|
2026-05-23 17:29:16 +08:00
|
|
|
|
server_ids: str = Field(..., min_length=3) # JSON array string
|
2026-05-24 16:26:40 +08:00
|
|
|
|
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
|
2026-05-21 23:18:49 +08:00
|
|
|
|
enabled: bool = True
|
2026-05-23 17:29:16 +08:00
|
|
|
|
# Schedule type
|
|
|
|
|
|
schedule_type: str = Field("push", pattern="^(push|script)$")
|
|
|
|
|
|
# Push-specific (required when schedule_type == 'push')
|
|
|
|
|
|
source_path: Optional[str] = None
|
2026-06-08 02:39:18 +08:00
|
|
|
|
target_path: Optional[str] = None
|
2026-05-23 17:29:16 +08:00
|
|
|
|
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
|
2026-05-21 23:18:49 +08:00
|
|
|
|
|
2026-05-24 16:26:40 +08:00
|
|
|
|
@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
|
|
|
|
|
|
|
2026-06-01 19:54:18 +08:00
|
|
|
|
@field_validator("source_path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_source_path(cls, v: object) -> object:
|
|
|
|
|
|
if v is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
text = str(v).strip()
|
|
|
|
|
|
if not text:
|
|
|
|
|
|
return None
|
|
|
|
|
|
return coerce_nexus_host_abs_path(text)
|
|
|
|
|
|
|
2026-06-08 02:39:18 +08:00
|
|
|
|
@field_validator("target_path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_target_path(cls, v: object) -> object:
|
|
|
|
|
|
return coerce_optional_remote_abs_path(v if v is None else str(v))
|
|
|
|
|
|
|
2026-05-21 23:18:49 +08:00
|
|
|
|
|
|
|
|
|
|
class ScheduleUpdate(BaseModel):
|
|
|
|
|
|
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
|
|
|
|
|
source_path: Optional[str] = None
|
2026-06-08 02:39:18 +08:00
|
|
|
|
target_path: Optional[str] = None
|
2026-05-21 23:18:49 +08:00
|
|
|
|
server_ids: Optional[str] = None
|
2026-05-24 16:26:40 +08:00
|
|
|
|
run_mode: Optional[str] = Field(None, pattern="^(once|cron)$")
|
2026-05-21 23:18:49 +08:00
|
|
|
|
cron_expr: Optional[str] = None
|
2026-05-24 16:26:40 +08:00
|
|
|
|
fire_at: Optional[str] = None
|
2026-05-22 11:01:17 +08:00
|
|
|
|
sync_mode: Optional[str] = Field(None, pattern="^(incremental|full|overwrite|checksum)$")
|
2026-05-21 23:18:49 +08:00
|
|
|
|
enabled: Optional[bool] = None
|
2026-05-23 17:29:16 +08:00
|
|
|
|
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
|
2026-05-21 23:18:49 +08:00
|
|
|
|
|
2026-06-01 19:54:18 +08:00
|
|
|
|
@field_validator("source_path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_source_path(cls, v: object) -> object:
|
|
|
|
|
|
if v is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
text = str(v).strip()
|
|
|
|
|
|
if not text:
|
|
|
|
|
|
return None
|
|
|
|
|
|
return coerce_nexus_host_abs_path(text)
|
|
|
|
|
|
|
2026-06-08 02:39:18 +08:00
|
|
|
|
@field_validator("target_path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_target_path(cls, v: object) -> object:
|
|
|
|
|
|
return coerce_optional_remote_abs_path(v if v is None else str(v))
|
|
|
|
|
|
|
2026-05-21 23:18:49 +08:00
|
|
|
|
|
|
|
|
|
|
# ── Preset ──
|
|
|
|
|
|
|
|
|
|
|
|
class PresetCreate(BaseModel):
|
|
|
|
|
|
name: str = Field(..., min_length=1, max_length=100)
|
2026-06-06 20:08:48 +08:00
|
|
|
|
username: str = Field("root", min_length=1, max_length=100)
|
2026-05-21 23:18:49 +08:00
|
|
|
|
encrypted_pw: str = Field(..., min_length=1)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-25 22:49:57 +08:00
|
|
|
|
class PresetUpdate(BaseModel):
|
|
|
|
|
|
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
2026-06-06 20:08:48 +08:00
|
|
|
|
username: Optional[str] = Field(None, min_length=1, max_length=100)
|
2026-05-25 22:49:57 +08:00
|
|
|
|
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)
|
2026-06-06 20:08:48 +08:00
|
|
|
|
username: str = Field("root", min_length=1, max_length=100)
|
2026-05-25 22:49:57 +08:00
|
|
|
|
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)
|
2026-06-06 20:08:48 +08:00
|
|
|
|
username: Optional[str] = Field(None, min_length=1, max_length=100)
|
2026-05-25 22:49:57 +08:00
|
|
|
|
private_key: Optional[str] = Field(None, min_length=1, description="留空则不修改私钥")
|
|
|
|
|
|
public_key: Optional[str] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-06 20:08:48 +08:00
|
|
|
|
class AddByIpRequest(BaseModel):
|
|
|
|
|
|
domain: str = Field(..., min_length=1, max_length=255, description="IP 或域名")
|
|
|
|
|
|
port: int = Field(22, ge=1, le=65535)
|
|
|
|
|
|
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
|
|
|
|
|
agent_port: int = Field(8601, ge=1, le=65535)
|
|
|
|
|
|
target_path: Optional[str] = Field("", description="推送目标路径,可后续编辑")
|
|
|
|
|
|
category: Optional[str] = None
|
|
|
|
|
|
platform_id: Optional[int] = None
|
|
|
|
|
|
node_id: Optional[int] = None
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("target_path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_target_path_add(cls, v: object) -> object:
|
|
|
|
|
|
return coerce_optional_remote_abs_path(v if v is None else str(v))
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-08 03:15:40 +08:00
|
|
|
|
class AddByIpsBatchRequest(BaseModel):
|
2026-06-08 04:30:24 +08:00
|
|
|
|
"""批量按 IP 添加:每行一个地址(可选 ``名称<TAB>IP``),凭据预设轮询 SSH,失败入 pending。"""
|
|
|
|
|
|
text: str = Field(..., min_length=1, max_length=50_000, description="每行一个 IP/域名,或 名称<TAB>IP")
|
2026-06-08 03:15:40 +08:00
|
|
|
|
port: int = Field(22, ge=1, le=65535)
|
|
|
|
|
|
agent_port: int = Field(8601, ge=1, le=65535)
|
|
|
|
|
|
target_path: Optional[str] = Field("", description="推送目标路径,可后续编辑")
|
|
|
|
|
|
category: Optional[str] = None
|
|
|
|
|
|
platform_id: Optional[int] = None
|
|
|
|
|
|
node_id: Optional[int] = None
|
|
|
|
|
|
|
|
|
|
|
|
@field_validator("target_path", mode="before")
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def _normalize_target_path_batch(cls, v: object) -> object:
|
|
|
|
|
|
return coerce_optional_remote_abs_path(v if v is None else str(v))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CredentialPollErrorOut(BaseModel):
|
|
|
|
|
|
preset_type: str
|
|
|
|
|
|
preset_name: str
|
|
|
|
|
|
username: str
|
|
|
|
|
|
error: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AddByIpsBatchItemResult(BaseModel):
|
|
|
|
|
|
domain: str
|
|
|
|
|
|
status: str = Field(..., description="created | pending | skipped")
|
|
|
|
|
|
server_id: Optional[int] = None
|
|
|
|
|
|
pending_id: Optional[int] = None
|
|
|
|
|
|
message: Optional[str] = None
|
|
|
|
|
|
matched_preset: Optional[str] = None
|
|
|
|
|
|
matched_username: Optional[str] = None
|
|
|
|
|
|
errors: Optional[List[CredentialPollErrorOut]] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AddByIpsBatchResult(BaseModel):
|
|
|
|
|
|
total: int = 0
|
|
|
|
|
|
created: int = 0
|
|
|
|
|
|
pending: int = 0
|
|
|
|
|
|
skipped: int = 0
|
|
|
|
|
|
input_lines: int = Field(0, description="非空输入行数(去重前)")
|
|
|
|
|
|
duplicates_removed: int = Field(0, description="输入中重复行已忽略的数量")
|
|
|
|
|
|
items: List[AddByIpsBatchItemResult] = []
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-21 23:18:49 +08:00
|
|
|
|
# ── 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
|