fix(agent99): deploy backup freshness exporter atomically
This commit is contained in:
@@ -7,6 +7,8 @@ umask 077
|
||||
readonly EXPECTED_HOST_IP="192.168.0.110"
|
||||
readonly EXPECTED_USER="wooo"
|
||||
readonly DEST_ROOT="/backup/scripts"
|
||||
readonly EXPORTER_ROOT="/home/wooo/scripts"
|
||||
readonly EXPORTER_FILE="backup-health-textfile-exporter.py"
|
||||
readonly STATUS_ROOT="/backup/status"
|
||||
readonly STAGE_ROOT="/backup/.agent99-backup-runtime-stage"
|
||||
readonly ROLLBACK_ROOT="/backup/.agent99-backup-runtime-rollback"
|
||||
@@ -31,6 +33,7 @@ readonly -a RUNTIME_FILES=(
|
||||
verify-offsite-full-sync.sh
|
||||
enforce-latest-only-retention.sh
|
||||
)
|
||||
readonly -a PAYLOAD_FILES=("${RUNTIME_FILES[@]}" "$EXPORTER_FILE")
|
||||
|
||||
MODE=""
|
||||
SOURCE_REVISION=""
|
||||
@@ -46,13 +49,14 @@ APPLY_COMPLETE=0
|
||||
ROLLBACK_VERIFIED=0
|
||||
declare -A FILE_EXISTED=()
|
||||
declare -A PREVIOUS_DIGEST=()
|
||||
declare -A PREVIOUS_METADATA=()
|
||||
declare -A EXPECTED_DIGEST=()
|
||||
|
||||
usage() {
|
||||
printf '%s\n' \
|
||||
"Usage: $0 --mode check|apply|verify --source-revision SHA --run-id ID --source-stage PATH" \
|
||||
"The executor is fixed to host 192.168.0.110, an Agent99 run-bound source stage," \
|
||||
"and the 17-file /backup/scripts transaction."
|
||||
"and the 18-file transaction: 17 /backup/scripts files plus the backup health exporter."
|
||||
}
|
||||
|
||||
fail() {
|
||||
@@ -93,7 +97,7 @@ case "$MODE" in check|apply|verify) ;; *) fail "invalid_mode" ;; esac
|
||||
expected_source_stage="/tmp/agent99-host110-backup-runtime-${RUN_ID}"
|
||||
[ "$SOURCE_STAGE" = "$expected_source_stage" ] || fail "invalid_source_stage"
|
||||
|
||||
for command_name in awk bash cp flock grep hostname id install mv pgrep python3 readlink rm sha256sum stat timeout tr; do
|
||||
for command_name in awk bash cp dirname flock grep hostname id install mv pgrep python3 readlink rm sha256sum stat timeout tr; do
|
||||
command -v "$command_name" >/dev/null 2>&1 || fail "required_command_missing_${command_name}"
|
||||
done
|
||||
|
||||
@@ -102,8 +106,9 @@ current_user="$(id -un)"
|
||||
hostname -I 2>/dev/null | tr ' ' '\n' | grep -qx "$EXPECTED_HOST_IP" || fail "unexpected_executor_host"
|
||||
[ -d "$SOURCE_STAGE" ] && [ ! -L "$SOURCE_STAGE" ] || fail "canonical_source_stage_unavailable"
|
||||
[ -d "$DEST_ROOT" ] && [ ! -L "$DEST_ROOT" ] || fail "backup_runtime_destination_unavailable"
|
||||
[ -d "$EXPORTER_ROOT" ] && [ ! -L "$EXPORTER_ROOT" ] || fail "backup_exporter_destination_unavailable"
|
||||
|
||||
manifest_rows="$(python3 - "$SOURCE_STAGE/manifest.json" "$SOURCE_REVISION" "$RUN_ID" "${RUNTIME_FILES[@]}" <<'PY'
|
||||
manifest_rows="$(python3 - "$SOURCE_STAGE/manifest.json" "$SOURCE_REVISION" "$RUN_ID" "${PAYLOAD_FILES[@]}" <<'PY'
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
@@ -184,7 +189,7 @@ source_digest() {
|
||||
|
||||
validate_source() {
|
||||
local name source_path expected_digest actual_digest resolved
|
||||
for name in "${RUNTIME_FILES[@]}"; do
|
||||
for name in "${PAYLOAD_FILES[@]}"; do
|
||||
source_path="$SOURCE_STAGE/$name"
|
||||
[ -f "$source_path" ] && [ ! -L "$source_path" ] || return 65
|
||||
resolved="$(readlink -f "$source_path")"
|
||||
@@ -196,10 +201,19 @@ validate_source() {
|
||||
done
|
||||
}
|
||||
|
||||
destination_path() {
|
||||
local name="$1"
|
||||
if [ "$name" = "$EXPORTER_FILE" ]; then
|
||||
printf '%s/%s\n' "$EXPORTER_ROOT" "$name"
|
||||
else
|
||||
printf '%s/%s\n' "$DEST_ROOT" "$name"
|
||||
fi
|
||||
}
|
||||
|
||||
verify_destination() {
|
||||
local name dest_path expected_digest actual_digest
|
||||
for name in "${RUNTIME_FILES[@]}"; do
|
||||
dest_path="$DEST_ROOT/$name"
|
||||
for name in "${PAYLOAD_FILES[@]}"; do
|
||||
dest_path="$(destination_path "$name")"
|
||||
[ -f "$dest_path" ] && [ ! -L "$dest_path" ] || return 68
|
||||
expected_digest="$(source_digest "$name")"
|
||||
actual_digest="$(working_digest "$dest_path")"
|
||||
@@ -227,7 +241,9 @@ document = {
|
||||
"sourceRevision": sys.argv[3],
|
||||
"sourceHead": sys.argv[4],
|
||||
"runId": sys.argv[5],
|
||||
"fileCount": 17,
|
||||
"fileCount": 18,
|
||||
"backupScriptFileCount": 17,
|
||||
"backupHealthExporterIncluded": True,
|
||||
"rollbackPerformed": sys.argv[6] == "1",
|
||||
"rollbackVerified": sys.argv[6] == "1",
|
||||
"verifierSha256": sys.argv[7],
|
||||
@@ -248,23 +264,26 @@ PY
|
||||
}
|
||||
|
||||
record_prestate() {
|
||||
local name dest_path digest
|
||||
local name dest_path digest metadata
|
||||
: > "$ROLLBACK_DIR/prestate.tsv"
|
||||
chmod 600 "$ROLLBACK_DIR/prestate.tsv"
|
||||
for name in "${RUNTIME_FILES[@]}"; do
|
||||
dest_path="$DEST_ROOT/$name"
|
||||
for name in "${PAYLOAD_FILES[@]}"; do
|
||||
dest_path="$(destination_path "$name")"
|
||||
if [ -f "$dest_path" ] && [ ! -L "$dest_path" ]; then
|
||||
[ "$(stat -c '%U:%G' "$dest_path")" = "wooo:wooo" ] || return 71
|
||||
FILE_EXISTED[$name]=1
|
||||
digest="$(working_digest "$dest_path")"
|
||||
metadata="$(stat -c '%u:%g:%a' "$dest_path")"
|
||||
PREVIOUS_DIGEST[$name]="$digest"
|
||||
printf '%s\t1\t%s\n' "$name" "$digest" >> "$ROLLBACK_DIR/prestate.tsv"
|
||||
PREVIOUS_METADATA[$name]="$metadata"
|
||||
printf '%s\t1\t%s\t%s\n' "$name" "$digest" "$metadata" >> "$ROLLBACK_DIR/prestate.tsv"
|
||||
cp -p -- "$dest_path" "$ROLLBACK_DIR/$name"
|
||||
[ "$(working_digest "$ROLLBACK_DIR/$name")" = "$digest" ] || return 72
|
||||
elif [ ! -e "$dest_path" ]; then
|
||||
FILE_EXISTED[$name]=0
|
||||
PREVIOUS_DIGEST[$name]="-"
|
||||
printf '%s\t0\t-\n' "$name" >> "$ROLLBACK_DIR/prestate.tsv"
|
||||
PREVIOUS_METADATA[$name]="-"
|
||||
printf '%s\t0\t-\t-\n' "$name" >> "$ROLLBACK_DIR/prestate.tsv"
|
||||
else
|
||||
return 73
|
||||
fi
|
||||
@@ -272,14 +291,15 @@ record_prestate() {
|
||||
}
|
||||
|
||||
rollback_transaction() {
|
||||
local name dest_path backup_path temporary failed=0
|
||||
local name dest_path destination_parent backup_path temporary failed=0
|
||||
set +e
|
||||
for name in "${RUNTIME_FILES[@]}"; do
|
||||
for name in "${PAYLOAD_FILES[@]}"; do
|
||||
[ -n "${FILE_EXISTED[$name]+x}" ] || { failed=1; continue; }
|
||||
dest_path="$DEST_ROOT/$name"
|
||||
dest_path="$(destination_path "$name")"
|
||||
if [ "${FILE_EXISTED[$name]}" = "1" ]; then
|
||||
backup_path="$ROLLBACK_DIR/$name"
|
||||
temporary="$DEST_ROOT/.${name}.agent99-rollback-${RUN_ID}"
|
||||
destination_parent="$(dirname "$dest_path")"
|
||||
temporary="$destination_parent/.${name}.agent99-rollback-${RUN_ID}"
|
||||
cp -p -- "$backup_path" "$temporary" \
|
||||
&& mv -f -- "$temporary" "$dest_path" \
|
||||
|| failed=1
|
||||
@@ -287,11 +307,12 @@ rollback_transaction() {
|
||||
rm -f -- "$dest_path" || failed=1
|
||||
fi
|
||||
done
|
||||
for name in "${RUNTIME_FILES[@]}"; do
|
||||
dest_path="$DEST_ROOT/$name"
|
||||
for name in "${PAYLOAD_FILES[@]}"; do
|
||||
dest_path="$(destination_path "$name")"
|
||||
if [ "${FILE_EXISTED[$name]:-}" = "1" ]; then
|
||||
[ -f "$dest_path" ] && [ ! -L "$dest_path" ] \
|
||||
&& [ "$(working_digest "$dest_path" 2>/dev/null)" = "${PREVIOUS_DIGEST[$name]:-invalid}" ] \
|
||||
&& [ "$(stat -c '%u:%g:%a' "$dest_path" 2>/dev/null)" = "${PREVIOUS_METADATA[$name]:-invalid}" ] \
|
||||
|| failed=1
|
||||
elif [ "${FILE_EXISTED[$name]:-}" = "0" ]; then
|
||||
[ ! -e "$dest_path" ] || failed=1
|
||||
@@ -328,13 +349,13 @@ cleanup() {
|
||||
validate_source || fail "source_validation_failed"
|
||||
|
||||
if [ "$MODE" = "check" ]; then
|
||||
printf '{"schemaVersion":"agent99_host110_backup_runtime_executor_v1","mode":"check","ok":true,"sourceRevision":"%s","sourceHead":"%s","fileCount":17,"remoteWritePerformed":false}\n' "$SOURCE_REVISION" "$SOURCE_HEAD"
|
||||
printf '{"schemaVersion":"agent99_host110_backup_runtime_executor_v1","mode":"check","ok":true,"sourceRevision":"%s","sourceHead":"%s","fileCount":18,"backupScriptFileCount":17,"backupHealthExporterIncluded":true,"remoteWritePerformed":false}\n' "$SOURCE_REVISION" "$SOURCE_HEAD"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$MODE" = "verify" ]; then
|
||||
verify_destination || fail "destination_verification_failed"
|
||||
printf '{"schemaVersion":"agent99_host110_backup_runtime_executor_v1","mode":"verify","ok":true,"sourceRevision":"%s","sourceHead":"%s","fileCount":17,"remoteWritePerformed":false}\n' "$SOURCE_REVISION" "$SOURCE_HEAD"
|
||||
printf '{"schemaVersion":"agent99_host110_backup_runtime_executor_v1","mode":"verify","ok":true,"sourceRevision":"%s","sourceHead":"%s","fileCount":18,"backupScriptFileCount":17,"backupHealthExporterIncluded":true,"remoteWritePerformed":false}\n' "$SOURCE_REVISION" "$SOURCE_HEAD"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -362,7 +383,7 @@ trap cleanup EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM HUP
|
||||
|
||||
for name in "${RUNTIME_FILES[@]}"; do
|
||||
for name in "${PAYLOAD_FILES[@]}"; do
|
||||
install -m 700 "$SOURCE_STAGE/$name" "$STAGE_DIR/$name"
|
||||
[ "$(working_digest "$STAGE_DIR/$name")" = "$(source_digest "$name")" ] || fail "stage_identity_failed"
|
||||
validate_file "$STAGE_DIR/$name" || fail "stage_validation_failed"
|
||||
@@ -374,16 +395,19 @@ APPLY_STARTED=1
|
||||
# common.sh is promoted first. Every managed mutating backup entrypoint then
|
||||
# takes the shared side of LOCK_PATH before doing work, while this process holds
|
||||
# the exclusive side until the independent verifier has completed.
|
||||
for name in "${RUNTIME_FILES[@]}"; do
|
||||
temporary="$DEST_ROOT/.${name}.agent99-${RUN_ID}"
|
||||
for name in "${PAYLOAD_FILES[@]}"; do
|
||||
dest_path="$(destination_path "$name")"
|
||||
destination_parent="$(dirname "$dest_path")"
|
||||
temporary="$destination_parent/.${name}.agent99-${RUN_ID}"
|
||||
install -m 755 "$STAGE_DIR/$name" "$temporary"
|
||||
mv -f -- "$temporary" "$DEST_ROOT/$name"
|
||||
mv -f -- "$temporary" "$dest_path"
|
||||
done
|
||||
|
||||
verify_destination || fail "post_apply_verification_failed"
|
||||
verifier_result="$(BACKUP_RUNTIME_DEPLOY_CONTEXT=1 python3 "$SOURCE_STAGE/verify-host110-backup-runtime.py" \
|
||||
--manifest "$SOURCE_STAGE/manifest.json" \
|
||||
--destination "$DEST_ROOT" \
|
||||
--exporter-destination "$EXPORTER_ROOT/$EXPORTER_FILE" \
|
||||
--source-revision "$SOURCE_REVISION" \
|
||||
--run-id "$RUN_ID" \
|
||||
--expected-verifier-sha256 "$VERIFIER_DIGEST")" || fail "independent_verifier_failed"
|
||||
@@ -400,7 +424,9 @@ if not (
|
||||
and row.get("ok") is True
|
||||
and row.get("sourceRevision") == sys.argv[2]
|
||||
and row.get("runId") == sys.argv[3]
|
||||
and row.get("fileCount") == 17
|
||||
and row.get("fileCount") == 18
|
||||
and row.get("backupScriptFileCount") == 17
|
||||
and row.get("backupHealthExporterIncluded") is True
|
||||
and row.get("remoteWritePerformed") is False
|
||||
and row.get("selfIdentityVerified") is True
|
||||
):
|
||||
@@ -423,7 +449,9 @@ print(json.dumps({
|
||||
"ok": True,
|
||||
"sourceRevision": sys.argv[1],
|
||||
"sourceHead": sys.argv[2],
|
||||
"fileCount": 17,
|
||||
"fileCount": 18,
|
||||
"backupScriptFileCount": 17,
|
||||
"backupHealthExporterIncluded": True,
|
||||
"rollbackPerformed": False,
|
||||
"receipt": sys.argv[3],
|
||||
"verifier": json.loads(sys.argv[4]),
|
||||
|
||||
Reference in New Issue
Block a user