Files
awoooi/scripts/reboot-recovery/install-host112-guest-recovery.sh
ogt c9bb4bbfc6
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m35s
CD Pipeline / build-and-deploy (push) Failing after 7m35s
CD Pipeline / post-deploy-checks (push) Has been skipped
AWOOOI Harbor 110 Local Repair / workflow-shape (push) Successful in 1s
AWOOOI Harbor 110 Local Repair / harbor-110-local-repair (push) Successful in 16s
fix(recovery): bind host112 closure artifacts
2026-07-14 12:37:28 +08:00

670 lines
25 KiB
Bash
Executable File

#!/usr/bin/env bash
# Host 112 guest recovery installer.
# Version: 2.1
# Last modified: 2026-07-14 12:10 (Asia/Taipei)
# Last modified by: Codex
# Change: make installation transactional, verify the Windows99 direct route
# and exact sudo policy, and keep installation separate from recovery runs.
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MODE="check"
MODE_EXPLICIT=0
ROLLBACK_DIR=""
TRACE_ID=""
RUN_ID=""
WORK_ITEM_ID=""
TARGET_USER="${HOST112_TARGET_USER:-kali}"
CONTROL_SOURCE_ADDRESS="${HOST112_CONTROL_SOURCE_ADDRESS:-192.168.0.110}"
AGENT99_SOURCE_ADDRESS="${HOST112_AGENT99_SOURCE_ADDRESS:-192.168.0.99}"
AGENT99_EXPECTED_KEY_FINGERPRINT="${HOST112_AGENT99_EXPECTED_KEY_FINGERPRINT:-SHA256:4ccZZSe0buoWyivi2mfTMXE9LZfCmFBPr/evCzZ0nzA}"
CONTROL_PUBLIC_KEY_PATH="${HOST112_CONTROL_PUBLIC_KEY_PATH:-${SCRIPT_DIR}/control.pub}"
READINESS_SOURCE="${SCRIPT_DIR}/host112-guest-readiness.sh"
SERVICE_SOURCE="${SCRIPT_DIR}/awoooi-host112-guest-recovery.service"
TIMER_SOURCE="${SCRIPT_DIR}/awoooi-host112-guest-recovery.timer"
READINESS_TARGET="/usr/local/sbin/awoooi-host112-guest-readiness"
INSTALLER_TARGET="/usr/local/sbin/awoooi-install-host112-guest-recovery"
SERVICE_TARGET="/etc/systemd/system/awoooi-host112-guest-recovery.service"
TIMER_TARGET="/etc/systemd/system/awoooi-host112-guest-recovery.timer"
SUDOERS_TARGET="/etc/sudoers.d/awoooi-host112-agent99"
STATE_ROOT="${HOST112_RECOVERY_STATE_DIR:-/var/lib/awoooi-host112-recovery}"
MANAGER_LOCK_FILE="${HOST112_MANAGER_LOCK_FILE:-/run/awoooi-host112-manager-recovery.lock}"
SUDOERS_ARGUMENT_REGEX='^--apply --trace-id [A-Za-z0-9][A-Za-z0-9._:@+-]{0,127} --run-id [A-Za-z0-9][A-Za-z0-9._:@+-]{0,127} --work-item-id [A-Za-z0-9][A-Za-z0-9._:@+-]{0,127}$'
SUDOERS_DRY_RUN_ARGUMENT_REGEX='^--dry-run --trace-id [A-Za-z0-9][A-Za-z0-9._:@+-]{0,127} --run-id [A-Za-z0-9][A-Za-z0-9._:@+-]{0,127} --work-item-id [A-Za-z0-9][A-Za-z0-9._:@+-]{0,127}$'
TRANSACTION_ACTIVE=0
ROLLBACK_IN_PROGRESS=0
ROLLBACK_ATTEMPTED=0
ROLLBACK_VERIFIED=0
INSTALL_VERIFIED=0
INSTALL_RECEIPT_WRITTEN=0
INSTALL_RECEIPT_REF="none"
INSTALL_RECEIPT_PATH=""
INSTALL_ATTEMPT_ID=""
BACKUP_DIR=""
sudoers_tmp=""
AUTH_TMP=""
TIMER_ACTIVATION_PERFORMED=0
USER_HOME=""
USER_GROUP=""
AUTH_DIR=""
AUTH_FILE=""
CONTROL_KEY_TYPE=""
CONTROL_KEY_BLOB=""
CONTROL_KEY_FINGERPRINT="unknown"
usage() {
cat <<'USAGE'
Usage:
install-host112-guest-recovery.sh --check
install-host112-guest-recovery.sh --apply --trace-id ID --run-id ID --work-item-id ID
install-host112-guest-recovery.sh --rollback BACKUP_DIR --trace-id ID --run-id ID --work-item-id ID
Apply installs and statically verifies the bounded host-112 recovery timer,
exact correlated apply/dry-run sudo rules, the host-110 forced readback key,
and the existing Windows99 direct Agent99 authorization. It starts only the
timer after all static gates pass; it never synchronously starts the recovery
oneshot, reboots the host, or changes VM power.
USAGE
}
set_mode() {
local requested="$1"
if [ "$MODE_EXPLICIT" -eq 1 ]; then
echo "BLOCKER duplicate_mode" >&2
exit 64
fi
MODE="$requested"
MODE_EXPLICIT=1
}
require_value() {
local flag="$1" value="${2:-}"
if [ -z "$value" ]; then
echo "BLOCKER missing_value=$flag" >&2
exit 64
fi
}
while [ "$#" -gt 0 ]; do
case "$1" in
--check) set_mode "check" ;;
--apply) set_mode "apply" ;;
--rollback)
set_mode "rollback"
require_value "$1" "${2:-}"
ROLLBACK_DIR="$2"
shift
;;
--trace-id)
require_value "$1" "${2:-}"
TRACE_ID="$2"
shift
;;
--run-id)
require_value "$1" "${2:-}"
RUN_ID="$2"
shift
;;
--work-item-id)
require_value "$1" "${2:-}"
WORK_ITEM_ID="$2"
shift
;;
-h|--help) usage; exit 0 ;;
*) echo "UNKNOWN_ARG=$1" >&2; usage >&2; exit 64 ;;
esac
shift
done
safe_identity() {
[[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9._:@+-]{0,127}$ ]]
}
identity_count=0
[ -n "$TRACE_ID" ] && identity_count=$((identity_count + 1))
[ -n "$RUN_ID" ] && identity_count=$((identity_count + 1))
[ -n "$WORK_ITEM_ID" ] && identity_count=$((identity_count + 1))
if [ "$identity_count" -ne 0 ] && [ "$identity_count" -ne 3 ]; then
echo "BLOCKER incomplete_control_identity" >&2
exit 64
fi
if [ "$identity_count" -eq 3 ]; then
for identity in "$TRACE_ID" "$RUN_ID" "$WORK_ITEM_ID"; do
if ! safe_identity "$identity"; then
echo "BLOCKER unsafe_control_identity" >&2
exit 64
fi
done
fi
if { [ "$MODE" = "apply" ] || [ "$MODE" = "rollback" ]; } && [ "$identity_count" -ne 3 ]; then
echo "BLOCKER installer_control_identity_required" >&2
exit 64
fi
cleanup() {
[ -z "$sudoers_tmp" ] || rm -f "$sudoers_tmp" >/dev/null 2>&1 || true
[ -z "$AUTH_TMP" ] || rm -f "$AUTH_TMP" >/dev/null 2>&1 || true
}
trap cleanup EXIT
resolve_target_user() {
USER_HOME="$(getent passwd "$TARGET_USER" 2>/dev/null | cut -d: -f6)"
USER_GROUP="$(id -gn "$TARGET_USER" 2>/dev/null || true)"
[ -n "$USER_HOME" ] || { echo "BLOCKER target_user_home_missing" >&2; return 1; }
[ -n "$USER_GROUP" ] || { echo "BLOCKER target_user_group_missing" >&2; return 1; }
AUTH_DIR="${USER_HOME}/.ssh"
AUTH_FILE="${AUTH_DIR}/authorized_keys"
}
sudo_policy_allows() {
if [ "${EUID:-$(id -u)}" -eq 0 ]; then
sudo -n -U "$TARGET_USER" -l "$@" >/dev/null 2>&1
else
sudo -n -l "$@" >/dev/null 2>&1
fi
}
agent99_direct_authorization_ready() {
local key_type key_blob fingerprint
[ -f "$AUTH_FILE" ] || return 1
while IFS=$'\t' read -r key_type key_blob; do
[ -n "$key_type" ] && [ -n "$key_blob" ] || continue
[[ "$key_blob" =~ ^[A-Za-z0-9+/=]+$ ]] || continue
fingerprint="$({ printf '%s %s\n' "$key_type" "$key_blob" | ssh-keygen -lf - 2>/dev/null || true; } | awk 'NR == 1 {print $2}')"
if [ "$fingerprint" = "$AGENT99_EXPECTED_KEY_FINGERPRINT" ]; then
return 0
fi
done < <(
awk -v source="$AGENT99_SOURCE_ADDRESS" '
function has_restrict(options, count, parts, i) {
count = split(options, parts, ",")
for (i = 1; i <= count; i++) {
if (parts[i] == "restrict") return 1
}
return 0
}
index($1, "from=\"" source "\"") > 0 \
&& has_restrict($1) \
&& index($1, "command=") == 0 \
&& $2 ~ /^(ssh-ed25519|ssh-rsa|ecdsa-sha2-)/ {
print $2 "\t" $3
}
' "$AUTH_FILE"
)
return 1
}
readiness_check_no_write_ready() {
local readiness_args
readiness_args=("$READINESS_TARGET" --check)
if [ "$identity_count" -eq 3 ]; then
readiness_args+=(
--trace-id "$TRACE_ID"
--run-id "$RUN_ID"
--work-item-id "$WORK_ITEM_ID"
)
fi
if READINESS_CHECK_OUTPUT="$("${readiness_args[@]}" 2>&1)"; then
READINESS_CHECK_EXIT=0
else
READINESS_CHECK_EXIT=$?
fi
grep -Eq '(^| )schema_version=host112_guest_recovery_v2($| )' <<<"$READINESS_CHECK_OUTPUT" \
&& grep -Eq '(^| )mode=check($| )' <<<"$READINESS_CHECK_OUTPUT" \
&& grep -Eq '(^| )runtime_write_performed=0($| )' <<<"$READINESS_CHECK_OUTPUT" \
&& grep -Eq '(^| )manager_runtime_write_performed=0($| )' <<<"$READINESS_CHECK_OUTPUT" \
&& grep -Eq '(^| )receipt_write_performed=0($| )' <<<"$READINESS_CHECK_OUTPUT" \
&& grep -Eq '(^| )ssh_port_listening=1($| )' <<<"$READINESS_CHECK_OUTPUT" \
&& grep -Eq '(^| )ssh_ready=1($| )' <<<"$READINESS_CHECK_OUTPUT"
}
refresh_gate_state() {
TARGET_FILES_READY=1
[ -x "$READINESS_TARGET" ] || TARGET_FILES_READY=0
[ -x "$INSTALLER_TARGET" ] || TARGET_FILES_READY=0
[ -f "$SERVICE_TARGET" ] || TARGET_FILES_READY=0
[ -f "$TIMER_TARGET" ] || TARGET_FILES_READY=0
[ -f "$SUDOERS_TARGET" ] || TARGET_FILES_READY=0
UNIT_CONTRACT_READY=0
if [ -f "$SERVICE_TARGET" ] \
&& [ -f "$TIMER_TARGET" ] \
&& grep -Fq 'ExecStart=/usr/local/sbin/awoooi-host112-guest-readiness --apply-timer' "$SERVICE_TARGET" \
&& grep -Fq 'TimeoutStartSec=480' "$SERVICE_TARGET" \
&& grep -Fq 'Unit=awoooi-host112-guest-recovery.service' "$TIMER_TARGET"; then
UNIT_CONTRACT_READY=1
fi
SUDOERS_VALID=0
if [ -f "$SUDOERS_TARGET" ] && visudo -cf "$SUDOERS_TARGET" >/dev/null 2>&1; then
SUDOERS_VALID=1
fi
SUDO_CORRELATED_APPLY_NOPASSWD=0
if sudo_policy_allows "$READINESS_TARGET" --apply \
--trace-id host112-policy-check \
--run-id host112-policy-check \
--work-item-id HOST112-WAZUH-MANAGER-RECOVERY; then
SUDO_CORRELATED_APPLY_NOPASSWD=1
fi
SUDO_CORRELATED_DRY_RUN_NOPASSWD=0
if sudo_policy_allows "$READINESS_TARGET" --dry-run \
--trace-id host112-policy-check \
--run-id host112-policy-check \
--work-item-id HOST112-WAZUH-MANAGER-RECOVERY; then
SUDO_CORRELATED_DRY_RUN_NOPASSWD=1
fi
SUDO_LEGACY_UNCORRELATED_APPLY_NOPASSWD=0
if sudo_policy_allows "$READINESS_TARGET" --apply; then
SUDO_LEGACY_UNCORRELATED_APPLY_NOPASSWD=1
fi
SUDO_UNSAFE_IDENTITY_NOPASSWD=0
if sudo_policy_allows "$READINESS_TARGET" --apply \
--trace-id 'unsafe value' \
--run-id host112-policy-check \
--work-item-id HOST112-WAZUH-MANAGER-RECOVERY; then
SUDO_UNSAFE_IDENTITY_NOPASSWD=1
fi
SUDO_POLICY_READY=0
if [ "$SUDOERS_VALID" -eq 1 ] \
&& [ "$SUDO_CORRELATED_APPLY_NOPASSWD" -eq 1 ] \
&& [ "$SUDO_CORRELATED_DRY_RUN_NOPASSWD" -eq 1 ] \
&& [ "$SUDO_LEGACY_UNCORRELATED_APPLY_NOPASSWD" -eq 0 ] \
&& [ "$SUDO_UNSAFE_IDENTITY_NOPASSWD" -eq 0 ]; then
SUDO_POLICY_READY=1
fi
AGENT99_99_DIRECT_ROUTE_READY=0
if agent99_direct_authorization_ready; then
AGENT99_99_DIRECT_ROUTE_READY=1
fi
READINESS_NO_WRITE_READY=0
READINESS_CHECK_EXIT=-1
READINESS_CHECK_OUTPUT=""
if [ -x "$READINESS_TARGET" ] && readiness_check_no_write_ready; then
READINESS_NO_WRITE_READY=1
fi
TIMER_ENABLED="$(systemctl is-enabled awoooi-host112-guest-recovery.timer 2>/dev/null || true)"
TIMER_ACTIVE="$(systemctl is-active awoooi-host112-guest-recovery.timer 2>/dev/null || true)"
TIMER_ENABLED="${TIMER_ENABLED:-unknown}"
TIMER_ACTIVE="${TIMER_ACTIVE:-unknown}"
}
check() {
local gate_ready=0 path
refresh_gate_state
if [ "$TARGET_FILES_READY" -eq 1 ] \
&& [ "$UNIT_CONTRACT_READY" -eq 1 ] \
&& [ "$SUDO_POLICY_READY" -eq 1 ] \
&& [ "$AGENT99_99_DIRECT_ROUTE_READY" -eq 1 ] \
&& [ "$READINESS_NO_WRITE_READY" -eq 1 ] \
&& [ "$TIMER_ENABLED" = "enabled" ] \
&& [ "$TIMER_ACTIVE" = "active" ]; then
gate_ready=1
fi
echo "AWOOOI_HOST112_GUEST_RECOVERY_INSTALLER=1"
echo "MODE=check"
for path in "$READINESS_TARGET" "$INSTALLER_TARGET" "$SERVICE_TARGET" "$TIMER_TARGET" "$SUDOERS_TARGET"; do
if [ -f "$path" ]; then
echo "TARGET path=$path present=1 sha256=$(sha256sum "$path" | awk '{print $1}')"
else
echo "TARGET path=$path present=0 sha256=missing"
fi
done
echo "TARGET_FILES_READY=$TARGET_FILES_READY"
echo "UNIT_CONTRACT_READY=$UNIT_CONTRACT_READY"
echo "TIMER_ENABLED=$TIMER_ENABLED"
echo "TIMER_ACTIVE=$TIMER_ACTIVE"
echo "SUDOERS_VALID=$SUDOERS_VALID"
echo "SUDO_CORRELATED_APPLY_NOPASSWD=$SUDO_CORRELATED_APPLY_NOPASSWD"
echo "SUDO_CORRELATED_DRY_RUN_NOPASSWD=$SUDO_CORRELATED_DRY_RUN_NOPASSWD"
echo "SUDO_LEGACY_UNCORRELATED_APPLY_NOPASSWD=$SUDO_LEGACY_UNCORRELATED_APPLY_NOPASSWD"
echo "SUDO_UNSAFE_IDENTITY_NOPASSWD=$SUDO_UNSAFE_IDENTITY_NOPASSWD"
echo "SUDO_POLICY_READY=$SUDO_POLICY_READY"
echo "AGENT99_99_DIRECT_ROUTE_READY=$AGENT99_99_DIRECT_ROUTE_READY"
echo "AGENT99_99_DIRECT_RUNTIME_PROBE=not_started"
echo "READINESS_NO_WRITE_READY=$READINESS_NO_WRITE_READY"
echo "READINESS_CHECK_EXIT=$READINESS_CHECK_EXIT"
echo "CHECK_GATE_READY=$gate_ready"
echo "INSTALLER_SYNCHRONOUS_RECOVERY_START=0"
echo "RECOVERY_EXECUTION_STATUS=separate_runtime_run_required"
[ "$gate_ready" -eq 1 ]
}
require_root() {
if [ "${EUID:-$(id -u)}" -ne 0 ]; then
echo "BLOCKER host112_installer_requires_root" >&2
exit 77
fi
}
backup_path() {
local path="$1" backup_dir="$2" relative
if [ -e "$path" ]; then
relative="${path#/}"
install -d "${backup_dir}/rootfs/$(dirname "$relative")"
cp -a "$path" "${backup_dir}/rootfs/${relative}"
printf 'present\t%s\n' "$path" >>"${backup_dir}/manifest.tsv"
else
printf 'missing\t%s\n' "$path" >>"${backup_dir}/manifest.tsv"
fi
}
create_backup() {
local run_slug
run_slug="$(printf '%s' "$RUN_ID" | tr -c 'A-Za-z0-9._+-' '_')"
BACKUP_DIR="${STATE_ROOT}/backups/host112-installer-${run_slug}-${INSTALL_ATTEMPT_ID}"
install -d -m 0750 "$BACKUP_DIR"
: >"${BACKUP_DIR}/manifest.tsv"
for path in "$READINESS_TARGET" "$INSTALLER_TARGET" "$SERVICE_TARGET" "$TIMER_TARGET" "$SUDOERS_TARGET" "$AUTH_FILE"; do
backup_path "$path" "$BACKUP_DIR"
done
systemctl is-enabled awoooi-host112-guest-recovery.timer >/dev/null 2>&1 \
&& touch "$BACKUP_DIR/timer_was_enabled" || true
systemctl is-active awoooi-host112-guest-recovery.timer >/dev/null 2>&1 \
&& touch "$BACKUP_DIR/timer_was_active" || true
}
restore_from_backup() {
local backup_dir="$1" state path ok=1
timeout 20 systemctl stop awoooi-host112-guest-recovery.timer >/dev/null 2>&1 || true
if systemctl is-active awoooi-host112-guest-recovery.service >/dev/null 2>&1; then
timeout 30 systemctl stop awoooi-host112-guest-recovery.service >/dev/null 2>&1 || ok=0
fi
systemctl disable awoooi-host112-guest-recovery.timer >/dev/null 2>&1 || true
while IFS=$'\t' read -r state path; do
[ -n "$path" ] || continue
if [ "$state" = "present" ]; then
install -d "$(dirname "$path")" || ok=0
cp -a "$backup_dir/rootfs/${path#/}" "$path" || ok=0
elif [ "$state" = "missing" ]; then
rm -f "$path" || ok=0
else
ok=0
fi
done <"$backup_dir/manifest.tsv"
systemctl daemon-reload >/dev/null 2>&1 || ok=0
if [ -f "$backup_dir/timer_was_enabled" ]; then
systemctl enable awoooi-host112-guest-recovery.timer >/dev/null 2>&1 || ok=0
fi
if [ -f "$backup_dir/timer_was_active" ]; then
systemctl start awoooi-host112-guest-recovery.timer >/dev/null 2>&1 || ok=0
fi
[ "$ok" -eq 1 ]
}
verify_rollback() {
local backup_dir="$1" state path
while IFS=$'\t' read -r state path; do
[ -n "$path" ] || continue
if [ "$state" = "present" ]; then
[ -e "$path" ] || return 1
cmp -s "$backup_dir/rootfs/${path#/}" "$path" || return 1
elif [ "$state" = "missing" ]; then
[ ! -e "$path" ] || return 1
else
return 1
fi
done <"$backup_dir/manifest.tsv"
if [ -f "$SUDOERS_TARGET" ]; then
visudo -cf "$SUDOERS_TARGET" >/dev/null 2>&1 || return 1
fi
if [ -f "$backup_dir/timer_was_enabled" ]; then
systemctl is-enabled awoooi-host112-guest-recovery.timer >/dev/null 2>&1 || return 1
else
! systemctl is-enabled awoooi-host112-guest-recovery.timer >/dev/null 2>&1 || return 1
fi
if [ -f "$backup_dir/timer_was_active" ]; then
systemctl is-active awoooi-host112-guest-recovery.timer >/dev/null 2>&1 || return 1
else
! systemctl is-active awoooi-host112-guest-recovery.timer >/dev/null 2>&1 || return 1
fi
}
write_install_receipt() {
local terminal="$1" install_verified="$2" rollback_attempted="$3" rollback_verified="$4" error_exit="$5"
local receipt_dir receipt_tmp generated_at readiness_sha installer_sha
receipt_dir="${STATE_ROOT}/install-receipts"
install -d -m 0750 "$receipt_dir"
if [ -z "$INSTALL_RECEIPT_PATH" ]; then
INSTALL_RECEIPT_PATH="${receipt_dir}/install-${RUN_ID//:/_}-${INSTALL_ATTEMPT_ID}.txt"
fi
generated_at="$(date --iso-8601=seconds)"
readiness_sha="$(sha256sum "$READINESS_TARGET" 2>/dev/null | awk '{print $1}')"
installer_sha="$(sha256sum "$INSTALLER_TARGET" 2>/dev/null | awk '{print $1}')"
readiness_sha="${readiness_sha:-missing}"
installer_sha="${installer_sha:-missing}"
receipt_tmp="${INSTALL_RECEIPT_PATH}.tmp.$$"
printf '%s\n' \
"schema_version=host112_guest_recovery_install_receipt_v1 generated_at=$generated_at trace_id=$TRACE_ID run_id=$RUN_ID work_item_id=$WORK_ITEM_ID terminal=$terminal install_verified=$install_verified installer_runtime_write_performed=1 manager_runtime_write_performed=0 static_post_gate_ready=${STATIC_POST_GATE_READY:-0} check_gate_ready=${CHECK_GATE_READY:-0} sudo_policy_ready=${SUDO_POLICY_READY:-0} agent99_99_direct_route_ready=${AGENT99_99_DIRECT_ROUTE_READY:-0} agent99_99_direct_runtime_probe=not_started timer_enabled=${TIMER_ENABLED:-unknown} timer_active=${TIMER_ACTIVE:-unknown} timer_activation_performed=$TIMER_ACTIVATION_PERFORMED synchronous_recovery_start=0 recovery_execution_performed=0 recovery_run_id=none recovery_receipt_ref=none rollback_attempted=$rollback_attempted rollback_verified=$rollback_verified error_exit=$error_exit backup_ref=$BACKUP_DIR readiness_sha256=$readiness_sha installer_sha256=$installer_sha prohibited_actions=host_reboot,vm_power_change,vm_reset,firewall_change,secret_read,database_write" \
>"$receipt_tmp"
chmod 0640 "$receipt_tmp"
if ! mv -n "$receipt_tmp" "$INSTALL_RECEIPT_PATH" \
|| [ -e "$receipt_tmp" ]; then
rm -f "$receipt_tmp" >/dev/null 2>&1 || true
return 1
fi
INSTALL_RECEIPT_WRITTEN=1
INSTALL_RECEIPT_REF="host112-install:${RUN_ID}:${INSTALL_ATTEMPT_ID}"
}
handle_apply_failure() {
local original_exit="${1:-1}" terminal="install_failed_rollback_failed"
trap - ERR INT TERM
set +e
if [ "$ROLLBACK_IN_PROGRESS" -eq 1 ]; then
exit 74
fi
ROLLBACK_IN_PROGRESS=1
if [ "$TRANSACTION_ACTIVE" -eq 1 ] && [ -n "$BACKUP_DIR" ]; then
ROLLBACK_ATTEMPTED=1
if restore_from_backup "$BACKUP_DIR" && verify_rollback "$BACKUP_DIR"; then
ROLLBACK_VERIFIED=1
terminal="install_failed_rolled_back_verified"
fi
fi
refresh_gate_state >/dev/null 2>&1 || true
write_install_receipt "$terminal" 0 "$ROLLBACK_ATTEMPTED" "$ROLLBACK_VERIFIED" "$original_exit" >/dev/null 2>&1 || true
echo "AWOOOI_HOST112_GUEST_RECOVERY_INSTALLER=1" >&2
echo "MODE=apply_failed" >&2
echo "TERMINAL=$terminal" >&2
echo "BACKUP_DIR=${BACKUP_DIR:-none}" >&2
echo "ROLLBACK_ATTEMPTED=$ROLLBACK_ATTEMPTED" >&2
echo "ROLLBACK_VERIFIED=$ROLLBACK_VERIFIED" >&2
echo "INSTALL_RECEIPT_REF=$INSTALL_RECEIPT_REF" >&2
echo "SYNCHRONOUS_RECOVERY_START=0" >&2
if [ "$ROLLBACK_VERIFIED" -ne 1 ]; then
exit 74
fi
[ "$original_exit" -ne 0 ] || original_exit=1
exit "$original_exit"
}
static_source_preflight() {
local source service_state
for tool in bash cksum cmp flock getent install sha256sum ssh-keygen sudo systemctl systemd-analyze timeout visudo; do
command -v "$tool" >/dev/null 2>&1 || { echo "BLOCKER required_tool_missing=$tool" >&2; return 1; }
done
for source in "$READINESS_SOURCE" "$SERVICE_SOURCE" "$TIMER_SOURCE" "$CONTROL_PUBLIC_KEY_PATH" "${SCRIPT_DIR}/install-host112-guest-recovery.sh"; do
[ -f "$source" ] || { echo "BLOCKER source_missing=$source" >&2; return 1; }
done
bash -n "$READINESS_SOURCE" "${SCRIPT_DIR}/install-host112-guest-recovery.sh"
grep -Fq 'ExecStart=/usr/local/sbin/awoooi-host112-guest-readiness --apply-timer' "$SERVICE_SOURCE"
grep -Fq 'TimeoutStartSec=480' "$SERVICE_SOURCE"
grep -Fq 'Unit=awoooi-host112-guest-recovery.service' "$TIMER_SOURCE"
read -r CONTROL_KEY_TYPE CONTROL_KEY_BLOB _ <"$CONTROL_PUBLIC_KEY_PATH"
case "$CONTROL_KEY_TYPE" in
ssh-ed25519|ssh-rsa|ecdsa-sha2-*) ;;
*) echo "BLOCKER unsupported_control_public_key_type" >&2; return 1 ;;
esac
[[ "$CONTROL_KEY_BLOB" =~ ^[A-Za-z0-9+/=]+$ ]] \
|| { echo "BLOCKER invalid_control_public_key_blob" >&2; return 1; }
CONTROL_KEY_FINGERPRINT="$(ssh-keygen -lf "$CONTROL_PUBLIC_KEY_PATH" | awk 'NR == 1 {print $2}')"
[ -n "$CONTROL_KEY_FINGERPRINT" ] || { echo "BLOCKER control_public_key_fingerprint_missing" >&2; return 1; }
agent99_direct_authorization_ready \
|| { echo "BLOCKER agent99_99_direct_authorization_not_ready" >&2; return 1; }
service_state="$(systemctl is-active awoooi-host112-guest-recovery.service 2>/dev/null || true)"
case "$service_state" in
active|activating) echo "BLOCKER host112_recovery_service_busy" >&2; return 1 ;;
esac
sudoers_tmp="$(mktemp)"
{
printf '%s ALL=(root) NOPASSWD: %s %s\n' \
"$TARGET_USER" "$READINESS_TARGET" "$SUDOERS_ARGUMENT_REGEX"
printf '%s ALL=(root) NOPASSWD: %s %s\n' \
"$TARGET_USER" "$READINESS_TARGET" "$SUDOERS_DRY_RUN_ARGUMENT_REGEX"
} >"$sudoers_tmp"
visudo -cf "$sudoers_tmp" >/dev/null
}
post_install_static_gate() {
STATIC_POST_GATE_READY=0
TARGET_HASHES_MATCH=0
UNIT_STATIC_VERIFY_READY=0
if cmp -s "$READINESS_SOURCE" "$READINESS_TARGET" \
&& cmp -s "${SCRIPT_DIR}/install-host112-guest-recovery.sh" "$INSTALLER_TARGET" \
&& cmp -s "$SERVICE_SOURCE" "$SERVICE_TARGET" \
&& cmp -s "$TIMER_SOURCE" "$TIMER_TARGET"; then
TARGET_HASHES_MATCH=1
fi
if systemd-analyze verify "$SERVICE_TARGET" "$TIMER_TARGET" >/dev/null 2>&1; then
UNIT_STATIC_VERIFY_READY=1
fi
refresh_gate_state
if [ "$TARGET_HASHES_MATCH" -eq 1 ] \
&& [ "$UNIT_STATIC_VERIFY_READY" -eq 1 ] \
&& [ "$TARGET_FILES_READY" -eq 1 ] \
&& [ "$UNIT_CONTRACT_READY" -eq 1 ] \
&& [ "$SUDO_POLICY_READY" -eq 1 ] \
&& [ "$AGENT99_99_DIRECT_ROUTE_READY" -eq 1 ] \
&& [ "$READINESS_NO_WRITE_READY" -eq 1 ] \
&& [ "$TIMER_ENABLED" = "enabled" ]; then
STATIC_POST_GATE_READY=1
fi
echo "TARGET_HASHES_MATCH=$TARGET_HASHES_MATCH"
echo "UNIT_STATIC_VERIFY_READY=$UNIT_STATIC_VERIFY_READY"
echo "STATIC_POST_GATE_READY=$STATIC_POST_GATE_READY"
[ "$STATIC_POST_GATE_READY" -eq 1 ]
}
wait_for_recovery_service_idle() {
local deadline state
deadline=$(( $(date +%s) + 30 ))
while true; do
state="$(systemctl is-active awoooi-host112-guest-recovery.service 2>/dev/null || true)"
case "$state" in
active|activating) ;;
*) return 0 ;;
esac
[ "$(date +%s)" -lt "$deadline" ] || return 1
sleep 1
done
}
resolve_target_user
if [ "$MODE" = "check" ]; then
check
exit $?
fi
require_root
INSTALL_ATTEMPT_ID="$(date +%Y%m%dT%H%M%S%z)-$$"
if [ "$MODE" = "rollback" ]; then
case "$ROLLBACK_DIR" in
"$STATE_ROOT"/backups/*) ;;
*) echo "BLOCKER invalid_host112_rollback_dir" >&2; exit 64 ;;
esac
[ -f "$ROLLBACK_DIR/manifest.tsv" ] \
|| { echo "BLOCKER rollback_manifest_missing" >&2; exit 66; }
BACKUP_DIR="$ROLLBACK_DIR"
exec 9>"$MANAGER_LOCK_FILE"
flock -w 30 9 || { echo "BLOCKER host112_installer_single_flight_busy" >&2; exit 75; }
ROLLBACK_ATTEMPTED=1
if restore_from_backup "$ROLLBACK_DIR" && verify_rollback "$ROLLBACK_DIR"; then
ROLLBACK_VERIFIED=1
fi
refresh_gate_state >/dev/null 2>&1 || true
write_install_receipt \
"manual_rollback_$(if [ "$ROLLBACK_VERIFIED" -eq 1 ]; then printf verified; else printf failed; fi)" \
0 1 "$ROLLBACK_VERIFIED" 0
echo "AWOOOI_HOST112_GUEST_RECOVERY_INSTALLER=1"
echo "MODE=rollback"
echo "ROLLBACK_APPLIED=$ROLLBACK_VERIFIED"
echo "ROLLBACK_VERIFIED=$ROLLBACK_VERIFIED"
echo "INSTALL_RECEIPT_REF=$INSTALL_RECEIPT_REF"
echo "RECOVERY_EXECUTION_PERFORMED=0"
[ "$ROLLBACK_VERIFIED" -eq 1 ]
exit $?
fi
static_source_preflight
exec 9>"$MANAGER_LOCK_FILE"
flock -w 30 9 || { echo "BLOCKER host112_installer_single_flight_busy" >&2; exit 75; }
create_backup
echo "BACKUP_DIR=$BACKUP_DIR"
TRANSACTION_ACTIVE=1
trap 'handle_apply_failure $?' ERR
trap 'handle_apply_failure 130' INT TERM
timeout 20 systemctl stop awoooi-host112-guest-recovery.timer >/dev/null 2>&1
wait_for_recovery_service_idle
install -m 0755 "$READINESS_SOURCE" "$READINESS_TARGET"
install -m 0755 "${SCRIPT_DIR}/install-host112-guest-recovery.sh" "$INSTALLER_TARGET"
install -m 0644 "$SERVICE_SOURCE" "$SERVICE_TARGET"
install -m 0644 "$TIMER_SOURCE" "$TIMER_TARGET"
install -m 0440 "$sudoers_tmp" "$SUDOERS_TARGET"
install -d -m 0700 -o "$TARGET_USER" -g "$USER_GROUP" "$AUTH_DIR"
if [ ! -f "$AUTH_FILE" ]; then
install -m 0600 -o "$TARGET_USER" -g "$USER_GROUP" /dev/null "$AUTH_FILE"
fi
AUTH_TMP="$(mktemp "${AUTH_FILE}.tmp.XXXXXX")"
awk -v blob="$CONTROL_KEY_BLOB" \
'index($0, blob) == 0 && index($0, "awoooi-host112-probe-110") == 0' \
"$AUTH_FILE" >"$AUTH_TMP"
printf 'from="%s",restrict,command="%s --check" %s %s awoooi-host112-probe-110\n' \
"$CONTROL_SOURCE_ADDRESS" "$READINESS_TARGET" "$CONTROL_KEY_TYPE" "$CONTROL_KEY_BLOB" >>"$AUTH_TMP"
chown "$TARGET_USER:$USER_GROUP" "$AUTH_TMP"
chmod 0600 "$AUTH_TMP"
mv "$AUTH_TMP" "$AUTH_FILE"
AUTH_TMP=""
systemctl daemon-reload
systemctl enable awoooi-host112-guest-recovery.timer >/dev/null
post_install_static_gate
systemctl start awoooi-host112-guest-recovery.timer
TIMER_ACTIVATION_PERFORMED=1
check
CHECK_GATE_READY=1
INSTALL_VERIFIED=1
write_install_receipt "installed_timer_activated_recovery_separate" 1 0 0 0
TRANSACTION_ACTIVE=0
trap - ERR INT TERM
echo "AWOOOI_HOST112_GUEST_RECOVERY_INSTALLER=1"
echo "MODE=apply"
echo "TRACE_ID=$TRACE_ID"
echo "RUN_ID=$RUN_ID"
echo "WORK_ITEM_ID=$WORK_ITEM_ID"
echo "BACKUP_DIR=$BACKUP_DIR"
echo "CONTROL_KEY_FINGERPRINT=$CONTROL_KEY_FINGERPRINT"
echo "INSTALL_VERIFIED=$INSTALL_VERIFIED"
echo "INSTALL_RECEIPT_WRITTEN=$INSTALL_RECEIPT_WRITTEN"
echo "INSTALL_RECEIPT_REF=$INSTALL_RECEIPT_REF"
echo "RECOVERY_EXECUTION_PERFORMED=0"
echo "RECOVERY_RUN_ID=none"
echo "RECOVERY_RECEIPT_REF=none"
echo "RECOVERY_EXECUTION_STATUS=separate_timer_or_agent99_run_required"
echo "AGENT99_99_DIRECT_RUNTIME_PROBE=not_started"
echo "REBOOT_PERFORMED=0"
echo "VM_POWER_CHANGE_PERFORMED=0"
echo "SECRET_READ=0"
echo "RESTRICTED_CONTROL_KEY_INSTALLED=1"