#!/bin/sh # Copyright (C) 2026 Doan Luu Duc Tai # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . set -eu umask 077 [ "$#" = 1 ] || { echo 'Usage: ./restore /absolute/path/to/new-stack' >&2; exit 1; } case "$1" in /*) ;; *) echo 'Destination must be absolute.' >&2; exit 1 ;; esac [ "$(uname -s):$(uname -m)" = Linux:x86_64 ] || { echo 'Requires x86_64 Linux.' >&2; exit 1; } [ "$(id -u)" != 0 ] || { echo 'Run as the service user, not root.' >&2; exit 1; } BUNDLE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) (cd "$BUNDLE" && sha256sum --check --strict SHA256SUMS) RELEASE_DIGEST=$(sha256sum "$BUNDLE/release.tar.gz") RELEASE_DIGEST=${RELEASE_DIGEST%% *} BACKUP_DIGEST=$(sha256sum "$BUNDLE/backup.tar.gz.gpg") BACKUP_DIGEST=${BACKUP_DIGEST%% *} INTENT="$RELEASE_DIGEST:$BACKUP_DIGEST" MARKER="$1/var/guix-recovery" pending() { [ -f "$MARKER" ] && [ ! -L "$MARKER" ] && [ -O "$MARKER" ] && [ "$(cat "$MARKER")" = "$INTENT" ] } RESUME=false if [ -e "$1" ] || [ -L "$1" ]; then pending || { echo 'Destination exists without a matching pending recovery; refusing to overwrite.' >&2; exit 1; } RESUME=true fi DIGEST=$(sha256sum "$BUNDLE/runtime.tar.gz") DIGEST=${DIGEST%% *} [ -f "/gnu/.stack-runtimes/$DIGEST" ] && [ "$(stat -c %u "/gnu/.stack-runtimes/$DIGEST")" = 0 ] || { echo 'Install the trusted closure first: sudo ./stack-recovery/bootstrap' >&2; exit 1; } for HELPER in newuidmap newgidmap; do command -v "$HELPER" >/dev/null || { echo "Install host $HELPER before restoring." >&2; exit 1; } done for MAPPING in /etc/subuid /etc/subgid; do awk -F: -v name="$(id -un)" -v uid="$(id -u)" \ '($1 == name || $1 == uid) && $3 >= 65536 { found=1 } END { exit !found }' "$MAPPING" || { echo "Allocate at least 65536 subordinate IDs in $MAPPING for $(id -un)." >&2; exit 1; } done [ -n "${XDG_RUNTIME_DIR:-}" ] && [ -d "$XDG_RUNTIME_DIR" ] && [ -w "$XDG_RUNTIME_DIR" ] || { echo 'A writable XDG_RUNTIME_DIR is required; use a normal login session.' >&2; exit 1; } [ "$(cat /proc/sys/net/ipv4/ip_unprivileged_port_start)" -le 80 ] || { echo 'Host administrator must set net.ipv4.ip_unprivileged_port_start=80.' >&2; exit 1; } STAGE=$(mktemp -d) trap 'chmod -R u+w -- "$STAGE"; rm -rf -- "$STAGE"' EXIT HUP INT TERM mkdir "$STAGE/runtime" tar -xzf "$BUNDLE/runtime.tar.gz" --no-same-owner -C "$STAGE/runtime" [ -x "$STAGE/runtime/bin/podman" ] && [ -x "$STAGE/runtime/bin/gpg" ] || { echo 'Guix closure incomplete; rerun the trusted capsule bootstrap.' >&2; exit 1; } PATH="$STAGE/runtime/bin:$PATH" CONTAINERS_CONF="$BUNDLE/containers.conf" export PATH CONTAINERS_CONF podman --root "$STAGE/storage" --runroot "$STAGE/run" info >/dev/null if [ "$RESUME" = false ]; then "$BUNDLE/stack" restore "$BUNDLE/backup.tar.gz.gpg" "$1" fi LOCK="$1/var/guix-recovery.lock" [ ! -L "$LOCK" ] && { [ ! -e "$LOCK" ] || [ -f "$LOCK" ]; } || { echo 'Recovery lock is not a regular file.' >&2; exit 1; } exec 9>>"$LOCK" flock -n 9 || { echo 'Another recovery is running for this destination.' >&2; exit 1; } if [ "$RESUME" = true ]; then pending || { echo 'Pending recovery changed; refusing activation.' >&2; exit 1; } else printf '%s\n' "$INTENT" > "$MARKER" fi [ -f "$1/var/image-lock.json" ] && [ -f "$1/var/images.tar" ] || { echo 'Backup has no complete image export; recovered data retained. Use backup --include-images.' >&2; exit 1; } printf '%s\n' 'Data restored. Activating paired release; rerun this restore command after any failure.' "$BUNDLE/stack" --root "$1" activate "$BUNDLE/release.tar.gz" 9>&- rm -- "$MARKER"