fix(agent99): deploy backup freshness exporter atomically
This commit is contained in:
@@ -214,6 +214,8 @@ $host110BackupRuntimeContract = [bool](
|
||||
$host110BackupRuntimeBroker.Contains('"merge-base", "--is-ancestor", $SourceRevision, $sourceHead') -and
|
||||
$host110BackupRuntimeBroker.Contains('executorSha256 = $executorDigest') -and
|
||||
$host110BackupRuntimeBroker.Contains('verifierSha256 = $verifierDigest') -and
|
||||
$host110BackupRuntimeBroker.Contains('"scripts/ops/backup-health-textfile-exporter.py"') -and
|
||||
$host110BackupRuntimeBroker.Contains('$ExpectedFileCount = 18') -and
|
||||
$host110BackupRuntimeBroker.Contains('StrictHostKeyChecking=yes') -and
|
||||
$host110BackupRuntimeBroker.Contains('$process.WaitForExit($TimeoutSeconds * 1000)') -and
|
||||
$host110BackupRuntimeBroker.Contains('taskkill.exe /PID $process.Id /T /F') -and
|
||||
|
||||
@@ -20,8 +20,8 @@ $IdentityFile = Join-Path $ExpectedAgentRoot "keys\agent99_ed25519"
|
||||
$EvidenceRoot = Join-Path $ExpectedAgentRoot "evidence\host110-backup-runtime"
|
||||
$SourceRootBase = Join-Path $ExpectedAgentRoot "deploy"
|
||||
$TransportMutexName = "Global\WoooAgent99Host110BackupRuntimeV2"
|
||||
$ExpectedFileCount = 17
|
||||
$ExpectedRemoteStageFileCount = 20
|
||||
$ExpectedFileCount = 18
|
||||
$ExpectedRemoteStageFileCount = 21
|
||||
$RuntimeRelativePaths = @(
|
||||
"scripts/backup/common.sh",
|
||||
"scripts/backup/backup-all.sh",
|
||||
@@ -39,7 +39,8 @@ $RuntimeRelativePaths = @(
|
||||
"scripts/backup/sync-offsite-backups.sh",
|
||||
"scripts/backup/backup-offsite-readiness-gate.sh",
|
||||
"scripts/backup/verify-offsite-full-sync.sh",
|
||||
"scripts/backup/enforce-latest-only-retention.sh"
|
||||
"scripts/backup/enforce-latest-only-retention.sh",
|
||||
"scripts/ops/backup-health-textfile-exporter.py"
|
||||
)
|
||||
$ExecutorRelativePath = "scripts/backup/host110-backup-runtime-executor.sh"
|
||||
$VerifierRelativePath = "scripts/backup/verify-host110-backup-runtime.py"
|
||||
@@ -281,7 +282,9 @@ function Get-Agent99ExecutorResult {
|
||||
[string]$parsed.mode -ne $ExpectedMode.ToLowerInvariant() -or
|
||||
-not [bool]$parsed.ok -or
|
||||
[string]$parsed.sourceRevision -ne $SourceRevision -or
|
||||
[int]$parsed.fileCount -ne $ExpectedFileCount
|
||||
[int]$parsed.fileCount -ne $ExpectedFileCount -or
|
||||
[int]$parsed.backupScriptFileCount -ne 17 -or
|
||||
-not [bool]$parsed.backupHealthExporterIncluded
|
||||
) {
|
||||
throw "host110_${ExpectedMode}_receipt_contract_failed"
|
||||
}
|
||||
@@ -313,6 +316,8 @@ function Get-Agent99VerifierResult {
|
||||
[string]$parsed.sourceRevision -ne $SourceRevision -or
|
||||
[string]$parsed.runId -ne $RunId -or
|
||||
[int]$parsed.fileCount -ne $ExpectedFileCount -or
|
||||
[int]$parsed.backupScriptFileCount -ne 17 -or
|
||||
-not [bool]$parsed.backupHealthExporterIncluded -or
|
||||
[bool]$parsed.remoteWritePerformed -or
|
||||
[string]$parsed.verifierSha256 -ne [string]$SourcePackage.verifierSha256
|
||||
) {
|
||||
@@ -324,7 +329,7 @@ function Get-Agent99VerifierResult {
|
||||
function Invoke-Agent99ReadOnlyPreflight {
|
||||
param([object]$SourcePackage)
|
||||
|
||||
$command = 'test "$(id -un)" = wooo && hostname -I | tr " " "\n" | grep -qx 192.168.0.110 && test -d /backup/scripts && test ! -L /backup/scripts && command -v timeout >/dev/null && command -v flock >/dev/null && command -v python3 >/dev/null && printf HOST110_BACKUP_PREFLIGHT_OK=1'
|
||||
$command = 'test "$(id -un)" = wooo && hostname -I | tr " " "\n" | grep -qx 192.168.0.110 && test -d /backup/scripts && test ! -L /backup/scripts && test -d /home/wooo/scripts && test ! -L /home/wooo/scripts && command -v timeout >/dev/null && command -v flock >/dev/null && command -v python3 >/dev/null && printf HOST110_BACKUP_PREFLIGHT_OK=1'
|
||||
$transport = Invoke-Agent99BoundedSsh $command 45
|
||||
if (-not $transport.ok -or ([string]$transport.stdout).Trim() -ne "HOST110_BACKUP_PREFLIGHT_OK=1") {
|
||||
throw "host110_read_only_preflight_failed"
|
||||
@@ -335,6 +340,8 @@ function Invoke-Agent99ReadOnlyPreflight {
|
||||
sourceRevision = $SourceRevision
|
||||
sourceHead = [string]$SourcePackage.sourceHead
|
||||
fileCount = $ExpectedFileCount
|
||||
backupScriptFileCount = 17
|
||||
backupHealthExporterIncluded = $true
|
||||
remoteWritePerformed = $false
|
||||
}
|
||||
}
|
||||
@@ -342,7 +349,7 @@ function Invoke-Agent99ReadOnlyPreflight {
|
||||
function Invoke-Agent99NoWriteVerifier {
|
||||
param([object]$SourcePackage)
|
||||
|
||||
$command = "timeout --signal=TERM --kill-after=5s 60s python3 - --manifest-base64 $($SourcePackage.manifestBase64) --destination /backup/scripts --source-revision $SourceRevision --run-id $RunId --expected-verifier-sha256 $($SourcePackage.verifierSha256)"
|
||||
$command = "timeout --signal=TERM --kill-after=5s 60s python3 - --manifest-base64 $($SourcePackage.manifestBase64) --destination /backup/scripts --exporter-destination /home/wooo/scripts/backup-health-textfile-exporter.py --source-revision $SourceRevision --run-id $RunId --expected-verifier-sha256 $($SourcePackage.verifierSha256)"
|
||||
$transport = Invoke-Agent99BoundedSsh $command 90 ([string]$SourcePackage.verifierPath)
|
||||
return Get-Agent99VerifierResult $transport $SourcePackage
|
||||
}
|
||||
|
||||
@@ -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]),
|
||||
|
||||
@@ -16,8 +16,9 @@ from pathlib import Path
|
||||
|
||||
|
||||
EXPECTED_DESTINATION = Path("/backup/scripts")
|
||||
EXPECTED_EXPORTER_DESTINATION = Path("/home/wooo/scripts/backup-health-textfile-exporter.py")
|
||||
RUNTIME_LOCK = Path("/tmp/agent99-host110-backup-runtime.lock")
|
||||
EXPECTED_FILES = (
|
||||
EXPECTED_BACKUP_FILES = (
|
||||
"common.sh",
|
||||
"backup-all.sh",
|
||||
"backup-host188-products.sh",
|
||||
@@ -36,6 +37,8 @@ EXPECTED_FILES = (
|
||||
"verify-offsite-full-sync.sh",
|
||||
"enforce-latest-only-retention.sh",
|
||||
)
|
||||
EXPORTER_FILE = "backup-health-textfile-exporter.py"
|
||||
EXPECTED_FILES = EXPECTED_BACKUP_FILES + (EXPORTER_FILE,)
|
||||
|
||||
|
||||
def fail(reason: str) -> None:
|
||||
@@ -78,6 +81,7 @@ def main() -> None:
|
||||
manifest_source.add_argument("--manifest")
|
||||
manifest_source.add_argument("--manifest-base64")
|
||||
parser.add_argument("--destination", required=True)
|
||||
parser.add_argument("--exporter-destination", required=True)
|
||||
parser.add_argument("--source-revision", required=True)
|
||||
parser.add_argument("--run-id", required=True)
|
||||
parser.add_argument("--expected-verifier-sha256", required=True)
|
||||
@@ -90,6 +94,9 @@ def main() -> None:
|
||||
destination = Path(args.destination)
|
||||
if destination != EXPECTED_DESTINATION or destination.is_symlink() or not destination.is_dir():
|
||||
fail("invalid_destination")
|
||||
exporter_destination = Path(args.exporter_destination)
|
||||
if exporter_destination != EXPECTED_EXPORTER_DESTINATION:
|
||||
fail("invalid_exporter_destination")
|
||||
runtime_lock = acquire_runtime_read_lock(destination)
|
||||
|
||||
if not re.fullmatch(r"[0-9a-f]{64}", args.expected_verifier_sha256):
|
||||
@@ -131,11 +138,14 @@ def main() -> None:
|
||||
|
||||
root = destination.resolve(strict=True)
|
||||
for name in EXPECTED_FILES:
|
||||
path = destination / name
|
||||
path = exporter_destination if name == EXPORTER_FILE else destination / name
|
||||
if path.is_symlink() or not path.is_file():
|
||||
fail("runtime_file_type_failed")
|
||||
resolved = path.resolve(strict=True)
|
||||
if resolved.parent != root:
|
||||
if name == EXPORTER_FILE:
|
||||
if resolved != exporter_destination:
|
||||
fail("runtime_file_boundary_failed")
|
||||
elif resolved.parent != root:
|
||||
fail("runtime_file_boundary_failed")
|
||||
stat = resolved.stat()
|
||||
if stat.st_uid != os.getuid() or stat.st_gid != os.getgid() or stat.st_mode & 0o777 != 0o755:
|
||||
@@ -149,7 +159,10 @@ def main() -> None:
|
||||
"sourceRevision": args.source_revision,
|
||||
"runId": args.run_id,
|
||||
"fileCount": len(EXPECTED_FILES),
|
||||
"backupScriptFileCount": len(EXPECTED_BACKUP_FILES),
|
||||
"backupHealthExporterIncluded": True,
|
||||
"destination": str(EXPECTED_DESTINATION),
|
||||
"exporterDestination": str(EXPECTED_EXPORTER_DESTINATION),
|
||||
"remoteWritePerformed": False,
|
||||
"secretValuesRead": False,
|
||||
"selfIdentityVerified": self_identity_verified,
|
||||
|
||||
@@ -64,6 +64,7 @@ def test_windows99_broker_fetches_exact_gitea_revision_and_is_bounded() -> None:
|
||||
assert '"checkout", "--quiet", $SourceRevision, "--"' in source
|
||||
assert 'executorSha256 = $executorDigest' in source
|
||||
assert 'verifierSha256 = $verifierDigest' in source
|
||||
assert '"scripts/ops/backup-health-textfile-exporter.py"' in source
|
||||
assert "/home/wooo/awoooi" not in source
|
||||
assert '"StrictHostKeyChecking=yes"' in source
|
||||
assert '"NumberOfPasswordPrompts=0"' in source
|
||||
@@ -98,6 +99,9 @@ def test_host110_executor_is_manifest_bound_atomic_and_fail_closed() -> None:
|
||||
assert 'readonly EXPECTED_HOST_IP="192.168.0.110"' in source
|
||||
assert 'readonly EXPECTED_USER="wooo"' in source
|
||||
assert 'readonly DEST_ROOT="/backup/scripts"' in source
|
||||
assert 'readonly EXPORTER_ROOT="/home/wooo/scripts"' in source
|
||||
assert 'readonly EXPORTER_FILE="backup-health-textfile-exporter.py"' in source
|
||||
assert 'readonly -a PAYLOAD_FILES=("${RUNTIME_FILES[@]}" "$EXPORTER_FILE")' in source
|
||||
assert '--source-stage' in source
|
||||
assert 'document.get("executorSha256"' in source
|
||||
assert 'document.get("verifierSha256"' in source
|
||||
@@ -105,16 +109,20 @@ def test_host110_executor_is_manifest_bound_atomic_and_fail_closed() -> None:
|
||||
assert 'verifier_identity_failed' in source
|
||||
assert 'flock -x -w 30 9' in source
|
||||
assert 'legacy_backup_runtime_active' in source
|
||||
assert source.index("for name in \"${RUNTIME_FILES[@]}\"") < source.index("mv -f -- \"$temporary\"")
|
||||
assert source.index("for name in \"${PAYLOAD_FILES[@]}\"") < source.index("mv -f -- \"$temporary\"")
|
||||
assert 'rollback_unverified' in source
|
||||
assert 'failed_rolled_back' in source
|
||||
assert 'working_digest "$dest_path"' in source
|
||||
assert 'PREVIOUS_METADATA' in source
|
||||
assert "stat -c '%u:%g:%a'" in source
|
||||
assert 'os.link(temporary, path)' in source
|
||||
assert 'run_identity_receipt_exists' in source
|
||||
assert 'run_identity_stage_exists' in source
|
||||
assert 'run_identity_rollback_exists' in source
|
||||
assert 'verify-host110-backup-runtime.py' in source
|
||||
assert 'selfIdentityVerified' in source
|
||||
assert '"backupScriptFileCount": 17' in source
|
||||
assert '"backupHealthExporterIncluded": True' in source
|
||||
assert '"productionServiceRestarted": False' in source
|
||||
assert '"secretValuesRead": False' in source
|
||||
assert "sudo " not in source
|
||||
@@ -139,12 +147,16 @@ def test_independent_verifier_accepts_exact_bundle_without_writing(tmp_path, mon
|
||||
verifier = _load_verifier()
|
||||
destination = tmp_path / "scripts"
|
||||
destination.mkdir()
|
||||
exporter_destination = tmp_path / verifier.EXPORTER_FILE
|
||||
rows = []
|
||||
for name in verifier.EXPECTED_FILES:
|
||||
for name in verifier.EXPECTED_BACKUP_FILES:
|
||||
path = destination / name
|
||||
path.write_text(f"runtime:{name}\n", encoding="utf-8")
|
||||
path.chmod(0o755)
|
||||
rows.append({"name": name, "sha256": _sha256(path)})
|
||||
exporter_destination.write_text("exporter\n", encoding="utf-8")
|
||||
exporter_destination.chmod(0o755)
|
||||
rows.append({"name": verifier.EXPORTER_FILE, "sha256": _sha256(exporter_destination)})
|
||||
verifier_digest = _sha256(VERIFIER)
|
||||
source_revision = "a" * 40
|
||||
run_id = "unit-exact"
|
||||
@@ -159,8 +171,12 @@ def test_independent_verifier_accepts_exact_bundle_without_writing(tmp_path, mon
|
||||
}
|
||||
manifest_path = tmp_path / "manifest.json"
|
||||
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
|
||||
before = {path.name: (path.stat().st_mtime_ns, _sha256(path)) for path in destination.iterdir()}
|
||||
before = {
|
||||
path.name: (path.stat().st_mtime_ns, _sha256(path))
|
||||
for path in [*destination.iterdir(), exporter_destination]
|
||||
}
|
||||
monkeypatch.setattr(verifier, "EXPECTED_DESTINATION", destination)
|
||||
monkeypatch.setattr(verifier, "EXPECTED_EXPORTER_DESTINATION", exporter_destination)
|
||||
monkeypatch.setattr(
|
||||
sys,
|
||||
"argv",
|
||||
@@ -170,6 +186,8 @@ def test_independent_verifier_accepts_exact_bundle_without_writing(tmp_path, mon
|
||||
str(manifest_path),
|
||||
"--destination",
|
||||
str(destination),
|
||||
"--exporter-destination",
|
||||
str(exporter_destination),
|
||||
"--source-revision",
|
||||
source_revision,
|
||||
"--run-id",
|
||||
@@ -182,8 +200,14 @@ def test_independent_verifier_accepts_exact_bundle_without_writing(tmp_path, mon
|
||||
verifier.main()
|
||||
|
||||
result = json.loads(capsys.readouterr().out)
|
||||
after = {path.name: (path.stat().st_mtime_ns, _sha256(path)) for path in destination.iterdir()}
|
||||
after = {
|
||||
path.name: (path.stat().st_mtime_ns, _sha256(path))
|
||||
for path in [*destination.iterdir(), exporter_destination]
|
||||
}
|
||||
assert result["ok"] is True
|
||||
assert result["fileCount"] == 18
|
||||
assert result["backupScriptFileCount"] == 17
|
||||
assert result["backupHealthExporterIncluded"] is True
|
||||
assert result["remoteWritePerformed"] is False
|
||||
assert result["selfIdentityVerified"] is True
|
||||
assert before == after
|
||||
@@ -193,13 +217,17 @@ def test_independent_verifier_rejects_drifted_bundle(tmp_path, monkeypatch, caps
|
||||
verifier = _load_verifier()
|
||||
destination = tmp_path / "scripts"
|
||||
destination.mkdir()
|
||||
exporter_destination = tmp_path / verifier.EXPORTER_FILE
|
||||
rows = []
|
||||
for name in verifier.EXPECTED_FILES:
|
||||
for name in verifier.EXPECTED_BACKUP_FILES:
|
||||
path = destination / name
|
||||
path.write_text(f"runtime:{name}\n", encoding="utf-8")
|
||||
path.chmod(0o755)
|
||||
rows.append({"name": name, "sha256": _sha256(path)})
|
||||
(destination / verifier.EXPECTED_FILES[0]).write_text("drifted\n", encoding="utf-8")
|
||||
exporter_destination.write_text("exporter\n", encoding="utf-8")
|
||||
exporter_destination.chmod(0o755)
|
||||
rows.append({"name": verifier.EXPORTER_FILE, "sha256": _sha256(exporter_destination)})
|
||||
(destination / verifier.EXPECTED_BACKUP_FILES[0]).write_text("drifted\n", encoding="utf-8")
|
||||
verifier_digest = _sha256(VERIFIER)
|
||||
source_revision = "d" * 40
|
||||
run_id = "unit-drift"
|
||||
@@ -214,6 +242,7 @@ def test_independent_verifier_rejects_drifted_bundle(tmp_path, monkeypatch, caps
|
||||
}
|
||||
encoded = base64.b64encode(json.dumps(manifest).encode()).decode()
|
||||
monkeypatch.setattr(verifier, "EXPECTED_DESTINATION", destination)
|
||||
monkeypatch.setattr(verifier, "EXPECTED_EXPORTER_DESTINATION", exporter_destination)
|
||||
monkeypatch.setattr(
|
||||
sys,
|
||||
"argv",
|
||||
@@ -223,6 +252,8 @@ def test_independent_verifier_rejects_drifted_bundle(tmp_path, monkeypatch, caps
|
||||
encoded,
|
||||
"--destination",
|
||||
str(destination),
|
||||
"--exporter-destination",
|
||||
str(exporter_destination),
|
||||
"--source-revision",
|
||||
source_revision,
|
||||
"--run-id",
|
||||
|
||||
Reference in New Issue
Block a user