feat(deployer): sync 前备份远程服务目录
rsync --delete 会清掉目标目录里的多余文件;先拷到 $HOME/bakeup/deployer-skill,失败则中止同步。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -84,8 +84,9 @@ GitOps 我已经 clone 在 ../infra-gitops,用那个目录开 MR
|
||||
避免同一台机器上多个项目的同名环境互相覆盖;需要固定名字时在 `_config.yaml` 写 `name:`。
|
||||
|
||||
重要边界:rsync 同步使用 `--delete`,远程多余的文件会被删除;scp 兜底不会删。
|
||||
数据库、证书等运行时数据必须放在排除目录或远程绝对路径挂载。涉及删除数据卷、
|
||||
清理远程文件的操作会先向你确认。
|
||||
每次 sync 前会把远程服务目录拷到该机 `$HOME/bakeup/deployer-skill/<服务名>/<时间戳>/`,每服务只留最近 5 份;
|
||||
备份失败则中止同步。数据库、证书等运行时数据必须放在排除目录或远程绝对路径挂载。
|
||||
涉及删除数据卷、清理远程文件的操作会先向你确认。
|
||||
|
||||
## 如何判断完成
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ description: >-
|
||||
## 边界
|
||||
|
||||
- 所有 sync/up/recreate/upgrade/restart 必须按单服务执行,禁止节点级批量。
|
||||
- `rsync` 带 `--delete`:运行时数据必须在 `data/`、`_data/` 或远程绝对路径挂载,否则会被清掉。
|
||||
- `rsync` 带 `--delete`:sync 前先把远程服务目录拷到 `$HOME/bakeup/deployer-skill/<name>/<时间戳>/`(每服务留 5 份);运行时数据仍须在 `data/`、`_data/` 或远程绝对路径挂载,否则会被清掉。
|
||||
- 仓库只放静态配置;证书、数据库、上传文件不进 Git,也不进同步范围。`unused/` 不参与发现与部署。
|
||||
- `node` 是 `~/.ssh/config` 的 Host 别名(可用 `user@host`)。用户没给别名就不要写假 node。
|
||||
- 密钥不入库。Compose 优先放远程 `.env`;Argo CD 的 dockerconfigjson / TLS 私钥只存在集群 Secret。
|
||||
|
||||
@@ -92,7 +92,7 @@ python3 <skill-dir>/scripts/deploy/sync.py <service-path>
|
||||
|
||||
脚本一律 `python3 -I -S <skill-dir>/scripts/deploy/<name>.py`。有 `make deploy TGT=` 才用 Makefile;没有就直接调脚本,不要读 Makefile 去发明封装。
|
||||
|
||||
`sync.py` 使用 `rsync -avz --delete`;本机无 rsync 时 scp 兜底(不 `--delete`)。运行时数据必须放在默认排除的 `data/`、`_data/`,或 compose 挂载的远程绝对路径,否则会被清掉。
|
||||
`sync.py` 使用 `rsync -avz --delete`;本机无 rsync 时 scp 兜底(不 `--delete`)。同步前先把远程服务目录拷到该机 `$HOME/bakeup/deployer-skill/<服务名>/<时间戳>/`(每服务保留最近 5 份,备份失败则中止)。运行时数据必须放在默认排除的 `data/`、`_data/`,或 compose 挂载的远程绝对路径,否则会被清掉。
|
||||
|
||||
## 新增服务 / 环境
|
||||
|
||||
@@ -111,7 +111,7 @@ python3 <skill-dir>/scripts/deploy/sync.py <service-path>
|
||||
2. 每个环境写 `compose.yaml`;共享的 node/base_path 放 `.pouch/deployer/_config.yaml`
|
||||
3. 环境有差异 → 在该环境的 `_config.yaml` 覆盖
|
||||
4. 同名冲突或需要固定远程目录名 → `_config.yaml` 写 `name:`
|
||||
5. 首次部署前确认目标主机的远程目录不存在旧内容(rsync `--delete` 会清掉)
|
||||
5. 首次部署:远程目录不存在或为空时跳过备份;已有内容会被拷到 `$HOME/bakeup/deployer-skill/` 再同步
|
||||
|
||||
## 下线服务
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ sync_exclude:
|
||||
```
|
||||
|
||||
运行时数据放这些目录或 compose 挂载的远程绝对路径(如 `/data01/docker/<svc>/`),避免被 `--delete` 清掉。
|
||||
sync 前的配置备份在远程 `$HOME/bakeup/deployer-skill/<服务名>/<时间戳>/`,不在同步范围内。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -12,12 +12,16 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
BAKEUP_KEEP = 5
|
||||
BAKEUP_ROOT = "deployer-skill"
|
||||
|
||||
_SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
if str(_SCRIPT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
@@ -25,7 +29,60 @@ if str(_SCRIPT_DIR) not in sys.path:
|
||||
from lib import DEFAULT_BASE_PATH, rsync_ssh_args, scp_base_args, service_info, ssh_cmd # noqa: E402
|
||||
|
||||
|
||||
def _safe_bakeup_name(name: str) -> str:
|
||||
cleaned = re.sub(r"[^A-Za-z0-9._-]+", "-", name).strip("-.")
|
||||
return cleaned or "service"
|
||||
|
||||
|
||||
def _backup_remote(info: dict) -> int:
|
||||
"""把远程服务目录拷到 $HOME/bakeup/deployer-skill/<name>/<timestamp>/,失败则中止 sync。"""
|
||||
name = _safe_bakeup_name(info["name"])
|
||||
remote_dir = info["remote_dir"]
|
||||
script = f"""
|
||||
set -eu
|
||||
remote_dir={shlex.quote(remote_dir)}
|
||||
name={shlex.quote(name)}
|
||||
root={shlex.quote(BAKEUP_ROOT)}
|
||||
keep={BAKEUP_KEEP}
|
||||
if [ ! -d "$remote_dir" ]; then
|
||||
echo "跳过备份: 远程目录不存在 $remote_dir"
|
||||
exit 0
|
||||
fi
|
||||
if [ -z "$(find "$remote_dir" -mindepth 1 -maxdepth 1 -print -quit)" ]; then
|
||||
echo "跳过备份: 远程目录为空 $remote_dir"
|
||||
exit 0
|
||||
fi
|
||||
stamp=$(date +%Y%m%d-%H%M%S)
|
||||
base="$HOME/bakeup/$root/$name"
|
||||
dest="$base/$stamp"
|
||||
mkdir -p "$dest"
|
||||
chmod 700 "$HOME/bakeup" "$HOME/bakeup/$root" "$base" "$dest"
|
||||
cp -a "$remote_dir"/. "$dest"/
|
||||
ls -1d "$base"/[0-9][0-9][0-9][0-9]* 2>/dev/null | sort -r | tail -n +"$((keep + 1))" | while IFS= read -r old; do
|
||||
rm -rf "$old"
|
||||
done
|
||||
echo "已备份到 $dest (保留最近 $keep 份)"
|
||||
"""
|
||||
cmd = ssh_cmd(info, script)
|
||||
print(f"备份远程目录 {info['node']}:{remote_dir}")
|
||||
print(f"备份位置: $HOME/bakeup/{BAKEUP_ROOT}/{name}/<时间戳>/")
|
||||
print("-" * 60)
|
||||
try:
|
||||
completed = subprocess.run(cmd, check=False)
|
||||
except FileNotFoundError:
|
||||
print("错误: ssh 命令未找到,无法备份")
|
||||
return 1
|
||||
if completed.returncode:
|
||||
print(f"错误: 远程备份失败,退出码: {completed.returncode};已中止同步")
|
||||
return completed.returncode
|
||||
print("-" * 60)
|
||||
return 0
|
||||
|
||||
|
||||
def sync_directory(info: dict) -> int:
|
||||
backup_rc = _backup_remote(info)
|
||||
if backup_rc:
|
||||
return backup_rc
|
||||
if shutil.which("rsync"):
|
||||
return _sync_rsync(info)
|
||||
print("本机没有 rsync,改用 scp(不会删除远程多余文件)")
|
||||
|
||||
Reference in New Issue
Block a user