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:`。
|
避免同一台机器上多个项目的同名环境互相覆盖;需要固定名字时在 `_config.yaml` 写 `name:`。
|
||||||
|
|
||||||
重要边界:rsync 同步使用 `--delete`,远程多余的文件会被删除;scp 兜底不会删。
|
重要边界:rsync 同步使用 `--delete`,远程多余的文件会被删除;scp 兜底不会删。
|
||||||
数据库、证书等运行时数据必须放在排除目录或远程绝对路径挂载。涉及删除数据卷、
|
每次 sync 前会把远程服务目录拷到该机 `$HOME/bakeup/deployer-skill/<服务名>/<时间戳>/`,每服务只留最近 5 份;
|
||||||
清理远程文件的操作会先向你确认。
|
备份失败则中止同步。数据库、证书等运行时数据必须放在排除目录或远程绝对路径挂载。
|
||||||
|
涉及删除数据卷、清理远程文件的操作会先向你确认。
|
||||||
|
|
||||||
## 如何判断完成
|
## 如何判断完成
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ description: >-
|
|||||||
## 边界
|
## 边界
|
||||||
|
|
||||||
- 所有 sync/up/recreate/upgrade/restart 必须按单服务执行,禁止节点级批量。
|
- 所有 sync/up/recreate/upgrade/restart 必须按单服务执行,禁止节点级批量。
|
||||||
- `rsync` 带 `--delete`:运行时数据必须在 `data/`、`_data/` 或远程绝对路径挂载,否则会被清掉。
|
- `rsync` 带 `--delete`:sync 前先把远程服务目录拷到 `$HOME/bakeup/deployer-skill/<name>/<时间戳>/`(每服务留 5 份);运行时数据仍须在 `data/`、`_data/` 或远程绝对路径挂载,否则会被清掉。
|
||||||
- 仓库只放静态配置;证书、数据库、上传文件不进 Git,也不进同步范围。`unused/` 不参与发现与部署。
|
- 仓库只放静态配置;证书、数据库、上传文件不进 Git,也不进同步范围。`unused/` 不参与发现与部署。
|
||||||
- `node` 是 `~/.ssh/config` 的 Host 别名(可用 `user@host`)。用户没给别名就不要写假 node。
|
- `node` 是 `~/.ssh/config` 的 Host 别名(可用 `user@host`)。用户没给别名就不要写假 node。
|
||||||
- 密钥不入库。Compose 优先放远程 `.env`;Argo CD 的 dockerconfigjson / TLS 私钥只存在集群 Secret。
|
- 密钥不入库。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 去发明封装。
|
脚本一律 `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`
|
2. 每个环境写 `compose.yaml`;共享的 node/base_path 放 `.pouch/deployer/_config.yaml`
|
||||||
3. 环境有差异 → 在该环境的 `_config.yaml` 覆盖
|
3. 环境有差异 → 在该环境的 `_config.yaml` 覆盖
|
||||||
4. 同名冲突或需要固定远程目录名 → `_config.yaml` 写 `name:`
|
4. 同名冲突或需要固定远程目录名 → `_config.yaml` 写 `name:`
|
||||||
5. 首次部署前确认目标主机的远程目录不存在旧内容(rsync `--delete` 会清掉)
|
5. 首次部署:远程目录不存在或为空时跳过备份;已有内容会被拷到 `$HOME/bakeup/deployer-skill/` 再同步
|
||||||
|
|
||||||
## 下线服务
|
## 下线服务
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ sync_exclude:
|
|||||||
```
|
```
|
||||||
|
|
||||||
运行时数据放这些目录或 compose 挂载的远程绝对路径(如 `/data01/docker/<svc>/`),避免被 `--delete` 清掉。
|
运行时数据放这些目录或 compose 挂载的远程绝对路径(如 `/data01/docker/<svc>/`),避免被 `--delete` 清掉。
|
||||||
|
sync 前的配置备份在远程 `$HOME/bakeup/deployer-skill/<服务名>/<时间戳>/`,不在同步范围内。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,16 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
BAKEUP_KEEP = 5
|
||||||
|
BAKEUP_ROOT = "deployer-skill"
|
||||||
|
|
||||||
_SCRIPT_DIR = Path(__file__).resolve().parent
|
_SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
if str(_SCRIPT_DIR) not in sys.path:
|
if str(_SCRIPT_DIR) not in sys.path:
|
||||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
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
|
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:
|
def sync_directory(info: dict) -> int:
|
||||||
|
backup_rc = _backup_remote(info)
|
||||||
|
if backup_rc:
|
||||||
|
return backup_rc
|
||||||
if shutil.which("rsync"):
|
if shutil.which("rsync"):
|
||||||
return _sync_rsync(info)
|
return _sync_rsync(info)
|
||||||
print("本机没有 rsync,改用 scp(不会删除远程多余文件)")
|
print("本机没有 rsync,改用 scp(不会删除远程多余文件)")
|
||||||
|
|||||||
Reference in New Issue
Block a user