summaryrefslogtreecommitdiff
path: root/infra/guix/restore.sh
diff options
context:
space:
mode:
Diffstat (limited to 'infra/guix/restore.sh')
-rw-r--r--infra/guix/restore.sh90
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"