完善异机备份告警与同日恢复点取代(07-27-automatic-offhost-backup)
这个提交包含在:
@@ -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"',
|
||||
|
||||
在新工单中引用
屏蔽一个用户