完成真实成对异机备份(07-26-internal-ops-recovery-baseline)
这个提交包含在:
+45
-2
@@ -67,8 +67,13 @@ STORAGE_BYTES="$(wc -c <"$STORAGE_FILE" | tr -d ' ')"
|
||||
BUILD_SHA="$(git -C "$ROOT_DIR" rev-parse --short=12 HEAD 2>/dev/null || true)"
|
||||
CREATED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
|
||||
node - "$TEMP_DIR/$MANIFEST_NAME" "$BACKUP_RUN_ID" "$CREATED_AT" "$BUILD_SHA" \
|
||||
"$DB_NAME" "$DB_BYTES" "$DB_SHA256" "$STORAGE_NAME" "$STORAGE_BYTES" "$STORAGE_SHA256" <<'NODE'
|
||||
MANIFEST_ARGS=(
|
||||
"$TEMP_DIR/$MANIFEST_NAME" "$BACKUP_RUN_ID" "$CREATED_AT" "$BUILD_SHA"
|
||||
"$DB_NAME" "$DB_BYTES" "$DB_SHA256" "$STORAGE_NAME" "$STORAGE_BYTES" "$STORAGE_SHA256"
|
||||
)
|
||||
|
||||
if command -v node >/dev/null 2>&1; then
|
||||
node - "${MANIFEST_ARGS[@]}" <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const [manifestPath, runId, createdAt, buildSha, dbFile, dbBytes, dbSha256, storageFile, storageBytes, storageSha256] = process.argv.slice(2);
|
||||
const manifest = {
|
||||
@@ -84,6 +89,44 @@ const manifest = {
|
||||
};
|
||||
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
|
||||
NODE
|
||||
elif command -v python3 >/dev/null 2>&1; then
|
||||
python3 - "${MANIFEST_ARGS[@]}" <<'PYTHON'
|
||||
import json
|
||||
import sys
|
||||
|
||||
(
|
||||
manifest_path,
|
||||
run_id,
|
||||
created_at,
|
||||
build_sha,
|
||||
db_file,
|
||||
db_bytes,
|
||||
db_sha256,
|
||||
storage_file,
|
||||
storage_bytes,
|
||||
storage_sha256,
|
||||
) = sys.argv[1:]
|
||||
|
||||
manifest = {
|
||||
"schemaVersion": 1,
|
||||
"runId": run_id,
|
||||
"createdAt": created_at,
|
||||
"buildSha": build_sha or None,
|
||||
"status": "complete",
|
||||
"artifacts": {
|
||||
"database": {"file": db_file, "bytes": int(db_bytes), "sha256": db_sha256},
|
||||
"storage": {"file": storage_file, "bytes": int(storage_bytes), "sha256": storage_sha256},
|
||||
},
|
||||
}
|
||||
|
||||
with open(manifest_path, "x", encoding="utf-8") as manifest_file:
|
||||
json.dump(manifest, manifest_file, ensure_ascii=True, indent=2)
|
||||
manifest_file.write("\n")
|
||||
PYTHON
|
||||
else
|
||||
echo "[ERR] node or python3 is required to write the backup manifest" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mv "$TEMP_DIR" "$FINAL_DIR"
|
||||
TEMP_DIR=""
|
||||
|
||||
@@ -24,9 +24,13 @@ require_command() {
|
||||
fi
|
||||
}
|
||||
|
||||
for command_name in docker pg_dump pg_restore psql tar sha256sum node; do
|
||||
for command_name in docker pg_dump pg_restore psql tar sha256sum; do
|
||||
require_command "$command_name"
|
||||
done
|
||||
if ! command -v node >/dev/null 2>&1 && ! command -v python3 >/dev/null 2>&1; then
|
||||
echo "[ERR] required command missing: node or python3" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
START_EPOCH="$(date +%s)"
|
||||
SOURCE_STORAGE="$DRILL_DIR/source-storage"
|
||||
@@ -64,7 +68,8 @@ BATCH_DIR="$BACKUP_ROOT/hyb-oa-backup-$DRILL_RUN_ID"
|
||||
DB_BACKUP="$BATCH_DIR/database.dump"
|
||||
STORAGE_BACKUP="$BATCH_DIR/storage.tar.gz"
|
||||
|
||||
node - "$BATCH_DIR/manifest.json" "$DB_BACKUP" "$STORAGE_BACKUP" <<'NODE'
|
||||
if command -v node >/dev/null 2>&1; then
|
||||
node - "$BATCH_DIR/manifest.json" "$DB_BACKUP" "$STORAGE_BACKUP" <<'NODE'
|
||||
const crypto = require("node:crypto");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
@@ -80,6 +85,39 @@ const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file
|
||||
if (sha256(dbPath) !== manifest.artifacts.database.sha256) throw new Error("database checksum mismatch");
|
||||
if (sha256(storagePath) !== manifest.artifacts.storage.sha256) throw new Error("storage checksum mismatch");
|
||||
NODE
|
||||
else
|
||||
python3 - "$BATCH_DIR/manifest.json" "$DB_BACKUP" "$STORAGE_BACKUP" <<'PYTHON'
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
manifest_path, db_path, storage_path = sys.argv[1:]
|
||||
with open(manifest_path, encoding="utf-8") as manifest_file:
|
||||
manifest_text = manifest_file.read()
|
||||
manifest = json.loads(manifest_text)
|
||||
if manifest.get("status") != "complete" or manifest.get("schemaVersion") != 1:
|
||||
raise RuntimeError("manifest contract mismatch")
|
||||
for artifact in (manifest["artifacts"]["database"], manifest["artifacts"]["storage"]):
|
||||
if os.path.isabs(artifact["file"]):
|
||||
raise RuntimeError("manifest contains absolute artifact path")
|
||||
if re.search(r"postgresql:|DATABASE_URL|drill_password", manifest_text):
|
||||
raise RuntimeError("manifest contains sensitive connection data")
|
||||
|
||||
def sha256(file_path):
|
||||
digest = hashlib.sha256()
|
||||
with open(file_path, "rb") as artifact_file:
|
||||
for block in iter(lambda: artifact_file.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
if sha256(db_path) != manifest["artifacts"]["database"]["sha256"]:
|
||||
raise RuntimeError("database checksum mismatch")
|
||||
if sha256(storage_path) != manifest["artifacts"]["storage"]["sha256"]:
|
||||
raise RuntimeError("storage checksum mismatch")
|
||||
PYTHON
|
||||
fi
|
||||
|
||||
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -qc 'DROP TABLE restore_drill_marker'
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户