Smail/test/tools/deploy_review.py
smail bf1e69f480
Some checks are pending
Security Checks / security (push) Waiting to run
修复生产审查凭据泄露风险并增加持久投递队列
2026-09-17 10:17:09 +08:00

122 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""仅在 47 服务器备份并升级 Smail,保留双库和失败证据。"""
import argparse
import hashlib
import json
import os
from pathlib import Path
import shutil
import sqlite3
import subprocess
import time
ROOT = Path('/opt/smail')
STAGING = Path('/var/tmp/smail-review-20260917')
DATABASES = ('smail.db', 'smail-state.db')
def run(*args):
return subprocess.check_output(args, text=True).strip()
def digest(path):
with path.open('rb') as stream:
return hashlib.file_digest(stream, 'sha256').hexdigest()
def snapshot(source, target):
with sqlite3.connect(f'file:{source}?mode=ro', uri=True) as original:
with sqlite3.connect(target) as copied:
original.backup(copied)
assert copied.execute('PRAGMA integrity_check').fetchone()[0] == 'ok'
target.chmod(0o600)
def wait_ready():
for _ in range(60):
result = subprocess.run([
'curl', '--silent', '--fail', '--max-time', '2', '--noproxy', '*',
'--cacert', str(ROOT / 'ssl/custom.crt'), '--resolve',
'mail.solit.cn:443:127.0.0.1', 'https://mail.solit.cn/api/ping',
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
if result.returncode == 0 and run('systemctl', 'is-active', 'smail.service') == 'active':
return
time.sleep(1)
raise RuntimeError('升级后服务未就绪')
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--main-sha', required=True)
parser.add_argument('--admin-sha', required=True)
args = parser.parse_args()
os.umask(0o077)
assert os.geteuid() == 0 and ROOT.resolve() == ROOT and not ROOT.is_symlink()
assert run('hostname') == 'ikun2'
assert '172.31.134.42/20' in run('ip', '-4', 'address', 'show')
assert run('systemctl', 'show', 'smail.service', '--property=User', '--value') == 'smail'
assert run('systemctl', 'is-active', 'smail.service') == 'active'
expected = {'smail': args.main_sha, 'smail-admin': args.admin_sha}
for name, checksum in expected.items():
path = STAGING / name
with path.open('rb') as binary:
header = binary.read(20)
assert header[:5] == b'\x7fELF\x02' and header[18:20] == b'\x3e\x00'
assert digest(path) == checksum
assert not (ROOT / name).is_symlink()
backup = Path('/var/backups') / time.strftime('smail-before-outbox-%Y%m%d-%H%M%S')
backup.mkdir(mode=0o700)
installed = False
subprocess.run(['systemctl', 'stop', 'smail.service'], check=True)
try:
# 停服归档包含配置、密钥和日志,仅保存在远程受保护目录。
archive = backup / 'smail.tar.gz'
subprocess.run(['tar', '-czf', str(archive), '-C', '/opt', 'smail'], check=True)
subprocess.run(['tar', '-dzf', str(archive), '-C', '/opt'], check=True)
archive.chmod(0o600)
for name in DATABASES:
source = ROOT / 'data' / name
assert source.resolve() == source and not source.is_symlink()
snapshot(source, backup / name)
for name in expected:
shutil.copy2(ROOT / name, backup / name)
installed = True
for name, checksum in expected.items():
subprocess.run(['install', '-m', '0755', '-o', 'root', '-g', 'root',
str(STAGING / name), str(ROOT / name)], check=True)
assert digest(ROOT / name) == checksum
subprocess.run(['systemctl', 'start', 'smail.service'], check=True)
wait_ready()
for name in DATABASES:
with sqlite3.connect(f'file:{ROOT / "data" / name}?mode=ro', uri=True) as database:
assert database.execute('PRAGMA integrity_check').fetchone()[0] == 'ok'
with sqlite3.connect(f'file:{ROOT / "data/smail.db"}?mode=ro', uri=True) as database:
version = database.execute('SELECT MAX(version) FROM schema_migration').fetchone()[0]
assert version == 23
states = dict(database.execute('SELECT state,COUNT(*) FROM outbound_delivery GROUP BY state'))
result = {'backup': str(backup), 'archive_sha256': digest(archive),
'artifacts': expected, 'migration': version, 'queue_states': states,
'service': run('systemctl', 'is-active', 'smail.service'),
'admin_version': run(str(ROOT / 'smail-admin'), 'version'),
'database_integrity': 'ok', 'database_reset': False}
(backup / 'result.json').write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding='utf-8')
print(json.dumps(result, ensure_ascii=False, indent=2))
except BaseException:
if installed:
subprocess.run(['systemctl', 'stop', 'smail.service'], check=True)
for name in DATABASES:
snapshot(ROOT / 'data' / name, backup / ('failed-' + name))
snapshot(backup / name, ROOT / 'data' / name)
shutil.chown(ROOT / 'data' / name, user='smail', group='smail')
for name in expected:
shutil.copy2(backup / name, ROOT / name)
subprocess.run(['systemctl', 'start', 'smail.service'], check=True)
print('升级失败,备份及故障证据保留于:' + str(backup))
raise
if __name__ == '__main__':
main()