* Join the tailnet from a cidata tailscale_authkey A tailscale_authkey file on the autoinstall drive gets the installed machine onto the tailnet on its own: the tailscale package is bundled in the offline mirror and installed only when the key is present, the key is staged root-only at /etc/tailscale/authkey, and a oneshot first-boot unit runs tailscale up once the network is really there. On success the key is deleted and the unit disables itself; on failure both survive to retry on the next boot, so an offline install joins whenever it first gets connectivity. ufw allows traffic in on tailscale0 via the same write-the-rule-then-ignore-the-chroot-exit-code dance as the SSH phase. Bundling rather than fetching at first boot is deliberate: pacman -Sy tailscale on a fresh machine is a partial upgrade, and pacman -Syu means an unattended full system upgrade racing the user's first login. The offline mirror is a coherent snapshot; installing from it keeps the target consistent and the install offline-capable. See plans/tailscale.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xc74h4WSYpuYPGuYbFAR1q * Do not hold boot hostage for the tailnet join Review caught that the join unit was Type=oneshot wanted by multi-user.target: target units implicitly gain After= for their Wants=, so an offline machine's boot -- SDDM included -- stalled for the full 10-minute timeout, and once timed out the join never retried within that boot. Type=simple counts as started the moment it forks, so boot proceeds while the join retries in the background for as long as the boot lasts. Cleanup moves inside the script, sequenced after tailscale up succeeds, which preserves the retry-across-boots behavior: key and unit survive a boot with no connectivity, and the key never outlives a successful join. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xc74h4WSYpuYPGuYbFAR1q --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
173 lines
6.1 KiB
Bash
173 lines
6.1 KiB
Bash
#!/bin/bash
|
|
#
|
|
# Unit tests for omarchy-cidata-load. The script takes a path prefix, so every
|
|
# case runs against a throwaway sandbox with mount/umount/udevadm stubbed out:
|
|
# "mounting" copies the fake drive's contents into the mountpoint, and every
|
|
# stub logs its invocation so the cases can assert what was (not) called.
|
|
|
|
set -euo pipefail
|
|
|
|
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
|
CIDATA_LOAD="$ROOT/configs/airootfs/usr/local/bin/omarchy-cidata-load"
|
|
|
|
pass() {
|
|
printf 'ok - %s\n' "$1"
|
|
}
|
|
|
|
fail() {
|
|
local description="$1"
|
|
local detail="${2:-}"
|
|
|
|
[[ -n $detail ]] && printf '%s\n' "$detail" >&2
|
|
printf 'not ok - %s\n' "$description" >&2
|
|
exit 1
|
|
}
|
|
|
|
work=$(mktemp -d)
|
|
trap 'chmod -R u+w "$work"; rm -rf "$work"' EXIT
|
|
|
|
stub_dir="$work/stubs"
|
|
mkdir -p "$stub_dir"
|
|
|
|
cat >"$stub_dir/udevadm" <<'STUB'
|
|
#!/bin/bash
|
|
printf 'udevadm %s\n' "$*" >>"$TEST_LOG"
|
|
STUB
|
|
|
|
cat >"$stub_dir/mount" <<'STUB'
|
|
#!/bin/bash
|
|
printf 'mount %s\n' "$*" >>"$TEST_LOG"
|
|
[[ ${MOUNT_FAIL:-} == 1 ]] && exit 32
|
|
device=$3 mountpoint=$4
|
|
cp -a "$(readlink -f "$device")"/. "$mountpoint"/
|
|
STUB
|
|
|
|
cat >"$stub_dir/umount" <<'STUB'
|
|
#!/bin/bash
|
|
printf 'umount %s\n' "$*" >>"$TEST_LOG"
|
|
STUB
|
|
|
|
chmod +x "$stub_dir"/*
|
|
|
|
new_sandbox() {
|
|
sandbox=$(mktemp -d "$work/sandbox.XXXXXX")
|
|
mkdir -p "$sandbox/dev/disk/by-label" "$sandbox/root" "$sandbox/media"
|
|
export TEST_LOG="$sandbox/calls.log"
|
|
: >"$TEST_LOG"
|
|
}
|
|
|
|
attach_drive() {
|
|
ln -s "$sandbox/media" "$sandbox/dev/disk/by-label/$1"
|
|
}
|
|
|
|
run_load() {
|
|
PATH="$stub_dir:$PATH" "$CIDATA_LOAD" "$sandbox"
|
|
}
|
|
|
|
write_required_pair() {
|
|
echo '{"disk_config": {}}' >"$sandbox/media/user_configuration.json"
|
|
echo '{"users": []}' >"$sandbox/media/user_credentials.json"
|
|
}
|
|
|
|
# No cidata drive attached: fall back to the wizard without mounting anything.
|
|
new_sandbox
|
|
! run_load || fail "no drive exits non-zero"
|
|
! grep -q '^mount ' "$TEST_LOG" || fail "no drive mounts nothing"
|
|
pass "no drive falls back to the wizard"
|
|
|
|
# The probe must wait for udev to finish enumerating before concluding there
|
|
# is no drive.
|
|
grep -q '^udevadm settle$' "$TEST_LOG" || fail "probe settles udev first"
|
|
pass "probe settles udev first"
|
|
|
|
# A drive with the full file set: everything lands in /root and the drive is
|
|
# unmounted afterwards.
|
|
new_sandbox
|
|
attach_drive cidata
|
|
write_required_pair
|
|
echo "Jeff" >"$sandbox/media/user_full_name.txt"
|
|
echo "jeff@example.com" >"$sandbox/media/user_email_address.txt"
|
|
echo "false" >"$sandbox/media/user_encrypt_installation.txt"
|
|
echo 'ssh-ed25519 AAAA jeff@host' >"$sandbox/media/authorized_keys"
|
|
echo 'tskey-auth-kFAKEKEY' >"$sandbox/media/tailscale_authkey"
|
|
run_load || fail "full file set loads"
|
|
for file in user_configuration.json user_credentials.json user_full_name.txt user_email_address.txt user_encrypt_installation.txt authorized_keys tailscale_authkey; do
|
|
[[ -f $sandbox/root/$file ]] || fail "full file set copies $file"
|
|
done
|
|
grep -q '^umount ' "$TEST_LOG" || fail "full file set unmounts the drive"
|
|
pass "full file set loads, copies everything, and unmounts"
|
|
|
|
# The uppercase label variant some tools produce works too.
|
|
new_sandbox
|
|
attach_drive CIDATA
|
|
write_required_pair
|
|
run_load || fail "uppercase CIDATA label loads"
|
|
pass "uppercase CIDATA label loads"
|
|
|
|
# The required pair alone is a valid autoinstall drive; the optional files
|
|
# stay optional.
|
|
new_sandbox
|
|
attach_drive cidata
|
|
write_required_pair
|
|
run_load || fail "required pair alone loads"
|
|
[[ ! -e $sandbox/root/authorized_keys ]] || fail "required pair alone copies no optional files"
|
|
pass "required pair alone loads without optional files"
|
|
|
|
# Optional files are copied individually when present.
|
|
new_sandbox
|
|
attach_drive cidata
|
|
write_required_pair
|
|
echo 'ssh-ed25519 AAAA jeff@host' >"$sandbox/media/authorized_keys"
|
|
run_load || fail "required pair plus authorized_keys loads"
|
|
[[ -f $sandbox/root/authorized_keys ]] || fail "authorized_keys is copied when present"
|
|
[[ ! -e $sandbox/root/user_full_name.txt ]] || fail "absent optional files are not copied"
|
|
[[ ! -e $sandbox/root/tailscale_authkey ]] || fail "absent tailscale_authkey is not copied"
|
|
pass "present optional files are copied, absent ones skipped"
|
|
|
|
# Half the required pair is not an autoinstall drive: unmount and fall back.
|
|
new_sandbox
|
|
attach_drive cidata
|
|
echo '{"disk_config": {}}' >"$sandbox/media/user_configuration.json"
|
|
! run_load || fail "half the required pair exits non-zero"
|
|
[[ ! -e $sandbox/root/user_configuration.json ]] || fail "half the required pair copies nothing"
|
|
grep -q '^umount ' "$TEST_LOG" || fail "half the required pair still unmounts"
|
|
pass "half the required pair falls back and unmounts"
|
|
|
|
# An empty drive labeled cidata is not an autoinstall drive either.
|
|
new_sandbox
|
|
attach_drive cidata
|
|
! run_load || fail "empty drive exits non-zero"
|
|
grep -q '^umount ' "$TEST_LOG" || fail "empty drive still unmounts"
|
|
pass "empty drive falls back and unmounts"
|
|
|
|
# A drive that will not mount falls back rather than failing the boot.
|
|
new_sandbox
|
|
attach_drive cidata
|
|
write_required_pair
|
|
! MOUNT_FAIL=1 run_load || fail "mount failure exits non-zero"
|
|
! grep -q '^umount ' "$TEST_LOG" || fail "mount failure has nothing to unmount"
|
|
pass "mount failure falls back to the wizard"
|
|
|
|
# A copy failure must not report a loaded drive: the install would start with
|
|
# missing inputs. Fall back and let the wizard produce them instead.
|
|
new_sandbox
|
|
attach_drive cidata
|
|
write_required_pair
|
|
chmod 555 "$sandbox/root"
|
|
! run_load 2>/dev/null || fail "copy failure exits non-zero"
|
|
grep -q '^umount ' "$TEST_LOG" || fail "copy failure still unmounts"
|
|
pass "copy failure falls back and unmounts"
|
|
|
|
# A partial copy must clean up after itself: the wizard removes only what it
|
|
# writes, so anything the loader left behind would leak into the interactive
|
|
# install that follows.
|
|
new_sandbox
|
|
attach_drive cidata
|
|
write_required_pair
|
|
echo 'ssh-ed25519 AAAA jeff@host' >"$sandbox/media/authorized_keys"
|
|
mkdir "$sandbox/root/user_credentials.json"
|
|
! run_load 2>/dev/null || fail "partial copy exits non-zero"
|
|
[[ ! -e $sandbox/root/user_configuration.json ]] || fail "partial copy removes what it copied"
|
|
[[ ! -e $sandbox/root/authorized_keys ]] || fail "partial copy leaves no authorized_keys behind"
|
|
pass "partial copy cleans up what it copied"
|