feat: add deployer

This commit is contained in:
2026-08-24 09:44:59 +08:00
parent 31bc5f45ce
commit e4d4319919
20 changed files with 1971 additions and 288 deletions
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env python3
"""
目录同步脚本
将指定目录同步到远程机器
用法:
python sync.py <目录路径>
例如: python sync.py hosts/web1/myapp
"""
from __future__ import annotations
import argparse
import os
import shlex
import shutil
import subprocess
import sys
from lib import DEFAULT_BASE_PATH, project_root, rsync_ssh_args, service_info, ssh_cmd
def sync_directory(info: dict) -> int:
if shutil.which("rsync"):
return _sync_rsync(info)
print("本机没有 rsync,改用 tar over SSH(不会删除远程多余文件)")
return _sync_tar(info)
def _sync_rsync(info: dict) -> int:
source_dir = info["service_dir"]
node = info["node"]
remote_path = f"{node}:{info['remote_dir']}"
rsync_cmd = [
"rsync",
"-avz",
"--delete",
*rsync_ssh_args(info),
]
for item in info["sync_exclude"]:
rsync_cmd.append(f"--exclude={item}/")
rsync_cmd.extend([f"{source_dir}/", remote_path])
print(f"正在同步 {source_dir}{remote_path}")
if info["sync_exclude"]:
print(f"排除目录: {', '.join(info['sync_exclude'])}")
print(f"执行命令: {' '.join(rsync_cmd)}")
print("-" * 60)
try:
subprocess.run(rsync_cmd, check=True)
print("-" * 60)
print("同步完成!")
return 0
except subprocess.CalledProcessError as e:
print(f"错误: rsync 执行失败,退出码: {e.returncode}")
return 1
except FileNotFoundError:
print("错误: rsync 命令未找到,请确保已安装 rsync")
return 1
def _sync_tar(info: dict) -> int:
source_dir = info["service_dir"]
remote_dir = info["remote_dir"]
tar_cmd = ["tar", "czf", "-", "-C", source_dir]
for item in info["sync_exclude"]:
tar_cmd.append(f"--exclude={item}")
tar_cmd.append(".")
remote_shell = (
f"mkdir -p {shlex.quote(remote_dir)} && "
f"tar xzf - -C {shlex.quote(remote_dir)}"
)
ssh = ssh_cmd(info, remote_shell)
print(f"正在同步 {source_dir}{info['node']}:{remote_dir}")
if info["sync_exclude"]:
print(f"排除目录: {', '.join(info['sync_exclude'])}")
print(f"执行命令: tar | {' '.join(ssh)}")
print("-" * 60)
tar = subprocess.Popen(tar_cmd, stdout=subprocess.PIPE)
try:
completed = subprocess.run(ssh, stdin=tar.stdout, check=False)
finally:
if tar.stdout:
tar.stdout.close()
tar.wait()
if tar.returncode:
print(f"错误: tar 打包失败,退出码: {tar.returncode}")
return tar.returncode
if completed.returncode:
print(f"错误: 远程 tar 解包失败,退出码: {completed.returncode}")
return completed.returncode
print("-" * 60)
print("同步完成!")
return 0
def main() -> int:
parser = argparse.ArgumentParser(
description="将指定目录同步到远程机器",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
python sync.py hosts/web1/myapp
python sync.py infra/traefik --base-path /opt/app
""",
)
parser.add_argument(
"directory",
help="要同步的目录路径(相对项目根,例如: hosts/web1/myapp",
)
parser.add_argument(
"--base-path",
default=DEFAULT_BASE_PATH,
help=f"远程基础路径(默认: {DEFAULT_BASE_PATH}",
)
args = parser.parse_args()
info = service_info(args.directory)
if args.base_path != DEFAULT_BASE_PATH:
name = info["name"]
info["base_path"] = args.base_path
info["remote_dir"] = f"{args.base_path}/{name}"
print(f"目标节点: {info['node']}")
if info.get("port") is not None:
print(f"SSH 端口: {info['port']}")
if info.get("identity_file"):
print(f"SSH 密钥: {info['identity_file']}")
print(f"源目录: {args.directory}")
print(f"远程路径: {info['remote_dir']}")
print()
return sync_directory(info)
if __name__ == "__main__":
sys.exit(main())