Files
system-backu/backend/app/schemas/job.py
T
publ 07b2016f33 feat: ETCD 备份 + 邮件/飞书通知
ETCD 备份(新增类型):
- 后端 core/backup/etcd.py:调用 etcdctl snapshot save(v3.5.17 二进制)
  生成快照文件,与 META.txt 一起 tar.gz 流式打包上传。
- 配置支持 endpoints(逗号分隔多节点)、basic auth、TLS(cacert/cert/key)。
- Dockerfile 下载 etcdctl 二进制(绕过 apt 签名问题)。

通知模块:
- 核心 app/core/notify.py:
  - 邮件:smtplib + MIMEMultipart(线程池里跑避免阻塞)
  - 飞书:httpx 调自定义机器人 webhook,发 interactive card
- settings API:
  - GET/PUT /api/settings/notifications(密码脱敏 *** 编辑占位)
  - POST /api/settings/notifications/test 测试通道
- Job 增加 notify_on 字段(none/failure/all),Alembic 0002 迁移。
- executor 在 run 完成后根据 job.notify_on 与 run.status 异步触发通知,
  重新加载最新 run 信息生成上下文。

前端:
- JobForm 加 ETCD 表单字段与 notify_on 选择器;type 选项加 etcd。
- JobList 显示通知策略徽标。
- Settings 页加通知配置卡片:邮件(折叠面板 + 测试按钮)+ 飞书 + 保存。
- 新增 api/settings.ts。
- 类型扩展:EtcdSourceConfig、NotificationsConfig、SmtpConfig。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-23 11:24:53 +08:00

137 lines
4.4 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.
from datetime import datetime
from typing import Literal, Optional
from pydantic import BaseModel, ConfigDict, Field, field_validator
from app.schemas.storage import StorageOut
class MySQLSourceConfig(BaseModel):
host: str = Field(..., min_length=1)
port: int = Field(3306, ge=1, le=65535)
user: str = Field(..., min_length=1)
password: str = Field("", description="可空(trust auth")
database: str = Field(..., min_length=1, description="要备份的库名")
use_ssl: bool = Field(False, description="True 启用 SSL/TLS;如服务器是自签名证书建议关闭")
extra_args: str = Field("", description="额外 mysqldump 参数,如 '--skip-lock-tables'")
class DirectorySourceConfig(BaseModel):
path: str = Field(..., min_length=1, description="要备份的目录绝对路径")
exclude_patterns: list[str] = Field(
default_factory=lambda: [".DS_Store", "__pycache__", "node_modules", ".git"]
)
class InfluxDBSourceConfigV1(BaseModel):
version: Literal["1"]
host: str = Field(..., min_length=1)
port: int = Field(8086, ge=1, le=65535)
database: str = Field(..., min_length=1)
username: str = Field("", description="留空表示无认证")
password: str = Field("")
class InfluxDBSourceConfigV2(BaseModel):
version: Literal["2"]
host: str = Field(..., min_length=1)
port: int = Field(8086, ge=1, le=65535)
org: str = Field(..., min_length=1)
bucket: str = Field(..., min_length=1)
token: str = Field(..., min_length=1)
class EtcdSourceConfig(BaseModel):
endpoints: str = Field(
...,
min_length=1,
description="ETCD endpoints,逗号分隔,如 http://10.0.0.1:2379,http://10.0.0.2:2379",
)
username: str = Field("", description="可选 basic auth 用户名")
password: str = Field("")
cacert: str = Field("", description="CA 证书路径(容器内)")
cert: str = Field("", description="客户端证书路径")
key: str = Field("", description="客户端私钥路径")
class JobBase(BaseModel):
name: str = Field(..., min_length=1, max_length=64)
type: Literal["mysql", "directory", "influxdb", "etcd"]
source_config: dict
storage_id: int
cron_expression: Optional[str] = Field(None, description="标准 5 段 cron 表达式,空表示手动")
enabled: bool = True
notify_on: Literal["none", "failure", "all"] = Field("none", description="何时发通知")
retention_count: int = Field(7, ge=1, le=365)
retention_days: Optional[int] = Field(None, ge=1, le=3650)
description: Optional[str] = Field(None, max_length=255)
@field_validator("cron_expression")
@classmethod
def _check_cron(cls, v: Optional[str]) -> Optional[str]:
if v is None or v.strip() == "":
return None
v = v.strip()
parts = v.split()
if len(parts) != 5:
raise ValueError("cron 必须是 5 段:分 时 日 月 周")
return v
class JobCreate(JobBase):
pass
class JobUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=64)
source_config: Optional[dict] = None
storage_id: Optional[int] = None
cron_expression: Optional[str] = None
enabled: Optional[bool] = None
notify_on: Optional[Literal["none", "failure", "all"]] = None
retention_count: Optional[int] = Field(None, ge=1, le=365)
retention_days: Optional[int] = Field(None, ge=1, le=3650)
description: Optional[str] = None
class JobOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
name: str
type: str
cron_expression: Optional[str]
enabled: bool
notify_on: str
retention_count: int
retention_days: Optional[int]
description: Optional[str]
storage: StorageOut
created_at: datetime
updated_at: datetime
# 解密后的数据源配置;敏感字段已脱敏为 "***"
source_config_safe: dict = Field(
default_factory=dict,
description="解密后的 source_config,敏感字段(password/token/secret_key)已脱敏",
)
class RunSummary(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
status: str
trigger: str
started_at: Optional[datetime]
finished_at: Optional[datetime]
duration_seconds: Optional[int]
artifact_size: Optional[int]
error_message: Optional[str]
class JobWithLastRun(JobOut):
last_run: Optional[RunSummary] = None
class JobRunResponse(BaseModel):
run_id: int
status: str