summaryrefslogtreecommitdiff
path: root/infra/guix/restore.sh
blob: 22e7e1034ba0275c8058d1ff055ddbaa8d74e0f2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
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"