完善异机备份告警与同日恢复点取代(07-27-automatic-offhost-backup)
ci-e2e / e2e (push) Waiting to run
ci-minimal / check-minimal (push) Waiting to run

这个提交包含在:
2026-07-27 13:54:16 +08:00
父节点 2526052e73
当前提交 a0006f07b8
共修改 15 个文件,包含 882 行新增和 105 行删除
+59 -1
查看文件
@@ -1,6 +1,7 @@
#!/usr/bin/env node
const fs = require('node:fs');
const path = require('node:path');
const childProcess = require('node:child_process');
const rootDir = path.resolve(__dirname, '..', '..');
const configPath = path.join(rootDir, 'server/src/config.js');
@@ -29,6 +30,8 @@ const offhostReceiverInstallerPath = path.join(
);
const automaticBackupRunnerPath = path.join(rootDir, 'scripts/run-automatic-backup.sh');
const offhostReceiverPath = path.join(rootDir, 'scripts/receive-offhost-backup.sh');
const groupAlertHelperPath = path.join(rootDir, 'scripts/send-wecom-group-alert.py');
const offhostDeadmanPath = path.join(rootDir, 'scripts/check-offhost-backup-deadman.py');
const nginxPaths = [
path.join(rootDir, 'deploy/trialrun/nginx-hyb-oa.conf'),
path.join(rootDir, 'deploy/trialrun/nginx.conf.example'),
@@ -157,6 +160,8 @@ const automaticBackupTimerScriptContent = readFile(automaticBackupTimerScriptPat
const offhostReceiverInstallerContent = readFile(offhostReceiverInstallerPath);
const automaticBackupRunnerContent = readFile(automaticBackupRunnerPath);
const offhostReceiverContent = readFile(offhostReceiverPath);
const groupAlertHelperContent = readFile(groupAlertHelperPath);
const offhostDeadmanContent = readFile(offhostDeadmanPath);
const nginxContents = nginxPaths.map((filePath) => ({
filePath,
content: readFile(filePath),
@@ -185,6 +190,25 @@ for (const key of ['APP_HOST_PORT', 'POSTGRES_DATA_DIR', 'STORAGE_HOST_DIR']) {
if (envExampleKeys.has('COMPOSE_PROJECT_NAME')) {
errors.push('.env.example 不应持久化 COMPOSE_PROJECT_NAME,调用方必须通过 -p 显式选择项目');
}
for (const key of ['ALERT_WEBHOOK_FILE', 'WECOM_WEBHOOK_URL', 'WECOM_WEBHOOK_KEY']) {
if (envExampleKeys.has(key)) {
errors.push(`.env.example 不应持久化企微群机器人配置 ${key}`);
}
}
const trackedFiles = childProcess.execFileSync('git', ['ls-files', '--cached', '--others', '--exclude-standard', '-z'], {
cwd: rootDir,
encoding: 'utf8',
}).split('\0').filter(Boolean);
const literalWebhookPattern = /https:\/\/qyapi\.weixin\.qq\.com\/cgi-bin\/webhook\/send\?key=/;
for (const relativePath of trackedFiles) {
const filePath = path.join(rootDir, relativePath);
if (!fs.statSync(filePath).isFile()) continue;
const content = fs.readFileSync(filePath);
if (content.includes(0)) continue;
if (literalWebhookPattern.test(content.toString('utf8'))) {
errors.push(`${relativePath} 不得持久化完整企微群机器人 Webhook URL`);
}
}
for (const serviceName of requiredServices) {
const block = extractServiceBlock(composeContent, serviceName);
@@ -306,6 +330,8 @@ for (const requiredFragment of [
'Environment=OFFHOST_TARGET=${OFFHOST_TARGET}',
'Environment=OFFHOST_SSH_KEY=${OFFHOST_SSH_KEY}',
'Environment=OFFHOST_KNOWN_HOSTS=${OFFHOST_KNOWN_HOSTS}',
'Environment=ALERT_WEBHOOK_FILE=${ALERT_WEBHOOK_FILE}',
'Environment=ALERT_HELPER=${ALERT_HELPER}',
'install)\n install_units',
'enable)\n enable_units',
]) {
@@ -325,7 +351,10 @@ for (const requiredFragment of [
'hyb-backup-finalize $run_id $manifest_sha',
'write_success_state "$run_id" "$manifest_sha"',
'prune_local_backups "$batch_name"',
'--summary-file=-',
'python3 "$ALERT_HELPER"',
'--webhook-file "$ALERT_WEBHOOK_FILE"',
'capture_command offhost_transfer rsync',
'last-error.log',
"docker inspect -f '{{json .Mounts}}'",
'if len(matches) != 1 or not matches[0]:',
]) {
@@ -345,6 +374,8 @@ for (const requiredFragment of [
'batch must contain exactly three regular files',
'manifest artifact checksum mismatch',
'mv -- "$partial_dir" "$archive_dir"',
'replacement_rollback=',
'publish_weekly_checkpoint',
'prune_named_directories "$BACKUP_ARCHIVE_DIR"',
'prune_named_directories "$BACKUP_WEEKLY_DIR"',
]) {
@@ -360,6 +391,9 @@ for (const requiredFragment of [
'BACKUP_INBOX_DIR=${inbox_dir}',
'BACKUP_ARCHIVE_DIR=${archive_dir}',
'BACKUP_WEEKLY_DIR=${weekly_dir}',
'DEADMAN_MAX_AGE_SECONDS="${DEADMAN_MAX_AGE_SECONDS:-93600}"',
'ExecStart=/usr/bin/python3 ${DEADMAN_TARGET}',
'systemctl enable --now "${DEADMAN_SERVICE}.timer"',
'archives retained',
'backup root contains an unmanaged top-level entry',
'backup root must not be inside a system-managed directory',
@@ -369,6 +403,30 @@ for (const requiredFragment of [
}
}
for (const requiredFragment of [
'stat.S_IMODE(info.st_mode) != 0o600',
'parsed.hostname != "qyapi.weixin.qq.com"',
'HYB_WECOM_ALERT_TEST_MODE',
'should_send(previous, status, category)',
'不要在群内粘贴原始日志',
]) {
if (!groupAlertHelperContent.includes(requiredFragment)) {
errors.push(`send-wecom-group-alert.py 缺少独立脱敏告警合同: ${requiredFragment}`);
}
}
for (const requiredFragment of [
'default=93600',
'archive_manifest_invalid',
'archive_time_in_future',
'FAIL_OFFHOST_DEADMAN',
'deadman-alert-state.json',
]) {
if (!offhostDeadmanContent.includes(requiredFragment)) {
errors.push(`check-offhost-backup-deadman.py 缺少 ARM 独立看门狗合同: ${requiredFragment}`);
}
}
for (const requiredFragment of [
'validate_data_path POSTGRES_DATA_DIR "$POSTGRES_DATA_DIR"',
'validate_data_path STORAGE_HOST_DIR "$STORAGE_HOST_DIR"',