Merge remote-tracking branch 'origin/main' into feature/ui-capability-platform
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
# feature/svc-db-ops 任务卡
|
||||
|
||||
任务主记录:PR #59 https://git-panel.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/59(merge 30a09686ea);状态:已完成(回填);owner:weichen
|
||||
|
||||
## 基线与授权
|
||||
|
||||
- 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= d54bfdd8e99d5d3d9a7f5b1db65ab90c841dd7e0
|
||||
- 工作区:/home/weichen/projects/Agent-System-Dev-wt-feature-svc-db-ops(worktree);分支:feature/svc-db-ops
|
||||
- 授权与交付终点:用户确认方案后授权实施 svc 数据库运维扩展;终点 = 实现合入 main 并回收工作区。
|
||||
仅改 Dev 仓 `scripts/svc.sh`(及 Makefile 帮助行);不碰主检出 .video_agent/(已本地 exclude)。
|
||||
- 主记录 PR 链接:<task-finish 时回填>
|
||||
|
||||
## 目标与非目标
|
||||
|
||||
目标(scripts/svc.sh 数据库运维扩展,均沿用既有登记/事件/安全边界):
|
||||
1. `set <name> [--port N] [--host H] [--apply] [--tag T...]`:显式变更登记。postgres 换端口
|
||||
运行中须 --apply(stop→改登记→start);未运行直接生效;端口冲突预检(登记+监听双查)。
|
||||
agent/static 的 set 只改 tags(换端口走 restart --port)。
|
||||
2. `db backup <name> [--db X] [--out F]`:pg_dump -Fc → runtime/<name>/backups/<ts>-<db>.dump
|
||||
(600),pg_restore --list 自校验;`db backups` 列表。
|
||||
3. `db restore <name> --file F [--db X] [--force]`:恢复;目标库非空拒绝覆盖,--force 才
|
||||
DROP WITH (FORCE)+重建+恢复;完成后回显表数。
|
||||
4. `db clone <src> <dst> [--port P] [--password PW] [--tag T] [--online]`:默认物理克隆
|
||||
(短暂停源→cp -a pgdata→登记目标(新端口/继承用户库认证)→复启源;目标不自动启动);
|
||||
--online 走 dump/restore 不停源(目标须已 create+start)。
|
||||
5. `db copy <src> <dst> [--db X] [--force]`:跨实例单库逻辑复制(双方须运行;目标非空须 --force)。
|
||||
6. `db ls <name>`:列出实例内数据库。
|
||||
|
||||
非目标:agent SQLite 备份(本轮不做,记剩余项);不做定时/增量备份;不做跨主机传输;
|
||||
restore/clone 不删除备份文件;不动后端二进制(无 migrate 子命令,迁移=数据搬运)。
|
||||
|
||||
## 同步记录
|
||||
|
||||
| 节点 | sync_base_sha | tested_head_sha | checked_at |
|
||||
|---|---|---|---|
|
||||
| 启动 | d54bfdd8e99d5d3d9a7f5b1db65ab90c841dd7e0 | d54bfdd8e99d5d3d9a7f5b1db65ab90c841dd7e0 | 2026-09-30T14:01:04Z |
|
||||
|
||||
## 关键实现约定
|
||||
|
||||
- 显式参数 psql/pg_dump/pg_restore 助手(不依赖全局 ENV_*),支持 clone/copy 双实例切换;
|
||||
- svc.env 单键改写(svc_set_key)先读后写,避开"重定向块内读同文件"截断陷阱;
|
||||
- 备份/克隆/搬库全部进 history.jsonl;restore/copy 的覆盖必须 --force;
|
||||
- 物理克隆复用既有"只停自己进程"约束;复制剔除 postmaster.pid/.opts,socket 目录(-k)
|
||||
本就在环境目录而非 pgdata,不随 cp 传播。
|
||||
|
||||
## 结果与验证
|
||||
|
||||
- 结果:svc.sh 新增数据库运维族:`set`(换端口/标签,运行中须 --apply)、`db backup/backups/
|
||||
restore`(pg_dump -Fc + 归档自检 + --force 覆盖防护)、`db clone`(默认物理:短暂停源→
|
||||
cp pgdata→登记目标→复启源;--online 不停源)、`db copy`(跨实例单库,--force 防护)、
|
||||
`db ls`;显式参数 pg_run/dump/restore 助手、svc_set_key 单键改写、check_port_available。
|
||||
Makefile 帮助行同步。
|
||||
- 验证(真实 postgresql@18,全部在任务工作区 runtime 内;进程已停、目录已清):
|
||||
- 备份→backups 列表→归档自检;坏文件 restore 被拒(非 pg_dump 归档)。
|
||||
- set:运行中无 --apply 拒绝;--apply 后旧端口释放/新端口监听/DSN 联动/数据完整(3 行)。
|
||||
- 物理克隆:源短暂停止→复制→源在自身端口复启→目标 8896 启动后双实例数据一致;
|
||||
测试中发现并修复 CLI --port 泄漏进源复启的 bug(clone/copy 入口清空地址覆盖)。
|
||||
- copy:空目标成功;目标非空拒绝;--force 覆盖后行数与源一致(自动生成密码实例亦验证)。
|
||||
- restore:新库成功(回显表数);再恢复同库拒绝;--force 成功。
|
||||
- --online 克隆:源 pid 全程不变,目标数据一致。
|
||||
- set --tag 合并去重;list --tag 过滤;history 记录 backup/restore/clone/copy/set 事件。
|
||||
- 错误路径:目标已存在/端口被登记占用/坏归档/非 postgres 服务均明确报错。
|
||||
- 影响与剩余项:agent/static 的 SQLite/静态资源备份未做(登记为后续项);定时/增量备份不在
|
||||
本轮;restore --force 的 DROP WITH (FORCE) 需 PG13+(宿主 18 满足)。
|
||||
|
||||
## 闭合记录(card-autoclose 回填)
|
||||
|
||||
- 依据:merge 30a09686ea812023e5f5e7283776e182af86ad61(PR #59)已包含于 origin/main,分支成果已落盘;本段为合入后回填。
|
||||
- 合并主题:Merge pull request 'Add db ops to svc.sh: port change, backup, restore, clone, cross-instance copy' (#59) from feature/svc-db-ops into main
|
||||
- 归档:随闭合移入 .task/archive/;任务执行期的过程证据见原任务记录与 PR。
|
||||
@@ -39,3 +39,4 @@
|
||||
| 同步前验收 | 1fd4fade4a882f11240a39256354fd04c8101ff1 | 599a6448d4e5b13e89753eaddc168292689b1367 | 2026-09-30T12:41:46Z |
|
||||
| 同步前验收 | 8c4fd019475e57aa70646bed9f2ee1a334b72c45 | 8ba1b0e71570aed2ef277d11763a89ffb99f1b56 | 2026-09-30T13:20:32Z |
|
||||
| 同步前验收 | a2657bc25f6b43f9b3e1a23cea86e3ebcfd80ecc | 60261a835a6167f1b3295cfbbc1644a5532f97df | 2026-09-30T13:45:01Z |
|
||||
| 同步前验收 | d54bfdd8e99d5d3d9a7f5b1db65ab90c841dd7e0 | 69871136e591b2b8416660651679554824efc823 | 2026-09-30T14:11:30Z |
|
||||
|
||||
@@ -46,7 +46,7 @@ help:
|
||||
@echo "make build 构建单二进制 bin/agent-system(契约同步→前端构建→embed→静态链接)"
|
||||
@echo "make test 全量测试(core + backend + frontend)"
|
||||
@echo "make svc 服务级操作:make svc ENV=<name> [ACTION=deploy|start|stop|restart|status|logs] [ARGS='--from dev@<sha> --tag ...']"
|
||||
@echo "make svc-list 服务总览:make svc-list [ARGS='--tag core'];数据库:scripts/svc.sh db create|dsn <name>"
|
||||
@echo "make svc-list 服务总览:make svc-list [ARGS='--tag core'];数据库运维:scripts/svc.sh db create|dsn|backup|backups|restore|clone|copy|ls|set"
|
||||
@echo "make svc-commits 列出可部署提交:make svc-commits SRC=dev|release [N=20]"
|
||||
@echo "make svc-history 事件历史:make svc-history [ENV] [N=20]"
|
||||
@echo "make dev 开发模式说明(前后端分离热更新)"
|
||||
|
||||
+435
-6
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# svc.sh —— 本机服务管理(服务模型 v2):按提交/版本部署并启动 后端(agent) / 前端(static)
|
||||
# / 数据库(postgres) 三类服务,统一登记(svc.env + history.jsonl)、标签、配置分层、
|
||||
# 生命周期(start/stop/restart/rm),全程不触碰主检出与其他已启动服务。
|
||||
# 生命周期(start/stop/restart/rm)与数据库运维(换端口/备份/恢复/克隆/搬库),
|
||||
# 全程不触碰主检出与其他已启动服务。
|
||||
#
|
||||
# 用法:
|
||||
# scripts/svc.sh <name> [action] [参数...] # 服务级操作(name 见命名规则)
|
||||
@@ -9,6 +10,13 @@
|
||||
# scripts/svc.sh list [--tag T] # 全服务总览(status 等价)
|
||||
# scripts/svc.sh db create <name> [参数] # 创建 PostgreSQL 服务(initdb)
|
||||
# scripts/svc.sh db dsn <name> [--reveal] [--db X] # 打印数据库连接串(默认掩码密码)
|
||||
# scripts/svc.sh db backup <name> [--db X] [--out F] # pg_dump 备份(自动自检归档)
|
||||
# scripts/svc.sh db backups <name> # 列出已有备份
|
||||
# scripts/svc.sh db restore <name> --file F [--db X] [--force] # 恢复(覆盖非空库须 --force)
|
||||
# scripts/svc.sh db clone <src> <dst> [--port P] [--password PW] [--online] # 克隆实例
|
||||
# scripts/svc.sh db copy <src> <dst> [--db X] [--force] # 跨实例搬库(双方须运行)
|
||||
# scripts/svc.sh db ls <name> # 列出实例内数据库
|
||||
# scripts/svc.sh set <name> [--port N --apply] [--tag T] # 变更登记(换端口/标签)
|
||||
# scripts/svc.sh rm <name> [--purge] # 注销服务(--purge 连数据一起删)
|
||||
# scripts/svc.sh commits [dev|release] [N] # 列出可部署提交(默认 dev,N=20)
|
||||
# scripts/svc.sh history [name] [N] # 事件历史(默认全部,最近 20 条)
|
||||
@@ -51,6 +59,11 @@
|
||||
# scripts/svc.sh ui-qa deploy --kind static --dir ../some/dist
|
||||
# scripts/svc.sh list --tag core && scripts/svc.sh history
|
||||
# scripts/svc.sh pg1 restart && scripts/svc.sh rm tmp1 --purge
|
||||
# scripts/svc.sh set pg1 --port 8895 --apply # 换端口并重启生效(登记/DSN 联动)
|
||||
# scripts/svc.sh db backup pg1 && scripts/svc.sh db backups pg1
|
||||
# scripts/svc.sh db clone pg1 pg2 --port 8896 # 物理克隆一套到新端口
|
||||
# scripts/svc.sh db copy pg1 pg2 --db app # 把 pg1 的 app 库搬到 pg2
|
||||
# scripts/svc.sh db restore pg2 --file <dump> --db app --force
|
||||
#
|
||||
# 安全约束:
|
||||
# - 只写 runtime/<name>/ 与 runtime/.worktrees/;不改主检出 git 状态、不动他人现场;
|
||||
@@ -59,7 +72,10 @@
|
||||
# - 只停自己启动的进程:pidfile + /proc/<pid>/cmdline 标记双校验;postgres 走
|
||||
# pg_ctl -m fast 优雅停机,SIGKILL 不用于 postmaster;
|
||||
# - 端口被非本脚本进程占用时报错并给出占用者,不抢不杀;
|
||||
# - svc.env 含密码时权限 600;DSN 默认掩码密码(--reveal 才显示)。
|
||||
# - svc.env 含密码时权限 600;DSN 默认掩码密码(--reveal 才显示);
|
||||
# - 备份/克隆只增不删;restore/copy 覆盖非空库必须显式 --force(drop 重建再恢复);
|
||||
# 备份文件在 runtime/<name>/backups/(600),随 rm --purge 一并删除;
|
||||
# - 物理克隆期间源实例短暂停止(一致性要求),完成后自动复启;--online 不停源。
|
||||
# <<usage-end>>
|
||||
set -euo pipefail
|
||||
|
||||
@@ -105,6 +121,7 @@ CLI_MBASE=""; CLI_MKEY=""; CLI_MNAME=""
|
||||
CLI_LBASE=""; CLI_LKEY=""; CLI_LNAME=""; CLI_TAVILY=""
|
||||
CLI_FROM=""; CLI_PICK=""; CLI_DRYRUN=0
|
||||
CLI_KIND=""; CLI_DIR=""; CLI_PURGE=0; CLI_REVEAL=0
|
||||
CLI_APPLY=0; CLI_FORCE=0; CLI_ONLINE=0; CLI_FILE=""; CLI_OUT=""
|
||||
CLI_DB_USER=""; CLI_DB_PASSWORD=""; CLI_DB_NAME=""; CLI_DB_AUTH=""
|
||||
CLI_TAGS=()
|
||||
|
||||
@@ -137,6 +154,11 @@ parse_args() {
|
||||
--tag) need_value "$@"; CLI_TAGS+=("$2"); shift 2 ;;
|
||||
--purge) CLI_PURGE=1; shift ;;
|
||||
--reveal) CLI_REVEAL=1; shift ;;
|
||||
--apply) CLI_APPLY=1; shift ;;
|
||||
--force) CLI_FORCE=1; shift ;;
|
||||
--online) CLI_ONLINE=1; shift ;;
|
||||
--file) need_value "$@"; CLI_FILE="$2"; shift 2 ;;
|
||||
--out) need_value "$@"; CLI_OUT="$2"; shift 2 ;;
|
||||
--user) need_value "$@"; CLI_DB_USER="$2"; shift 2 ;;
|
||||
--password) need_value "$@"; CLI_DB_PASSWORD="$2"; shift 2 ;;
|
||||
--db) need_value "$@"; CLI_DB_NAME="$2"; shift 2 ;;
|
||||
@@ -159,7 +181,7 @@ addr_compose() {
|
||||
}
|
||||
|
||||
# ---- 名称与端口 ----
|
||||
RESERVED_ENVS=" status list add db rm create dsn commits history deploy stop start restart logs help version "
|
||||
RESERVED_ENVS=" status list add set db rm create dsn commits history deploy stop start restart logs help version "
|
||||
|
||||
# valid_env NAME:服务名合法性(小写字母开头、a-z0-9-、≤16 位;保留字不可用)。
|
||||
valid_env() {
|
||||
@@ -930,9 +952,23 @@ cmd_db() {
|
||||
local sub="${1:-}"
|
||||
[ $# -gt 0 ] && shift
|
||||
case "$sub" in
|
||||
create) db_create "$@" ;;
|
||||
dsn) db_dsn "$@" ;;
|
||||
*) c_err "用法:scripts/svc.sh db create <name> [--port --user --password --db --auth-mode --tag] | db dsn <name> [--reveal] [--db X]"; exit 2 ;;
|
||||
create) db_create "$@" ;;
|
||||
dsn) db_dsn "$@" ;;
|
||||
backup) db_backup "$@" ;;
|
||||
backups) db_backups "$@" ;;
|
||||
restore) db_restore "$@" ;;
|
||||
clone) db_clone "$@" ;;
|
||||
copy) db_copy "$@" ;;
|
||||
ls) db_ls "$@" ;;
|
||||
set) cmd_set "$@" ;;
|
||||
*) c_err "用法:scripts/svc.sh db create|dsn|backup|backups|restore|clone|copy|ls|set <name> [...]
|
||||
db create <name> [--port --user --password --db --auth-mode --tag]
|
||||
db dsn <name> [--reveal] [--db X]
|
||||
db backup <name> [--db X] [--out F] | db backups <name>
|
||||
db restore <name> --file F [--db X] [--force]
|
||||
db clone <src> <dst> [--port P] [--password PW] [--tag T] [--online]
|
||||
db copy <src> <dst> [--db X] [--force]
|
||||
db ls <name> | db set <name> [--port N --apply]"; exit 2 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
@@ -1093,6 +1129,398 @@ db_dsn() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---- 数据库运维:换端口 / 备份 / 恢复 / 克隆 / 搬库 ----
|
||||
|
||||
# check_port_available PORT SKIP_NAME:显式选端口时校验(未被其他登记占用且未在监听)。
|
||||
check_port_available() {
|
||||
local p="$1" skip="$2"
|
||||
if ports_recorded "$p" "$skip"; then
|
||||
c_err "[$ENV_NAME] 端口 $p 已被其他服务登记占用"
|
||||
return 1
|
||||
fi
|
||||
if port_listening "$p"; then
|
||||
c_err "[$ENV_NAME] 端口 $p 正被监听(非本脚本登记的进程)"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# svc_set_key KEY VALUE:改写当前服务 svc.env 的单键(先读后写,避开重定向截断陷阱)。
|
||||
svc_set_key() {
|
||||
local key="$1" val="$2" rest
|
||||
[ -f "$ENV_DIR/svc.env" ] || { c_err "svc.env 不存在:$ENV_DIR"; return 1; }
|
||||
rest="$(grep -v "^$key=" "$ENV_DIR/svc.env" || true)"
|
||||
{ [ -n "$rest" ] && printf '%s\n' "$rest"; printf '%s=%s\n' "$key" "$val"; } > "$ENV_DIR/svc.env"
|
||||
chmod 600 "$ENV_DIR/svc.env"
|
||||
return 0
|
||||
}
|
||||
|
||||
# 显式参数版 psql/dump/restore:clone/copy 需要同时操作两个实例,不依赖全局 ENV_*。
|
||||
pg_run() { # HOST PORT USER PASS DB SQL [tA]
|
||||
local host="$1" port="$2" user="$3" pass="$4" db="$5" sql="$6" ta="${7:-}" pgbin
|
||||
pgbin="$(pg_bindir)"
|
||||
PGPASSWORD="$pass" "$pgbin/psql" -h "$host" -p "$port" -U "$user" -d "$db" ${ta:+-tA} -c "$sql" 2>/dev/null
|
||||
}
|
||||
|
||||
pg_dump_run() { # HOST PORT USER PASS DB OUT
|
||||
local host="$1" port="$2" user="$3" pass="$4" db="$5" out="$6" pgbin
|
||||
pgbin="$(pg_bindir)"
|
||||
PGPASSWORD="$pass" "$pgbin/pg_dump" -h "$host" -p "$port" -U "$user" -d "$db" -Fc -f "$out"
|
||||
}
|
||||
|
||||
pg_restore_list() { # FILE:exit 0 = 合法 pg_dump 归档
|
||||
"$(pg_bindir)/pg_restore" --list "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
pg_restore_run() { # HOST PORT USER PASS DB FILE:出错回显 stderr 尾部
|
||||
local host="$1" port="$2" user="$3" pass="$4" db="$5" file="$6" pgbin err
|
||||
pgbin="$(pg_bindir)"
|
||||
if ! err="$(PGPASSWORD="$pass" "$pgbin/pg_restore" -h "$host" -p "$port" -U "$user" -d "$db" \
|
||||
--no-owner --no-privileges "$file" 2>&1)"; then
|
||||
printf '%s\n' "$err" | tail -5 >&2
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# copy_database SH SP SU SPASS SDB DH DP DU DPASS DDB [FORCE]:单库逻辑复制
|
||||
# (dump → 建库/覆盖 → restore);目标库非空且无 FORCE 时拒绝。返回 0 = 成功。
|
||||
copy_database() {
|
||||
local sh="$1" sp="$2" su="$3" spass="$4" sdb="$5"
|
||||
local dh="$6" dp="$7" du="$8" dpass="$9" ddb="${10}" force="${11:-0}"
|
||||
local tmp exists objects
|
||||
tmp="$(mktemp /tmp/svc-copy-XXXXXXXX.dump)"
|
||||
if ! pg_dump_run "$sh" "$sp" "$su" "$spass" "$sdb" "$tmp"; then
|
||||
rm -f "$tmp"; c_err "pg_dump $sdb 失败(源 $sh:$sp)"; return 1
|
||||
fi
|
||||
if ! pg_restore_list "$tmp"; then
|
||||
rm -f "$tmp"; c_err "备份归档自检失败"; return 1
|
||||
fi
|
||||
exists="$(pg_run "$dh" "$dp" "$du" "$dpass" postgres "SELECT 1 FROM pg_database WHERE datname='$ddb'" tA || true)"
|
||||
if [ -n "$exists" ]; then
|
||||
objects="$(pg_run "$dh" "$dp" "$du" "$dpass" "$ddb" "SELECT count(*) FROM pg_tables WHERE schemaname NOT IN ('pg_catalog','information_schema')" tA || true)"
|
||||
case "$objects" in ''|*[!0-9]*) objects=0 ;; esac
|
||||
if [ "$objects" -gt 0 ] && [ "$force" != 1 ]; then
|
||||
rm -f "$tmp"
|
||||
c_err "目标库 $ddb 已有 $objects 张表——拒绝覆盖(--force 覆盖)"
|
||||
return 1
|
||||
fi
|
||||
if [ "$force" = 1 ]; then
|
||||
pg_run "$dh" "$dp" "$du" "$dpass" postgres "DROP DATABASE IF EXISTS \"$ddb\" WITH (FORCE)" >/dev/null \
|
||||
|| { rm -f "$tmp"; c_err "drop $ddb 失败"; return 1; }
|
||||
pg_run "$dh" "$dp" "$du" "$dpass" postgres "CREATE DATABASE \"$ddb\"" >/dev/null \
|
||||
|| { rm -f "$tmp"; c_err "create $ddb 失败"; return 1; }
|
||||
fi
|
||||
else
|
||||
pg_run "$dh" "$dp" "$du" "$dpass" postgres "CREATE DATABASE \"$ddb\"" >/dev/null \
|
||||
|| { rm -f "$tmp"; c_err "create $ddb 失败"; return 1; }
|
||||
fi
|
||||
if ! pg_restore_run "$dh" "$dp" "$du" "$dpass" "$ddb" "$tmp"; then
|
||||
rm -f "$tmp"; return 1
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
return 0
|
||||
}
|
||||
|
||||
db_backup() {
|
||||
local name="${1:-}"
|
||||
[ -n "$name" ] || { c_err "用法:scripts/svc.sh db backup <name> [--db X] [--out FILE]"; exit 2; }
|
||||
shift
|
||||
parse_args "$@"
|
||||
env_config "$name"
|
||||
[ "$SVC_KIND" = postgres ] || { c_err "[$ENV_NAME] kind=$SVC_KIND 不是 postgres"; exit 2; }
|
||||
if [ -z "$(read_pid)" ] || ! pid_alive "$(read_pid)"; then
|
||||
c_err "[$ENV_NAME] 未运行——备份走 TCP,请先 scripts/svc.sh $ENV_NAME start"
|
||||
exit 1
|
||||
fi
|
||||
local db="${CLI_DB_NAME:-$(svc_get dbname)}"
|
||||
local host="${ENV_ADDR%:*}" port="${ENV_ADDR##*:}"
|
||||
local user pass; user="$(svc_get user)"; pass="$(svc_get password)"
|
||||
local out="${CLI_OUT:-$ENV_DIR/backups/$(date -u +%Y%m%dT%H%M%SZ)-$db.dump}"
|
||||
mkdir -p "$(dirname "$out")"; chmod 700 "$(dirname "$out")"
|
||||
( umask 077; : > "$out" )
|
||||
c_info "[$ENV_NAME] pg_dump $db → $out"
|
||||
if ! pg_dump_run "$host" "$port" "$user" "$pass" "$db" "$out"; then
|
||||
rm -f "$out"
|
||||
hist_event backup fail "$(read_pid)" "db=$db"
|
||||
c_err "[$ENV_NAME] 备份失败(上方输出)"
|
||||
exit 1
|
||||
fi
|
||||
if ! pg_restore_list "$out"; then
|
||||
rm -f "$out"
|
||||
hist_event backup fail "$(read_pid)" "归档自检失败 db=$db"
|
||||
c_err "[$ENV_NAME] 备份归档自检失败(pg_restore --list 不通过),已删除"
|
||||
exit 1
|
||||
fi
|
||||
hist_event backup ok "$(read_pid)" "db=$db file=$(basename "$out") size=$(du -h "$out" | cut -f1)"
|
||||
c_ok "[$ENV_NAME] 备份完成:$out($(du -h "$out" | cut -f1))"
|
||||
}
|
||||
|
||||
db_backups() {
|
||||
local name="${1:-}"
|
||||
[ -n "$name" ] || { c_err "用法:scripts/svc.sh db backups <name>"; exit 2; }
|
||||
env_config "$name"
|
||||
[ "$SVC_KIND" = postgres ] || { c_err "[$ENV_NAME] kind=$SVC_KIND 不是 postgres"; exit 2; }
|
||||
local dir="$ENV_DIR/backups" f
|
||||
if [ ! -d "$dir" ] || [ -z "$(ls "$dir"/*.dump 2>/dev/null || true)" ]; then
|
||||
c_info "[$ENV_NAME] 暂无备份(scripts/svc.sh db backup $ENV_NAME)"
|
||||
return 0
|
||||
fi
|
||||
c_info "[$ENV_NAME] 备份列表($dir)"
|
||||
for f in "$dir"/*.dump; do
|
||||
[ -f "$f" ] || continue
|
||||
printf ' %s %6s %s\n' "$(stat -c %y "$f" | cut -d. -f1)" "$(du -h "$f" | cut -f1)" "$(basename "$f")"
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
db_restore() {
|
||||
local name="${1:-}"
|
||||
[ -n "$name" ] || { c_err "用法:scripts/svc.sh db restore <name> --file F [--db X] [--force]"; exit 2; }
|
||||
shift
|
||||
parse_args "$@"
|
||||
[ -n "$CLI_FILE" ] || { c_err "需要 --file <备份文件>"; exit 2; }
|
||||
[ -f "$CLI_FILE" ] || { c_err "备份文件不存在:$CLI_FILE"; exit 2; }
|
||||
env_config "$name"
|
||||
[ "$SVC_KIND" = postgres ] || { c_err "[$ENV_NAME] kind=$SVC_KIND 不是 postgres"; exit 2; }
|
||||
if [ -z "$(read_pid)" ] || ! pid_alive "$(read_pid)"; then
|
||||
c_err "[$ENV_NAME] 未运行——恢复走 TCP,请先 scripts/svc.sh $ENV_NAME start"
|
||||
exit 1
|
||||
fi
|
||||
pg_restore_list "$CLI_FILE" || { c_err "不是有效的 pg_dump 归档:$CLI_FILE"; exit 2; }
|
||||
local db="${CLI_DB_NAME:-$(svc_get dbname)}"
|
||||
local host="${ENV_ADDR%:*}" port="${ENV_ADDR##*:}"
|
||||
local user pass; user="$(svc_get user)"; pass="$(svc_get password)"
|
||||
local exists objects
|
||||
exists="$(pg_run "$host" "$port" "$user" "$pass" postgres "SELECT 1 FROM pg_database WHERE datname='$db'" tA || true)"
|
||||
if [ -n "$exists" ]; then
|
||||
objects="$(pg_run "$host" "$port" "$user" "$pass" "$db" "SELECT count(*) FROM pg_tables WHERE schemaname NOT IN ('pg_catalog','information_schema')" tA || true)"
|
||||
case "$objects" in ''|*[!0-9]*) objects=0 ;; esac
|
||||
if [ "$objects" -gt 0 ] && [ "$CLI_FORCE" != 1 ]; then
|
||||
c_err "[$ENV_NAME] 目标库 $db 已存在且含 $objects 张表——拒绝覆盖;确认覆盖加 --force"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$CLI_FORCE" = 1 ]; then
|
||||
c_warn "[$ENV_NAME] --force:删除并重建库 $db"
|
||||
pg_run "$host" "$port" "$user" "$pass" postgres "DROP DATABASE IF EXISTS \"$db\" WITH (FORCE)" >/dev/null \
|
||||
|| { c_err "[$ENV_NAME] drop $db 失败"; exit 1; }
|
||||
pg_run "$host" "$port" "$user" "$pass" postgres "CREATE DATABASE \"$db\"" >/dev/null \
|
||||
|| { c_err "[$ENV_NAME] create $db 失败"; exit 1; }
|
||||
fi
|
||||
else
|
||||
pg_run "$host" "$port" "$user" "$pass" postgres "CREATE DATABASE \"$db\"" >/dev/null \
|
||||
|| { c_err "[$ENV_NAME] 建库 $db 失败"; exit 1; }
|
||||
fi
|
||||
c_info "[$ENV_NAME] 恢复 $db ← $CLI_FILE"
|
||||
if ! pg_restore_run "$host" "$port" "$user" "$pass" "$db" "$CLI_FILE"; then
|
||||
hist_event restore fail "$(read_pid)" "db=$db file=$(basename "$CLI_FILE")"
|
||||
c_err "[$ENV_NAME] 恢复失败(上方输出)"
|
||||
exit 1
|
||||
fi
|
||||
local tables
|
||||
tables="$(pg_run "$host" "$port" "$user" "$pass" "$db" "SELECT count(*) FROM pg_tables WHERE schemaname NOT IN ('pg_catalog','information_schema')" tA || true)"
|
||||
hist_event restore ok "$(read_pid)" "db=$db file=$(basename "$CLI_FILE") tables=${tables:-?}"
|
||||
c_ok "[$ENV_NAME] 恢复完成:库 $db(${tables:-?} 张表)← $(basename "$CLI_FILE")"
|
||||
}
|
||||
|
||||
db_clone() {
|
||||
local src="${1:-}" dst="${2:-}"
|
||||
[ -n "$src" ] && [ -n "$dst" ] || {
|
||||
c_err "用法:scripts/svc.sh db clone <src> <dst> [--port P] [--host H] [--password PW] [--tag T] [--online]"
|
||||
exit 2
|
||||
}
|
||||
shift 2
|
||||
parse_args "$@"
|
||||
local req_port="${CLI_PORT:-}" req_host="${CLI_HOST:-}"
|
||||
CLI_PORT=""; CLI_HOST=""; CLI_ADDR="" # 克隆不消费地址覆盖——防泄漏到源复启与目标解析
|
||||
valid_env "$dst" || { c_err "非法目标服务名:$dst"; exit 2; }
|
||||
[ "$dst" != "$src" ] || { c_err "目标不能与源相同"; exit 2; }
|
||||
env_config "$src"
|
||||
[ "$SVC_KIND" = postgres ] || { c_err "[$src] kind=$SVC_KIND 不是 postgres"; exit 2; }
|
||||
local src_name="$ENV_NAME" src_addr="$ENV_ADDR" src_dir="$ENV_DIR"
|
||||
local src_user pass db auth pgdata ver tags
|
||||
src_user="$(svc_get user)"; pass="$(svc_get password)"; db="$(svc_get dbname)"
|
||||
auth="$(svc_get authmode)"; pgdata="$(svc_get pgdata)"; ver="$(svc_get version)"; tags="$(svc_get tags)"
|
||||
[ -n "$pgdata" ] && [ -d "$pgdata" ] || { c_err "[$src] 无数据目录"; exit 1; }
|
||||
|
||||
if [ "$CLI_ONLINE" = 1 ]; then
|
||||
# 逻辑克隆:不停源;目标须已 create+start,复制源登记库。
|
||||
[ -f "$RUNTIME/$dst/svc.env" ] || {
|
||||
c_err "[$dst] 未登记——--online 要求目标先 db create + start(或省略 --online 走物理克隆)"
|
||||
exit 2
|
||||
}
|
||||
env_config "$dst"
|
||||
[ "$SVC_KIND" = postgres ] || { c_err "[$dst] kind=$SVC_KIND 不是 postgres"; exit 2; }
|
||||
[ -n "$(read_pid)" ] && pid_alive "$(read_pid)" || { c_err "[$dst] 未运行——先 start"; exit 1; }
|
||||
local dh="${ENV_ADDR%:*}" dp="${ENV_ADDR##*:}" du dpass dname
|
||||
du="$(svc_get user)"; dpass="$(svc_get password)"; dname="$ENV_NAME"
|
||||
c_info "[$src].$db → [$dst].$db 在线克隆(不停源)"
|
||||
if copy_database "${src_addr%:*}" "${src_addr##*:}" "$src_user" "$pass" "$db" \
|
||||
"$dh" "$dp" "$du" "$dpass" "$db" "$CLI_FORCE"; then
|
||||
ENV_NAME="$dname"; ENV_DIR="$RUNTIME/$dname"; ENV_ADDR="$dh:$dp"; SVC_KIND=postgres; SRC_LABEL="clone@$src_name"
|
||||
hist_event clone ok "" "online db=$db from $src_name"
|
||||
ENV_NAME="$src_name"; ENV_DIR="$src_dir"; ENV_ADDR="$src_addr"
|
||||
hist_event clone ok "" "online db=$db to $dst"
|
||||
c_ok "[$src] → [$dst] 在线克隆完成(库 $db)"
|
||||
return 0
|
||||
fi
|
||||
ENV_NAME="$src_name"; ENV_DIR="$src_dir"; ENV_ADDR="$src_addr"
|
||||
hist_event clone fail "" "online db=$db to $dst"
|
||||
c_err "在线克隆失败(上方输出)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 物理克隆(默认):短暂停源 → cp pgdata → 登记目标 → 复启源;目标不自动启动。
|
||||
[ -e "$RUNTIME/$dst" ] && { c_err "[$dst] 目录已存在:$RUNTIME/$dst(换名或 rm)"; exit 2; }
|
||||
local saved_name="$ENV_NAME"
|
||||
ENV_NAME="$dst" # 端口校验以目标视角提示
|
||||
local dst_port="${req_port:-}"
|
||||
if [ -n "$dst_port" ]; then
|
||||
check_port_available "$dst_port" "$dst" || exit 1
|
||||
else
|
||||
dst_port="$(derive_port "$dst")" || { c_err "[$dst] 无空闲派生端口,请 --port 指定"; exit 1; }
|
||||
fi
|
||||
ENV_NAME="$saved_name"
|
||||
local dst_host="${req_host:-127.0.0.1}"
|
||||
local dst_pass="${CLI_DB_PASSWORD:-$pass}"
|
||||
local was_running=0
|
||||
if [ -n "$(read_pid)" ] && pid_alive "$(read_pid)"; then was_running=1; fi
|
||||
if [ "$was_running" = 1 ]; then
|
||||
c_info "[$src] 物理克隆需短暂停止源实例(一致性),完成后自动复启"
|
||||
pg_do_stop || { c_err "[$src] 停止失败,克隆中止(数据未动)"; return 1; }
|
||||
fi
|
||||
mkdir -p "$RUNTIME/$dst"; chmod 700 "$RUNTIME/$dst"
|
||||
c_info "[$dst] 复制数据目录($pgdata → $RUNTIME/$dst/pgdata)"
|
||||
if ! cp -a "$pgdata" "$RUNTIME/$dst/pgdata"; then
|
||||
c_err "[$dst] 数据目录复制失败"
|
||||
[ "$was_running" = 1 ] && { env_config "$src_name"; pg_do_start; }
|
||||
return 1
|
||||
fi
|
||||
rm -f "$RUNTIME/$dst/pgdata/postmaster.pid" "$RUNTIME/$dst/pgdata/postmaster.opts"
|
||||
local t out_tags="" seen=" "
|
||||
for t in $(printf '%s' "$tags" | tr ',' ' ') ${CLI_TAGS[@]+"${CLI_TAGS[@]}"}; do
|
||||
[ -n "$t" ] || continue
|
||||
case "$seen" in *" $t "*) continue ;; esac
|
||||
seen="$seen $t "; out_tags="${out_tags:+$out_tags,}$t"
|
||||
done
|
||||
{
|
||||
printf 'kind=postgres\n'
|
||||
printf 'env=%s\n' "$dst"
|
||||
printf 'pid=\n'
|
||||
printf 'addr=%s:%s\n' "$dst_host" "$dst_port"
|
||||
printf 'tags=%s\n' "$out_tags"
|
||||
printf 'source=clone@%s\n' "$src_name"
|
||||
printf 'version=%s\n' "$ver"
|
||||
printf 'pidcheck=%s\n' "$RUNTIME/$dst/pgdata"
|
||||
printf 'user=%s\n' "$src_user"
|
||||
printf 'password=%s\n' "$dst_pass"
|
||||
printf 'dbname=%s\n' "$db"
|
||||
printf 'authmode=%s\n' "$auth"
|
||||
printf 'pgdata=%s\n' "$RUNTIME/$dst/pgdata"
|
||||
} > "$RUNTIME/$dst/svc.env"
|
||||
chmod 600 "$RUNTIME/$dst/svc.env"
|
||||
ENV_NAME="$dst"; ENV_DIR="$RUNTIME/$dst"; ENV_ADDR="$dst_host:$dst_port"; SVC_KIND=postgres
|
||||
SRC_LABEL="clone@$src_name"
|
||||
hist_event create ok "" "physical clone from $src_name port=$dst_port"
|
||||
if [ "$was_running" = 1 ]; then
|
||||
env_config "$src_name"
|
||||
pg_do_start || c_warn "[$src] 复启失败(克隆已成功;查看 $ENV_DIR/pg.log)"
|
||||
fi
|
||||
c_ok "[$dst] 克隆完成(未启动):scripts/svc.sh $dst start;DSN:scripts/svc.sh db dsn $dst --reveal"
|
||||
return 0
|
||||
}
|
||||
|
||||
db_copy() {
|
||||
local src="${1:-}" dst="${2:-}"
|
||||
[ -n "$src" ] && [ -n "$dst" ] || { c_err "用法:scripts/svc.sh db copy <src> <dst> [--db X] [--force]"; exit 2; }
|
||||
shift 2
|
||||
parse_args "$@"
|
||||
CLI_PORT=""; CLI_HOST=""; CLI_ADDR="" # copy 不消费地址覆盖
|
||||
[ "$src" != "$dst" ] || { c_err "源与目标不能相同"; exit 2; }
|
||||
env_config "$src"
|
||||
[ "$SVC_KIND" = postgres ] || { c_err "[$src] kind=$SVC_KIND 不是 postgres"; exit 2; }
|
||||
[ -n "$(read_pid)" ] && pid_alive "$(read_pid)" || { c_err "[$src] 未运行——copy 走 TCP,双方都需 start"; exit 1; }
|
||||
local sh="${ENV_ADDR%:*}" sp="${ENV_ADDR##*:}" su pass sdb sname sdir
|
||||
su="$(svc_get user)"; pass="$(svc_get password)"; sname="$ENV_NAME"; sdir="$ENV_DIR"
|
||||
sdb="${CLI_DB_NAME:-$(svc_get dbname)}"
|
||||
env_config "$dst"
|
||||
[ "$SVC_KIND" = postgres ] || { c_err "[$dst] kind=$SVC_KIND 不是 postgres"; exit 2; }
|
||||
[ -n "$(read_pid)" ] && pid_alive "$(read_pid)" || { c_err "[$dst] 未运行——copy 走 TCP,双方都需 start"; exit 1; }
|
||||
local dh="${ENV_ADDR%:*}" dp="${ENV_ADDR##*:}" du dpass dname
|
||||
du="$(svc_get user)"; dpass="$(svc_get password)"; dname="$ENV_NAME"
|
||||
c_info "[$sname].$sdb → [$dname].$sdb(逻辑复制)"
|
||||
if copy_database "$sh" "$sp" "$su" "$pass" "$sdb" "$dh" "$dp" "$du" "$dpass" "$sdb" "$CLI_FORCE"; then
|
||||
ENV_NAME="$dname"; ENV_DIR="$RUNTIME/$dname"; ENV_ADDR="$dh:$dp"; SVC_KIND=postgres
|
||||
hist_event copy ok "" "from $sname db=$sdb"
|
||||
ENV_NAME="$sname"; ENV_DIR="$sdir"; ENV_ADDR="$sh:$sp"
|
||||
hist_event copy ok "" "to $dname db=$sdb"
|
||||
c_ok "复制完成:$sname/$sdb → $dname/$sdb"
|
||||
return 0
|
||||
fi
|
||||
ENV_NAME="$sname"; ENV_DIR="$sdir"; ENV_ADDR="$sh:$sp"
|
||||
hist_event copy fail "" "to $dname db=$sdb"
|
||||
c_err "复制失败(上方输出)"
|
||||
return 1
|
||||
}
|
||||
|
||||
db_ls() {
|
||||
local name="${1:-}"
|
||||
[ -n "$name" ] || { c_err "用法:scripts/svc.sh db ls <name>"; exit 2; }
|
||||
env_config "$name"
|
||||
[ "$SVC_KIND" = postgres ] || { c_err "[$ENV_NAME] kind=$SVC_KIND 不是 postgres"; exit 2; }
|
||||
[ -n "$(read_pid)" ] && pid_alive "$(read_pid)" || { c_err "[$ENV_NAME] 未运行——先 start"; exit 1; }
|
||||
pg_run "${ENV_ADDR%:*}" "${ENV_ADDR##*:}" "$(svc_get user)" "$(svc_get password)" postgres \
|
||||
"SELECT datname FROM pg_database WHERE NOT datistemplate ORDER BY 1" tA || true
|
||||
return 0
|
||||
}
|
||||
|
||||
cmd_set() {
|
||||
local name="${1:-}"
|
||||
[ -n "$name" ] || { c_err "用法:scripts/svc.sh set <name> [--port N] [--host H] [--apply] [--tag T...]"; exit 2; }
|
||||
shift
|
||||
parse_args "$@"
|
||||
env_config "$name"
|
||||
[ -f "$ENV_DIR/svc.env" ] || { c_err "[$ENV_NAME] 无登记(先 db create / deploy)"; exit 2; }
|
||||
# tags:给了 --tag 才改(合并去重;postgres/agent/static 通用)。
|
||||
if [ "${#CLI_TAGS[@]}" -gt 0 ]; then
|
||||
local merged; merged="$(svc_merge_tags)"
|
||||
svc_set_key tags "$merged"
|
||||
c_ok "[$ENV_NAME] 标签更新:$merged"
|
||||
fi
|
||||
case "$SVC_KIND" in
|
||||
postgres)
|
||||
if [ -n "$CLI_ADDR$CLI_HOST$CLI_PORT" ]; then
|
||||
local old; old="$(svc_get addr)"; [ -n "$old" ] || old="$ENV_ADDR"
|
||||
local running=0
|
||||
if [ -n "$(read_pid)" ] && pid_alive "$(read_pid)"; then running=1; fi
|
||||
if [ "$running" = 1 ] && [ "$CLI_APPLY" != 1 ]; then
|
||||
c_err "[$ENV_NAME] 运行中——端口/地址变更需重启生效:加 --apply(停止→改登记→重启),或先 stop 再 set"
|
||||
exit 1
|
||||
fi
|
||||
check_port_available "${ENV_ADDR##*:}" "$ENV_NAME" || exit 1
|
||||
if [ "$running" = 1 ]; then
|
||||
pg_do_stop
|
||||
svc_set_key addr "$ENV_ADDR"
|
||||
pg_do_start || { c_err "[$ENV_NAME] 新地址重启失败(登记已是 $ENV_ADDR);查看 $ENV_DIR/pg.log"; return 1; }
|
||||
else
|
||||
svc_set_key addr "$ENV_ADDR"
|
||||
fi
|
||||
hist_event set ok "$(read_pid)" "addr $old → $ENV_ADDR"
|
||||
c_ok "[$ENV_NAME] 地址已更新:$old → $ENV_ADDR"
|
||||
elif [ "${#CLI_TAGS[@]}" -eq 0 ]; then
|
||||
c_warn "[$ENV_NAME] 未指定任何变更(--port/--host/--addr/--tag)"
|
||||
fi
|
||||
;;
|
||||
agent|static)
|
||||
[ -n "$CLI_ADDR$CLI_HOST$CLI_PORT" ] && \
|
||||
c_warn "[$ENV_NAME] agent/static 换端口请用 restart --port N(进程与登记同步更新);set 只改登记"
|
||||
[ "${#CLI_TAGS[@]}" -eq 0 ] && c_warn "[$ENV_NAME] 未指定任何变更(--tag)"
|
||||
;;
|
||||
*) c_err "[$ENV_NAME] 未知 kind:$SVC_KIND"; return 1 ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---- 生命周期分派 ----
|
||||
|
||||
do_start() {
|
||||
@@ -1313,6 +1741,7 @@ main() {
|
||||
exit 0
|
||||
;;
|
||||
db) shift; cmd_db "$@"; exit 0 ;;
|
||||
set) shift; cmd_set "$@"; exit 0 ;;
|
||||
rm) shift; cmd_rm "$@"; exit 0 ;;
|
||||
commits) shift; cmd_commits "$@"; exit 0 ;;
|
||||
history) shift; cmd_history "$@"; exit 0 ;;
|
||||
|
||||
Reference in New Issue
Block a user