repos
/ taproot main

taproot

mirror

The dotfiles and containers I use to set up a machine for development, one container to write code in and another that runs a local coding model on the desktop's GPU.

alpine-linuxcaddydevelopment-environmentdockerdotfileshomelabinfrastructureneovimserver-configurationtmux

10.2 KB · 319 lines · Bash Raw History
  1#!/bin/sh
  2#
  3# restic-setup.sh
  4#
  5# Check the restic credentials in ~/.restic, and walk through entering them if
  6# they are missing or wrong, so setting up backups on a fresh container is one
  7# command that says what it wants rather than two files to open in nvim and
  8# get subtly wrong.
  9#
 10#   restic-setup             check, then offer to fix whatever is wrong
 11#   restic-setup --check     check only, quietly; exit 0 if backups will work
 12#   restic-setup --password  print a suggested password and exit, writing nothing
 13#
 14# The files it manages, all in the bythewood-restic volume:
 15#
 16#   ~/.restic/b2-env           B2_ACCOUNT_ID, B2_ACCOUNT_KEY, RESTIC_HOST
 17#   ~/.restic/password         password for the webdev repo
 18#
 19# From a taproot clone this is `make restic`, and --check is what `make doctor`
 20# calls, so keep it silent and keep its exit code meaningful.
 21
 22set -eu
 23
 24RESTIC_DIR="$HOME/.restic"
 25B2_ENV="$RESTIC_DIR/b2-env"
 26PW_FILE="$RESTIC_DIR/password"
 27REPO="b2:overshard-backups:webdev"
 28
 29# Reads the header above rather than a line range, so editing the comment cannot
 30# silently turn --help into someone else's text.
 31usage() {
 32    awk 'NR>1 && /^#/ { sub(/^#[ ]?/, ""); print; next } NR>1 { exit }' "$0"
 33}
 34
 35# 32 characters of /dev/urandom in groups of eight. Long enough that nobody is
 36# ever going to type it by hand, grouped so it can be read back off a screen.
 37gen_password() {
 38    LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom \
 39        | head -c 32 \
 40        | sed 's/.\{8\}/&-/g; s/-$//'
 41    echo ""
 42}
 43
 44CHECK_ONLY=no
 45case "${1:-}" in
 46--check) CHECK_ONLY=yes ;;
 47--password)
 48    # The password alone on stdout and the note on stderr, so it pipes cleanly.
 49    gen_password
 50    echo "" >&2
 51    echo "a suggestion, nothing was written. put it in 1Password." >&2
 52    exit 0 ;;
 53--help|-h) usage; exit 0 ;;
 54"") ;;
 55*) echo "unknown option: $1" >&2; usage >&2; exit 2 ;;
 56esac
 57
 58# Everything below reads the current state without changing it, so the check
 59# path and the interactive path agree on what is wrong.
 60B2_ID=""
 61B2_KEY=""
 62HOSTTAG=""
 63if [ -f "$B2_ENV" ]; then
 64    # A subshell, so a malformed b2-env cannot leak variables or a `set -e`
 65    # failure into this script.
 66    eval "$(
 67        . "$B2_ENV" >/dev/null 2>&1 || true
 68        printf 'B2_ID=%s\n'   "$(printf '%s' "${B2_ACCOUNT_ID:-}"  | sed "s/'/'\\\\''/g; s/^/'/; s/$/'/")"
 69        printf 'B2_KEY=%s\n'  "$(printf '%s' "${B2_ACCOUNT_KEY:-}" | sed "s/'/'\\\\''/g; s/^/'/; s/$/'/")"
 70        printf 'HOSTTAG=%s\n' "$(printf '%s' "${RESTIC_HOST:-}"    | sed "s/'/'\\\\''/g; s/^/'/; s/$/'/")"
 71    )"
 72fi
 73
 74have_all_fields() {
 75    [ -n "$B2_ID" ] && [ -n "$B2_KEY" ] && [ -n "$HOSTTAG" ] && [ -s "$PW_FILE" ]
 76}
 77
 78# Opening the repository is the only check that proves anything, since a wrong
 79# key, a wrong password and no network all land here. Timed out because restic
 80# retries a failing backend about ten times with backoff and `make doctor`
 81# calls this. 124 is the timeout's own exit code.
 82RESTIC_TIMEOUT=${RESTIC_TIMEOUT:-30}
 83restic_probe() {
 84    B2_ACCOUNT_ID="$B2_ID" B2_ACCOUNT_KEY="$B2_KEY" \
 85    RESTIC_REPOSITORY="$REPO" RESTIC_PASSWORD_FILE="$PW_FILE" \
 86        timeout "$RESTIC_TIMEOUT" restic cat config "$@"
 87}
 88
 89repo_opens() {
 90    restic_probe >/dev/null 2>&1
 91}
 92
 93if [ "$CHECK_ONLY" = yes ]; then
 94    have_all_fields || exit 1
 95    repo_opens || exit 1
 96    exit 0
 97fi
 98
 99mask() {
100    v=$1
101    n=$(printf '%s' "$v" | wc -c)
102    if   [ -z "$v" ];    then printf 'not set'
103    elif [ "$n" -le 8 ]; then printf '********'
104    else printf '%s...%s' "$(printf '%s' "$v" | cut -c1-4)" "$(printf '%s' "$v" | cut -c$((n-1))-)"
105    fi
106}
107
108echo "restic credentials in $RESTIC_DIR"
109echo ""
110printf '  %-18s %s\n' "B2_ACCOUNT_ID"  "$(mask "$B2_ID")"
111printf '  %-18s %s\n' "B2_ACCOUNT_KEY" "$(mask "$B2_KEY")"
112printf '  %-18s %s\n' "RESTIC_HOST"    "${HOSTTAG:-not set}"
113printf '  %-18s %s\n' "repo password"  "$([ -s "$PW_FILE" ] && echo set || echo 'not set')"
114echo ""
115
116if have_all_fields; then
117    printf 'opening %s ... ' "$REPO"
118    if repo_opens; then
119        echo "ok"
120        echo ""
121        echo "backups are working. nothing to do here."
122        echo ""
123        echo "  make backup      take a snapshot from this machine"
124        echo "  make snapshots   last snapshot per host, and what the repo costs"
125        exit 0
126    fi
127    echo "FAILED"
128    echo ""
129    echo "the files are filled in but the repository will not open. that is"
130    echo "usually a revoked or mistyped B2 key, a wrong repo password, or no"
131    echo "network. entering them again is the fastest way to find out which."
132else
133    echo "something is missing, so backups will not run yet."
134fi
135
136echo ""
137
138if [ ! -t 0 ]; then
139    echo "this needs a terminal to read the values. run it as:" >&2
140    echo "" >&2
141    echo "  make restic" >&2
142    exit 1
143fi
144
145cat <<'GUIDE'
146------------------------------------------------------------------------
147where these come from
148
149  Everything is in 1Password first. Look for the Backblaze B2 item (the
150  application key) and the restic repository password. If both are there,
151  paste them below and you are done.
152
153  If you need a NEW B2 application key, at https://secure.backblaze.com
154
155    1. Buckets. There should be a private bucket named overshard-backups.
156       Create it if it is gone: private, no encryption, no object lock.
157       Restic does its own encryption, and object lock would break prune.
158
159    2. Application Keys, then "Add a New Application Key".
160         name         anything, e.g. webdev-desktop
161         bucket       overshard-backups, not "All"
162         access       Read and Write
163         leave the file-name prefix and duration empty
164
165    3. Save it. The keyID and the applicationKey are shown ONCE. The
166       keyID is B2_ACCOUNT_ID, the applicationKey is B2_ACCOUNT_KEY.
167       Put both back into 1Password before you close the page.
168
169  The repo password is NOT recoverable. If it is lost, the snapshots in
170  that repository are lost with it, and the only fix is a new repository.
171------------------------------------------------------------------------
172GUIDE
173echo ""
174
175# Enter keeps whatever is already there, so one field can be changed without
176# retyping the rest. These set a global rather than printing a value, because
177# inside $(...) the prompt is captured too and you end up typing blind.
178ANSWER=""
179
180ask() {
181    printf '%s' "$1"
182    [ -n "$2" ] && printf ' [enter keeps %s]' "$(mask "$2")"
183    printf ': '
184    read -r ANSWER || ANSWER=""
185    [ -n "$ANSWER" ] || ANSWER=$2
186}
187
188ask_secret() {
189    printf '%s' "$1"
190    [ -n "$2" ] && printf ' [enter keeps it]'
191    printf ': '
192    stty -echo 2>/dev/null || true
193    read -r ANSWER || ANSWER=""
194    stty echo 2>/dev/null || true
195    printf '\n'
196    [ -n "$ANSWER" ] || ANSWER=$2
197}
198
199ask        "B2 keyID"          "$B2_ID";  NEW_ID=$ANSWER
200ask_secret "B2 applicationKey" "$B2_KEY"; NEW_KEY=$ANSWER
201
202# Retention is applied per host, so a typo here quietly gives a third machine
203# its own 7/4/6 window and stops pruning the real one.
204while :; do
205    ask "this machine, desktop or laptop" "$HOSTTAG"
206    NEW_HOST=$ANSWER
207    case "$NEW_HOST" in
208    desktop|laptop) break ;;
209    "") echo "  nothing entered, and nothing to keep" ;;
210    *)  echo "  must be exactly 'desktop' or 'laptop'" ;;
211    esac
212done
213
214CURRENT_PW=""
215[ -s "$PW_FILE" ] && CURRENT_PW=$(cat "$PW_FILE")
216
217# A generated one only works for a repository that does not exist yet, and the
218# second machine has to be given the first one's password instead, so it is
219# offered rather than just written.
220SUGGESTED=""
221if [ -n "$CURRENT_PW" ]; then
222    ask_secret "restic repository password" "$CURRENT_PW"; NEW_PW=$ANSWER
223else
224    SUGGESTED=$(gen_password)
225    echo ""
226    echo "there is no repo password here yet, so here is one:"
227    echo ""
228    echo "    $SUGGESTED"
229    echo ""
230    echo "press enter to take it, and put it in 1Password now, because it is"
231    echo "not recoverable and nothing else has a copy. if this machine is"
232    echo "joining backups that already exist, paste that repository's own"
233    echo "password over it instead, since a new one will not open it."
234    echo ""
235    printf 'restic repository password [enter takes the one above]: '
236    read -r ANSWER || ANSWER=""
237    NEW_PW=${ANSWER:-$SUGGESTED}
238    [ "$NEW_PW" = "$SUGGESTED" ] || SUGGESTED=""
239fi
240
241if [ -z "$NEW_ID" ] || [ -z "$NEW_KEY" ] || [ -z "$NEW_PW" ]; then
242    echo "" >&2
243    echo "one of the values is empty. nothing was written." >&2
244    exit 1
245fi
246
247# Written to a temp file and moved into place, so an interrupted run cannot
248# leave half a credentials file behind.
249mkdir -p "$RESTIC_DIR"
250chmod 700 "$RESTIC_DIR"
251
252umask 077
253
254tmp="$B2_ENV.tmp.$$"
255cat > "$tmp" <<ENV
256# Written by restic-setup, sourced by restic-backup, restic-restore and
257# restic-status. RESTIC_HOST is this machine's snapshot tag.
258export B2_ACCOUNT_ID="$NEW_ID"
259export B2_ACCOUNT_KEY="$NEW_KEY"
260export RESTIC_HOST="$NEW_HOST"
261ENV
262chmod 600 "$tmp"
263mv "$tmp" "$B2_ENV"
264
265tmp="$PW_FILE.tmp.$$"
266printf '%s' "$NEW_PW" > "$tmp"
267chmod 600 "$tmp"
268mv "$tmp" "$PW_FILE"
269
270echo ""
271echo "wrote $B2_ENV and $PW_FILE (0600)"
272
273if [ -n "$SUGGESTED" ]; then
274    echo ""
275    echo "the generated password, one last time, since this is the last screen"
276    echo "that will ever show it:"
277    echo ""
278    echo "    $SUGGESTED"
279fi
280
281echo ""
282
283B2_ID=$NEW_ID
284B2_KEY=$NEW_KEY
285HOSTTAG=$NEW_HOST
286
287printf 'opening %s ... ' "$REPO"
288if repo_opens; then
289    echo "ok"
290    echo ""
291    echo "backups are set up. next:"
292    echo ""
293    echo "  make backup      take a snapshot from this machine"
294    echo "  make restore     pull everything back, on a fresh container"
295    echo "  make snapshots   last snapshot per host, and what the repo costs"
296    exit 0
297fi
298
299echo "FAILED"
300echo ""
301echo "the credentials are saved but the repository did not open. in order of"
302echo "how often it is the cause:"
303echo ""
304echo "  the applicationKey is wrong, or was revoked in the B2 console"
305echo "  the key is not scoped to the overshard-backups bucket"
306echo "  the repo password is wrong, which a generated one will be if the"
307echo "    repository already existed"
308echo "  no network out of this container"
309echo ""
310echo "(it gives up after ${RESTIC_TIMEOUT}s. set RESTIC_TIMEOUT to wait longer.)"
311echo ""
312echo "the raw error, which usually names one of those:"
313echo ""
314restic_probe 2>&1 | sed 's/^/  /' | head -20 || true
315echo ""
316echo "if this is a brand new bucket with no repository in it yet, that is"
317echo "expected: restic-backup runs 'restic init' on its first run."
318exit 1