diff options
Diffstat (limited to 'infra/guix/restore.sh')
| -rw-r--r-- | infra/guix/restore.sh | 90 |
1 files changed, 90 insertions, 0 deletions
diff --git a/infra/guix/restore.sh b/infra/guix/restore.sh new file mode 100644 index 0000000..22e7e10 --- /dev/null +++ b/infra/guix/restore.sh @@ -0,0 +1,90 @@ +#!/bin/sh +# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai> +# +# 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 <https://www.gnu.org/licenses/>. + +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" |
