103 lines
3.6 KiB
Bash
Executable File
103 lines
3.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
umask 0077
|
|
|
|
INSTALL_ROOT="${INSTALL_ROOT:-/opt/aios-orchestrator-gateway}"
|
|
STATE_ROOT="${STATE_ROOT:-/var/lib/aios-orchestrator-gateway}"
|
|
RECEIPT="$STATE_ROOT/install-receipt.json"
|
|
|
|
fail() {
|
|
printf 'AIOS_ORCHESTRATOR_ROLLBACK=BLOCKED reason=%s\n' "$1" >&2
|
|
exit 2
|
|
}
|
|
|
|
[[ "$(id -u)" -eq 0 ]] || fail OWNER_REQUIRED
|
|
[[ -f "$RECEIPT" ]] || fail RECEIPT_MISSING
|
|
[[ ! -L "$RECEIPT" ]] || fail RECEIPT_SYMLINK
|
|
[[ "$(stat -c '%u:%a' "$RECEIPT")" == "0:600" ]] || fail RECEIPT_PERMISSION
|
|
|
|
readarray -t VALUES < <(
|
|
python3 - "$RECEIPT" "$INSTALL_ROOT/releases" "$INSTALL_ROOT/current" <<'PY'
|
|
import json, os, stat, sys
|
|
from pathlib import Path
|
|
receipt = Path(sys.argv[1])
|
|
releases = Path(sys.argv[2]).resolve()
|
|
current_link = Path(sys.argv[3])
|
|
data = json.loads(receipt.read_text())
|
|
previous = data.get("previous_release")
|
|
current = data.get("current_release")
|
|
if not previous or not current:
|
|
raise SystemExit(2)
|
|
previous_path = Path(previous).resolve()
|
|
current_path = Path(current).resolve()
|
|
if previous_path.parent != releases or current_path.parent != releases:
|
|
raise SystemExit(2)
|
|
if not stat.S_ISDIR(os.lstat(previous_path).st_mode) or not stat.S_ISDIR(os.lstat(current_path).st_mode):
|
|
raise SystemExit(2)
|
|
for release in (previous_path, current_path):
|
|
if len(release.name) != 64 or any(ch not in "0123456789abcdef" for ch in release.name):
|
|
raise SystemExit(2)
|
|
digest_file = release / "SOURCE_DIGEST"
|
|
if digest_file.is_symlink() or not digest_file.is_file():
|
|
raise SystemExit(2)
|
|
if digest_file.read_text(encoding="utf-8").strip() != release.name:
|
|
raise SystemExit(2)
|
|
last = data.get("last_rollback")
|
|
if isinstance(last, dict) and last.get("to") == current and current_link.is_symlink():
|
|
if current_link.resolve() == current_path:
|
|
print("ALREADY_ROLLED_BACK")
|
|
raise SystemExit(0)
|
|
print(previous_path)
|
|
print(current_path)
|
|
PY
|
|
) || fail RECEIPT_INVALID
|
|
|
|
if [[ "${VALUES[0]:-}" == "ALREADY_ROLLED_BACK" ]]; then
|
|
echo "AIOS_ORCHESTRATOR_ROLLBACK=PASS result=NOOP_ALREADY_ROLLED_BACK"
|
|
exit 0
|
|
fi
|
|
[[ "${#VALUES[@]}" -eq 2 ]] || fail RECEIPT_INVALID
|
|
PREVIOUS="${VALUES[0]}"
|
|
CURRENT="${VALUES[1]}"
|
|
systemctl stop aios-orchestrator-gateway-poll.timer || true
|
|
systemctl stop aios-orchestrator-gateway-poll.service || true
|
|
systemctl stop aios-orchestrator-gateway.service || true
|
|
ln -s "$PREVIOUS" "$INSTALL_ROOT/.current-rollback-$$"
|
|
mv -Tf "$INSTALL_ROOT/.current-rollback-$$" "$INSTALL_ROOT/current"
|
|
systemctl start aios-orchestrator-gateway.service
|
|
systemctl is-active --quiet aios-orchestrator-gateway.service
|
|
systemctl start aios-orchestrator-gateway-poll.timer
|
|
systemctl is-active --quiet aios-orchestrator-gateway-poll.timer
|
|
|
|
python3 - "$RECEIPT" "$PREVIOUS" "$CURRENT" <<'PY'
|
|
import json, os, sys, tempfile
|
|
from pathlib import Path
|
|
path = Path(sys.argv[1])
|
|
data = json.loads(path.read_text())
|
|
data["current_release"] = sys.argv[2]
|
|
data["previous_release"] = sys.argv[3]
|
|
data["last_rollback"] = {"from": sys.argv[3], "to": sys.argv[2]}
|
|
fd, temp = tempfile.mkstemp(prefix=".rollback-receipt-", dir=path.parent)
|
|
try:
|
|
with os.fdopen(fd, "w") as handle:
|
|
json.dump(data, handle, indent=2, sort_keys=True)
|
|
handle.write("\n")
|
|
handle.flush()
|
|
os.fsync(handle.fileno())
|
|
os.chmod(temp, 0o600)
|
|
os.chown(temp, 0, 0)
|
|
os.replace(temp, path)
|
|
directory = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
|
|
try:
|
|
os.fsync(directory)
|
|
finally:
|
|
os.close(directory)
|
|
finally:
|
|
try:
|
|
os.unlink(temp)
|
|
except FileNotFoundError:
|
|
pass
|
|
PY
|
|
|
|
printf 'AIOS_ORCHESTRATOR_ROLLBACK=PASS current=%s previous=%s\n' "$PREVIOUS" "$CURRENT"
|