Files
homelab-infra/infrastructure/shared-postgresql/tests/live_restore.py
T
panxiao81 3a2fe5fa0c
yaml / yaml (pull_request) Successful in 41s
ansible / collection-test (pull_request) Successful in 2m41s
terraform / validate (pull_request) Successful in 2m41s
ansible / lint (pull_request) Successful in 4m36s
feat: 纳管共享 etcd 与 k3s 外 PostgreSQL 高可用及备份
2026-09-25 19:34:48 +00:00

64 lines
3.0 KiB
Python

#!/usr/bin/env python3
"""将真实 pgBackRest 备份恢复到临时目录,仅用私有 Unix socket 启动并验证。"""
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import time
def run(args, **kwargs):
return subprocess.run(['sudo', '-n', '-u', 'pgprod', *args],
check=True, capture_output=True, text=True, timeout=240, **kwargs)
def main():
if sys.argv[1:] != ['--run']:
raise SystemExit('需显式 --run;仅恢复到新的临时目录,不接触原 PGDATA。')
root = Path(tempfile.mkdtemp(prefix='homelab-pg-restore-', dir='/tmp'))
data = root / 'data'
started = False
try:
(root / 'pg_hba.conf').write_text('local all pgprod peer\n')
(root / 'test.conf').write_text(
f"data_directory='{data}'\nhba_file='{root}/pg_hba.conf'\n"
f"unix_socket_directories='{root}'\nunix_socket_permissions=0700\n"
"listen_addresses=''\nport=55432\nssl=off\narchive_mode=off\n"
"shared_buffers=32MB\nmax_connections=80\nmax_wal_senders=5\n"
"hot_standby=on\nprimary_conninfo=''\n")
subprocess.run(['sudo', '-n', 'chown', '-R', 'pgprod:pgprod', str(root)], check=True)
run(['pgbackrest', '--config=/etc/homelab-postgresql/prod/pgbackrest.conf', '--stanza=prod',
'--pg1-path=' + str(data), '--type=immediate', '--target-action=promote',
'--archive-mode=off', 'restore'])
print('真实仓库备份已恢复到临时目录', flush=True)
run(['/usr/lib/postgresql/18/bin/pg_ctl', '-D', str(data), '-l', str(root / 'server.log'),
'-o', '-c config_file=' + str(root / 'test.conf'), '-w', '-t', '60', 'start'])
started = True
# pg_ctl ready 可早于 WAL recovery 完成;等待恢复结束并可写。
for _ in range(60):
result = run(['/usr/lib/postgresql/18/bin/psql', '-X', '-At', '-h', str(root), '-p', '55432',
'-d', 'postgres', '-c',
"SELECT NOT pg_is_in_recovery() AND NOT rolsuper AND rolcreatedb AND rolcreaterole "
"FROM pg_roles WHERE rolname='ayatori'"])
if result.stdout.strip() == 't':
break
time.sleep(1)
else:
raise RuntimeError('恢复后 SQL/管理角色验证未通过')
print('PASS:恢复实例可写、Ayatori 管理角色属性正确;仅私有 Unix socket', flush=True)
except subprocess.CalledProcessError as e:
print(e.stderr, file=sys.stderr)
raise
finally:
# pg_ctl start 超时也可能已经创建 postmaster,清理前必须先确认停止。
if started or (data / 'postmaster.pid').exists():
run(['/usr/lib/postgresql/18/bin/pg_ctl', '-D', str(data), '-m', 'immediate', '-w', 'stop'])
subprocess.run(['sudo', '-n', 'chown', '-R', str(os.getuid()) + ':' + str(os.getgid()), str(root)], check=True)
shutil.rmtree(root)
if __name__ == '__main__':
main()