mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:27:02 +01:00
Split the Pi flasher and the boot-repair test harness
raspberry_pi_flash_sd.sh 661 -> 166 and test_boot_repair.sh 315 -> 104. Heredoc positions were checked before choosing seams this time. test_boot_repair.sh is the first linux_configuration script in this effort that could be verified by running it: every case builds a throwaway root in a temp dir and passes --root. 31 passed after the split, and 31 passed from a detached worktree at HEAD - a real baseline comparison rather than trusting the post-split number. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
This commit is contained in:
parent
5387c5d19d
commit
45b56b3e61
@ -0,0 +1,18 @@
|
||||
{
|
||||
"title": "Split raspberry_pi_flash_sd.sh and test_boot_repair.sh under the cap",
|
||||
"objective": "Bring raspberry_pi_flash_sd.sh (661) and test_boot_repair.sh (315) under the 250-line cap, choosing seams that avoid heredocs after the enforce_vbox_hosts failure.",
|
||||
"acceptance_criteria": [
|
||||
"Both entry scripts and all four new libs are at most 250 lines.",
|
||||
"The pre-commit shellcheck hook passes on every touched file, with no new suppression and no disable directive.",
|
||||
"No lib sources a sibling; every lib parses under bash -n.",
|
||||
"Every seam is chosen outside a heredoc: the heredoc line numbers are listed before picking cut points.",
|
||||
"test_boot_repair.sh is a sandboxed fixture harness, so it MUST be run: it has to report the same pass count after the split as a clean worktree at HEAD reports before it.",
|
||||
"raspberry_pi_flash_sd.sh is not executed - it writes partition tables to an SD card."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Executing raspberry_pi_flash_sd.sh or any flashing step.",
|
||||
"The seven deferred shell scripts.",
|
||||
"Changing what any script does when run."
|
||||
],
|
||||
"verifier": "meta/scripts/verify_shell_split.sh for both, plus running test_boot_repair.sh and comparing its pass count against the same script run from a detached worktree at HEAD, plus pre-commit run --files and check_file_length.sh."
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
{
|
||||
"intent": "Split two more linux_configuration scripts, one of which could finally be verified by actually running it.",
|
||||
"scope": [
|
||||
"single_use/features/raspberry_pi_flash_sd.sh (661 -> 166) + pi_flash_remote, pi_flash_image, pi_flash_exec",
|
||||
"boot_recovery/tests/test_boot_repair.sh (315 -> 104) + boot_repair_cases.sh",
|
||||
"Non-goal: raspberry_pi_flash_sd.sh not executed"
|
||||
],
|
||||
"changes": [
|
||||
"Both scripts were checked for heredoc positions BEFORE choosing seams, after the enforce_vbox_hosts split cut inside one. raspberry_pi_flash_sd.sh has heredocs at lines 68 and 609; the three cut points (134, 273, 460) sit clear of both.",
|
||||
"test_boot_repair.sh's fixtures and repair cases moved to boot_repair_cases.sh, leaving the assertions and the runner in the entry."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "bash linux_configuration/scripts/boot_recovery/tests/test_boot_repair.sh",
|
||||
"result": "pass",
|
||||
"evidence": "31 passed, 0 failed. This is the first linux_configuration script in the whole effort that could be RUN as verification rather than statically checked: its header documents that every case builds a throwaway root in a temp dir and passes --root, so nothing on the machine is touched."
|
||||
},
|
||||
{
|
||||
"command": "same script from a detached worktree at HEAD",
|
||||
"result": "pass",
|
||||
"evidence": "31 passed, 0 failed - the identical count, so the split neither lost nor duplicated a case. Establishing the baseline from a clean worktree rather than trusting the post-split number is the shell equivalent of the pass-count rule used throughout python_pkg."
|
||||
},
|
||||
{
|
||||
"command": "meta/scripts/verify_shell_split.sh for both scripts",
|
||||
"result": "pass",
|
||||
"evidence": "Both report 'libs sourced, functions defined' on the first attempt - no chained libs, no SCRIPT_DIR collision, no heredoc cut."
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run --files <6 changed files>",
|
||||
"result": "pass",
|
||||
"evidence": "Clean, including shellcheck over each lib in isolation."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"raspberry_pi_flash_sd.sh writes partition tables to an SD card and was not run; only the stubbed run and the static checks cover it.",
|
||||
"Checking heredoc positions before choosing a seam is currently a manual step. The verifier catches the failure afterwards via bash -n, but the splitter still has no heredoc awareness."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit; both scripts return to 661 and 315 lines and the four libs disappear.",
|
||||
"After rollback, re-run test_boot_repair.sh and expect 31 passed."
|
||||
]
|
||||
}
|
||||
221
linux_configuration/scripts/boot_recovery/tests/lib/boot_repair_cases.sh
Executable file
221
linux_configuration/scripts/boot_recovery/tests/lib/boot_repair_cases.sh
Executable file
@ -0,0 +1,221 @@
|
||||
#!/bin/bash
|
||||
# Test fixtures and the individual repair cases.
|
||||
#
|
||||
# Sourced by test_boot_repair.sh; split out to keep it under the 250-line
|
||||
# cap. Sourced rather than run, so it inherits the caller's strict mode
|
||||
# and the variables defined above the source line.
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Fixture construction
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# make_root <name> -> prints the path to a fresh fixture root
|
||||
#
|
||||
# Builds a consistent system: one complete kernel tree, an ESP whose kernel
|
||||
# and initramfs match it, a gating fstab and serial downloads.
|
||||
make_root() {
|
||||
local name="$1"
|
||||
local r="$WORKROOT/$name"
|
||||
local kver="${2:-7.1.5-arch1-2}"
|
||||
|
||||
mkdir -p "$r/usr/lib/modules/$kver/kernel/fs/fat"
|
||||
mkdir -p "$r/boot" "$r/etc" "$r/var/cache/pacman/pkg"
|
||||
|
||||
# Fake kernel image carrying the version banner boot-repair greps for.
|
||||
printf 'Linux version %s (linux@archlinux) fake image\n' "$kver" \
|
||||
>"$r/usr/lib/modules/$kver/vmlinuz"
|
||||
: >"$r/usr/lib/modules/$kver/modules.dep"
|
||||
: >"$r/usr/lib/modules/$kver/kernel/fs/fat/vfat.ko.zst"
|
||||
|
||||
# ESP contents, consistent with the kernel tree.
|
||||
cp "$r/usr/lib/modules/$kver/vmlinuz" "$r/boot/vmlinuz-linux"
|
||||
printf 'usr/lib/modules/%s/kernel/fs/fat/vfat.ko.zst\n' "$kver" \
|
||||
>"$r/boot/initramfs-linux.img"
|
||||
mkdir -p "$r/boot/EFI" "$r/boot/loader"
|
||||
|
||||
printf 'UUID=DEAD-BEEF\t/boot\tvfat\tdefaults\t0 2\n' >"$r/etc/fstab"
|
||||
printf 'ParallelDownloads = 1\n' >"$r/etc/pacman.conf"
|
||||
|
||||
printf '%s' "$r"
|
||||
}
|
||||
|
||||
run_repair() {
|
||||
"$BOOT_REPAIR" --root "$1" "${@:2}" 2>&1
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Tests
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
test_healthy_system() {
|
||||
echo "TEST: healthy system reports consistent"
|
||||
local r out rc
|
||||
r="$(make_root healthy)"
|
||||
out="$(run_repair "$r" --dry-run)"
|
||||
rc=$?
|
||||
assert_eq "exit status is 0" "$rc" "0"
|
||||
assert_contains "reports consistent" "$out" "nothing to repair"
|
||||
assert_contains "picks the right kernel" "$out" "target kernel: 7.1.5-arch1-2"
|
||||
}
|
||||
|
||||
test_stale_esp_kernel_detected() {
|
||||
echo "TEST: stale ESP kernel is detected and repaired"
|
||||
local r out rc
|
||||
r="$(make_root stale)"
|
||||
# Simulate the real incident: ESP still holds the previous kernel.
|
||||
printf 'Linux version 7.0.5-arch1-1 (linux@archlinux) fake image\n' \
|
||||
>"$r/boot/vmlinuz-linux"
|
||||
|
||||
out="$(run_repair "$r" --dry-run)"
|
||||
rc=$?
|
||||
assert_eq "dry-run exits 1" "$rc" "1"
|
||||
assert_contains "detects the mismatch" "$out" "ESP kernel is 7.0.5-arch1-1 but installed tree is 7.1.5-arch1-2"
|
||||
assert_contains "dry-run changes nothing" "$out" "would install"
|
||||
|
||||
# The ESP must be untouched by a dry run.
|
||||
assert_contains "dry-run left ESP alone" "$(cat "$r/boot/vmlinuz-linux")" "7.0.5-arch1-1"
|
||||
|
||||
out="$(run_repair "$r")"
|
||||
assert_contains "repairs the kernel image" "$out" "installed kernel 7.1.5-arch1-2 to the ESP"
|
||||
assert_contains "ESP now has the new kernel" "$(cat "$r/boot/vmlinuz-linux")" "7.1.5-arch1-2"
|
||||
}
|
||||
|
||||
test_incomplete_tree_ignored() {
|
||||
echo "TEST: a gutted module tree is not chosen as the target"
|
||||
local r out
|
||||
r="$(make_root gutted)"
|
||||
# The exact wreckage the failed upgrade leaves: metadata only, no kernel/,
|
||||
# no vmlinuz. It sorts newer, so a naive 'newest' pick would take it.
|
||||
mkdir -p "$r/usr/lib/modules/9.9.9-arch1-1"
|
||||
: >"$r/usr/lib/modules/9.9.9-arch1-1/modules.dep"
|
||||
: >"$r/usr/lib/modules/9.9.9-arch1-1/modules.alias"
|
||||
|
||||
out="$(run_repair "$r" --dry-run)"
|
||||
assert_contains "ignores the incomplete tree" "$out" "target kernel: 7.1.5-arch1-2"
|
||||
assert_not_contains "does not pick 9.9.9" "$out" "target kernel: 9.9.9-arch1-1"
|
||||
}
|
||||
|
||||
test_picks_newest_complete() {
|
||||
echo "TEST: newest COMPLETE tree wins"
|
||||
local r out kver
|
||||
r="$(make_root multi)"
|
||||
kver="7.2.0-arch1-1"
|
||||
mkdir -p "$r/usr/lib/modules/$kver/kernel/fs/fat"
|
||||
printf 'Linux version %s (linux@archlinux) fake image\n' "$kver" \
|
||||
>"$r/usr/lib/modules/$kver/vmlinuz"
|
||||
: >"$r/usr/lib/modules/$kver/modules.dep"
|
||||
|
||||
out="$(run_repair "$r" --dry-run)"
|
||||
assert_contains "selects the newer complete tree" "$out" "target kernel: 7.2.0-arch1-1"
|
||||
}
|
||||
|
||||
test_fstab_nofail_fixed() {
|
||||
echo "TEST: noauto/nofail on /boot is detected and reset"
|
||||
local r out
|
||||
r="$(make_root fstab)"
|
||||
printf 'UUID=DEAD-BEEF\t/boot\tvfat\tdefaults,noauto,nofail\t0 2\n' >"$r/etc/fstab"
|
||||
|
||||
out="$(run_repair "$r" --dry-run)"
|
||||
assert_contains "detects the silent-failure options" "$out" "noauto/nofail"
|
||||
|
||||
out="$(run_repair "$r")"
|
||||
assert_contains "resets the options" "$out" "reset to 'defaults'"
|
||||
assert_contains "fstab now gates" "$(cat "$r/etc/fstab")" "defaults"
|
||||
assert_not_contains "nofail is gone" "$(cat "$r/etc/fstab")" "nofail"
|
||||
# A backup must exist so the change is reversible.
|
||||
if compgen -G "$r/etc/fstab.bak-*" >/dev/null; then
|
||||
pass "fstab backup written"
|
||||
else
|
||||
fail "fstab backup written"
|
||||
fi
|
||||
}
|
||||
|
||||
test_shadow_files_removed() {
|
||||
echo "TEST: shadow kernel files in an unmounted /boot are removed"
|
||||
local r out
|
||||
r="$(make_root shadow)"
|
||||
# An unmounted /boot has no EFI/ or loader/ — that is what makes the files
|
||||
# below shadows on the root filesystem rather than the real ESP.
|
||||
rm -rf "${r:?}/boot"
|
||||
mkdir -p "$r/boot"
|
||||
printf 'Linux version 7.1.5-arch1-2 (linux@archlinux) fake\n' >"$r/boot/vmlinuz-linux"
|
||||
printf 'usr/lib/modules/7.1.5-arch1-2/x\n' >"$r/boot/initramfs-linux.img"
|
||||
printf 'stale\n' >"$r/boot/initramfs-linux-fallback.img"
|
||||
|
||||
out="$(run_repair "$r")"
|
||||
assert_contains "detects the shadows" "$out" "orphaned kernel file"
|
||||
assert_contains "removes them" "$out" "removed 3 shadow file"
|
||||
# The fallback image is not regenerated by a --root run, so its absence
|
||||
# proves the deletion happened. (vmlinuz-linux legitimately reappears:
|
||||
# sync_kernel_image reinstalls the target kernel straight afterwards.)
|
||||
if [[ ! -f $r/boot/initramfs-linux-fallback.img ]]; then
|
||||
pass "shadow fallback image deleted"
|
||||
else
|
||||
fail "shadow fallback image deleted"
|
||||
fi
|
||||
}
|
||||
|
||||
test_shadow_guard_refuses_real_esp() {
|
||||
echo "TEST: SAFETY — never delete from a directory that looks like a real ESP"
|
||||
local r out
|
||||
r="$(make_root guarded)"
|
||||
# EFI/ and loader/ are present (make_root creates them), so even though the
|
||||
# fixture is "unmounted", boot-repair must refuse to delete anything.
|
||||
out="$(run_repair "$r")"
|
||||
assert_contains "refuses to delete" "$out" "refusing to delete anything"
|
||||
if [[ -f $r/boot/vmlinuz-linux ]]; then
|
||||
pass "real ESP kernel preserved"
|
||||
else
|
||||
fail "real ESP kernel preserved" "boot-repair deleted a real ESP kernel!"
|
||||
fi
|
||||
}
|
||||
|
||||
test_parallel_downloads_fixed() {
|
||||
echo "TEST: ParallelDownloads > 1 is detected and set to 1"
|
||||
local r out
|
||||
r="$(make_root paralleldl)"
|
||||
printf 'ParallelDownloads = 5\n' >"$r/etc/pacman.conf"
|
||||
|
||||
out="$(run_repair "$r" --dry-run)"
|
||||
assert_contains "detects the hook-segfault trigger" "$out" "ParallelDownloads = 5"
|
||||
|
||||
out="$(run_repair "$r")"
|
||||
assert_contains "sets it to 1" "$out" "set ParallelDownloads = 1"
|
||||
assert_contains "config updated" "$(cat "$r/etc/pacman.conf")" "ParallelDownloads = 1"
|
||||
}
|
||||
|
||||
test_missing_modules_dep() {
|
||||
echo "TEST: missing modules.dep is reported"
|
||||
local r out
|
||||
r="$(make_root nodep)"
|
||||
rm -f "$r/usr/lib/modules/7.1.5-arch1-2/modules.dep"
|
||||
|
||||
out="$(run_repair "$r" --dry-run)"
|
||||
assert_contains "detects missing modules.dep" "$out" "modules.dep missing for 7.1.5-arch1-2"
|
||||
}
|
||||
|
||||
test_no_kernel_at_all() {
|
||||
echo "TEST: no complete kernel tree gives actionable offline guidance"
|
||||
local r out rc
|
||||
r="$(make_root nokernel)"
|
||||
rm -rf "$r/usr/lib/modules"
|
||||
mkdir -p "$r/usr/lib/modules"
|
||||
|
||||
out="$(run_repair "$r" --dry-run)"
|
||||
rc=$?
|
||||
assert_eq "exits non-zero" "$rc" "1"
|
||||
assert_contains "explains the offline recovery" "$out" "pacman -U /var/cache/pacman/pkg/linux-"
|
||||
}
|
||||
|
||||
test_help_and_bad_args() {
|
||||
echo "TEST: CLI contract"
|
||||
local out rc
|
||||
out="$("$BOOT_REPAIR" --help 2>&1)"
|
||||
rc=$?
|
||||
assert_eq "--help exits 0" "$rc" "0"
|
||||
assert_contains "help mentions dry-run" "$out" "--dry-run"
|
||||
|
||||
out="$("$BOOT_REPAIR" --nonsense 2>&1)"
|
||||
rc=$?
|
||||
assert_eq "unknown option exits 2" "$rc" "2"
|
||||
}
|
||||
@ -62,220 +62,9 @@ assert_eq() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Fixture construction
|
||||
# ----------------------------------------------------------------------------
|
||||
# shellcheck source=lib/boot_repair_cases.sh
|
||||
source "$SCRIPT_DIR/lib/boot_repair_cases.sh"
|
||||
|
||||
# make_root <name> -> prints the path to a fresh fixture root
|
||||
#
|
||||
# Builds a consistent system: one complete kernel tree, an ESP whose kernel
|
||||
# and initramfs match it, a gating fstab and serial downloads.
|
||||
make_root() {
|
||||
local name="$1"
|
||||
local r="$WORKROOT/$name"
|
||||
local kver="${2:-7.1.5-arch1-2}"
|
||||
|
||||
mkdir -p "$r/usr/lib/modules/$kver/kernel/fs/fat"
|
||||
mkdir -p "$r/boot" "$r/etc" "$r/var/cache/pacman/pkg"
|
||||
|
||||
# Fake kernel image carrying the version banner boot-repair greps for.
|
||||
printf 'Linux version %s (linux@archlinux) fake image\n' "$kver" \
|
||||
>"$r/usr/lib/modules/$kver/vmlinuz"
|
||||
: >"$r/usr/lib/modules/$kver/modules.dep"
|
||||
: >"$r/usr/lib/modules/$kver/kernel/fs/fat/vfat.ko.zst"
|
||||
|
||||
# ESP contents, consistent with the kernel tree.
|
||||
cp "$r/usr/lib/modules/$kver/vmlinuz" "$r/boot/vmlinuz-linux"
|
||||
printf 'usr/lib/modules/%s/kernel/fs/fat/vfat.ko.zst\n' "$kver" \
|
||||
>"$r/boot/initramfs-linux.img"
|
||||
mkdir -p "$r/boot/EFI" "$r/boot/loader"
|
||||
|
||||
printf 'UUID=DEAD-BEEF\t/boot\tvfat\tdefaults\t0 2\n' >"$r/etc/fstab"
|
||||
printf 'ParallelDownloads = 1\n' >"$r/etc/pacman.conf"
|
||||
|
||||
printf '%s' "$r"
|
||||
}
|
||||
|
||||
run_repair() {
|
||||
"$BOOT_REPAIR" --root "$1" "${@:2}" 2>&1
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Tests
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
test_healthy_system() {
|
||||
echo "TEST: healthy system reports consistent"
|
||||
local r out rc
|
||||
r="$(make_root healthy)"
|
||||
out="$(run_repair "$r" --dry-run)"
|
||||
rc=$?
|
||||
assert_eq "exit status is 0" "$rc" "0"
|
||||
assert_contains "reports consistent" "$out" "nothing to repair"
|
||||
assert_contains "picks the right kernel" "$out" "target kernel: 7.1.5-arch1-2"
|
||||
}
|
||||
|
||||
test_stale_esp_kernel_detected() {
|
||||
echo "TEST: stale ESP kernel is detected and repaired"
|
||||
local r out rc
|
||||
r="$(make_root stale)"
|
||||
# Simulate the real incident: ESP still holds the previous kernel.
|
||||
printf 'Linux version 7.0.5-arch1-1 (linux@archlinux) fake image\n' \
|
||||
>"$r/boot/vmlinuz-linux"
|
||||
|
||||
out="$(run_repair "$r" --dry-run)"
|
||||
rc=$?
|
||||
assert_eq "dry-run exits 1" "$rc" "1"
|
||||
assert_contains "detects the mismatch" "$out" "ESP kernel is 7.0.5-arch1-1 but installed tree is 7.1.5-arch1-2"
|
||||
assert_contains "dry-run changes nothing" "$out" "would install"
|
||||
|
||||
# The ESP must be untouched by a dry run.
|
||||
assert_contains "dry-run left ESP alone" "$(cat "$r/boot/vmlinuz-linux")" "7.0.5-arch1-1"
|
||||
|
||||
out="$(run_repair "$r")"
|
||||
assert_contains "repairs the kernel image" "$out" "installed kernel 7.1.5-arch1-2 to the ESP"
|
||||
assert_contains "ESP now has the new kernel" "$(cat "$r/boot/vmlinuz-linux")" "7.1.5-arch1-2"
|
||||
}
|
||||
|
||||
test_incomplete_tree_ignored() {
|
||||
echo "TEST: a gutted module tree is not chosen as the target"
|
||||
local r out
|
||||
r="$(make_root gutted)"
|
||||
# The exact wreckage the failed upgrade leaves: metadata only, no kernel/,
|
||||
# no vmlinuz. It sorts newer, so a naive 'newest' pick would take it.
|
||||
mkdir -p "$r/usr/lib/modules/9.9.9-arch1-1"
|
||||
: >"$r/usr/lib/modules/9.9.9-arch1-1/modules.dep"
|
||||
: >"$r/usr/lib/modules/9.9.9-arch1-1/modules.alias"
|
||||
|
||||
out="$(run_repair "$r" --dry-run)"
|
||||
assert_contains "ignores the incomplete tree" "$out" "target kernel: 7.1.5-arch1-2"
|
||||
assert_not_contains "does not pick 9.9.9" "$out" "target kernel: 9.9.9-arch1-1"
|
||||
}
|
||||
|
||||
test_picks_newest_complete() {
|
||||
echo "TEST: newest COMPLETE tree wins"
|
||||
local r out kver
|
||||
r="$(make_root multi)"
|
||||
kver="7.2.0-arch1-1"
|
||||
mkdir -p "$r/usr/lib/modules/$kver/kernel/fs/fat"
|
||||
printf 'Linux version %s (linux@archlinux) fake image\n' "$kver" \
|
||||
>"$r/usr/lib/modules/$kver/vmlinuz"
|
||||
: >"$r/usr/lib/modules/$kver/modules.dep"
|
||||
|
||||
out="$(run_repair "$r" --dry-run)"
|
||||
assert_contains "selects the newer complete tree" "$out" "target kernel: 7.2.0-arch1-1"
|
||||
}
|
||||
|
||||
test_fstab_nofail_fixed() {
|
||||
echo "TEST: noauto/nofail on /boot is detected and reset"
|
||||
local r out
|
||||
r="$(make_root fstab)"
|
||||
printf 'UUID=DEAD-BEEF\t/boot\tvfat\tdefaults,noauto,nofail\t0 2\n' >"$r/etc/fstab"
|
||||
|
||||
out="$(run_repair "$r" --dry-run)"
|
||||
assert_contains "detects the silent-failure options" "$out" "noauto/nofail"
|
||||
|
||||
out="$(run_repair "$r")"
|
||||
assert_contains "resets the options" "$out" "reset to 'defaults'"
|
||||
assert_contains "fstab now gates" "$(cat "$r/etc/fstab")" "defaults"
|
||||
assert_not_contains "nofail is gone" "$(cat "$r/etc/fstab")" "nofail"
|
||||
# A backup must exist so the change is reversible.
|
||||
if compgen -G "$r/etc/fstab.bak-*" >/dev/null; then
|
||||
pass "fstab backup written"
|
||||
else
|
||||
fail "fstab backup written"
|
||||
fi
|
||||
}
|
||||
|
||||
test_shadow_files_removed() {
|
||||
echo "TEST: shadow kernel files in an unmounted /boot are removed"
|
||||
local r out
|
||||
r="$(make_root shadow)"
|
||||
# An unmounted /boot has no EFI/ or loader/ — that is what makes the files
|
||||
# below shadows on the root filesystem rather than the real ESP.
|
||||
rm -rf "${r:?}/boot"
|
||||
mkdir -p "$r/boot"
|
||||
printf 'Linux version 7.1.5-arch1-2 (linux@archlinux) fake\n' >"$r/boot/vmlinuz-linux"
|
||||
printf 'usr/lib/modules/7.1.5-arch1-2/x\n' >"$r/boot/initramfs-linux.img"
|
||||
printf 'stale\n' >"$r/boot/initramfs-linux-fallback.img"
|
||||
|
||||
out="$(run_repair "$r")"
|
||||
assert_contains "detects the shadows" "$out" "orphaned kernel file"
|
||||
assert_contains "removes them" "$out" "removed 3 shadow file"
|
||||
# The fallback image is not regenerated by a --root run, so its absence
|
||||
# proves the deletion happened. (vmlinuz-linux legitimately reappears:
|
||||
# sync_kernel_image reinstalls the target kernel straight afterwards.)
|
||||
if [[ ! -f $r/boot/initramfs-linux-fallback.img ]]; then
|
||||
pass "shadow fallback image deleted"
|
||||
else
|
||||
fail "shadow fallback image deleted"
|
||||
fi
|
||||
}
|
||||
|
||||
test_shadow_guard_refuses_real_esp() {
|
||||
echo "TEST: SAFETY — never delete from a directory that looks like a real ESP"
|
||||
local r out
|
||||
r="$(make_root guarded)"
|
||||
# EFI/ and loader/ are present (make_root creates them), so even though the
|
||||
# fixture is "unmounted", boot-repair must refuse to delete anything.
|
||||
out="$(run_repair "$r")"
|
||||
assert_contains "refuses to delete" "$out" "refusing to delete anything"
|
||||
if [[ -f $r/boot/vmlinuz-linux ]]; then
|
||||
pass "real ESP kernel preserved"
|
||||
else
|
||||
fail "real ESP kernel preserved" "boot-repair deleted a real ESP kernel!"
|
||||
fi
|
||||
}
|
||||
|
||||
test_parallel_downloads_fixed() {
|
||||
echo "TEST: ParallelDownloads > 1 is detected and set to 1"
|
||||
local r out
|
||||
r="$(make_root paralleldl)"
|
||||
printf 'ParallelDownloads = 5\n' >"$r/etc/pacman.conf"
|
||||
|
||||
out="$(run_repair "$r" --dry-run)"
|
||||
assert_contains "detects the hook-segfault trigger" "$out" "ParallelDownloads = 5"
|
||||
|
||||
out="$(run_repair "$r")"
|
||||
assert_contains "sets it to 1" "$out" "set ParallelDownloads = 1"
|
||||
assert_contains "config updated" "$(cat "$r/etc/pacman.conf")" "ParallelDownloads = 1"
|
||||
}
|
||||
|
||||
test_missing_modules_dep() {
|
||||
echo "TEST: missing modules.dep is reported"
|
||||
local r out
|
||||
r="$(make_root nodep)"
|
||||
rm -f "$r/usr/lib/modules/7.1.5-arch1-2/modules.dep"
|
||||
|
||||
out="$(run_repair "$r" --dry-run)"
|
||||
assert_contains "detects missing modules.dep" "$out" "modules.dep missing for 7.1.5-arch1-2"
|
||||
}
|
||||
|
||||
test_no_kernel_at_all() {
|
||||
echo "TEST: no complete kernel tree gives actionable offline guidance"
|
||||
local r out rc
|
||||
r="$(make_root nokernel)"
|
||||
rm -rf "$r/usr/lib/modules"
|
||||
mkdir -p "$r/usr/lib/modules"
|
||||
|
||||
out="$(run_repair "$r" --dry-run)"
|
||||
rc=$?
|
||||
assert_eq "exits non-zero" "$rc" "1"
|
||||
assert_contains "explains the offline recovery" "$out" "pacman -U /var/cache/pacman/pkg/linux-"
|
||||
}
|
||||
|
||||
test_help_and_bad_args() {
|
||||
echo "TEST: CLI contract"
|
||||
local out rc
|
||||
out="$("$BOOT_REPAIR" --help 2>&1)"
|
||||
rc=$?
|
||||
assert_eq "--help exits 0" "$rc" "0"
|
||||
assert_contains "help mentions dry-run" "$out" "--dry-run"
|
||||
|
||||
out="$("$BOOT_REPAIR" --nonsense 2>&1)"
|
||||
rc=$?
|
||||
assert_eq "unknown option exits 2" "$rc" "2"
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Runner
|
||||
|
||||
187
linux_configuration/scripts/single_use/features/lib/pi_flash_exec.sh
Executable file
187
linux_configuration/scripts/single_use/features/lib/pi_flash_exec.sh
Executable file
@ -0,0 +1,187 @@
|
||||
#!/bin/bash
|
||||
# Flashing from the remote laptop.
|
||||
#
|
||||
# Sourced by raspberry_pi_flash_sd.sh; split out to keep it under the 250-line
|
||||
# cap. Sourced rather than run, so it inherits the caller's strict mode
|
||||
# and the variables defined above the source line.
|
||||
|
||||
# =============================================================================
|
||||
# Remote Flash
|
||||
# =============================================================================
|
||||
|
||||
phase_flash_remote() {
|
||||
log_info "=== Flash Raspberry Pi OS to SD Card on Remote Laptop ==="
|
||||
|
||||
discover_remote_laptop
|
||||
|
||||
setup_ssh_key_to_remote "$REMOTE_LAPTOP_IP" "$REMOTE_LAPTOP_USER"
|
||||
|
||||
local remote="${REMOTE_LAPTOP_USER}@${REMOTE_LAPTOP_IP}"
|
||||
|
||||
log_info "Checking for SD card on remote laptop..."
|
||||
echo "Block devices on ${remote}:"
|
||||
ssh "$remote" "lsblk -d -o NAME,SIZE,TYPE,RM,TRAN,MODEL" || true
|
||||
echo
|
||||
|
||||
log_info "Auto-detecting SD card on remote laptop..."
|
||||
local sd_device
|
||||
sd_device=$(ssh "$remote" "lsblk -d -o NAME,RM,TRAN | grep -E '1.*(usb|mmc)' | awk '{print \"/dev/\"\$1}' | head -1" 2> /dev/null || true)
|
||||
|
||||
if [[ -z $sd_device ]]; then
|
||||
die "No SD card detected on remote laptop. Please insert an SD card and try again."
|
||||
fi
|
||||
|
||||
local sd_info
|
||||
# shellcheck disable=SC2029 # Intentional client-side expansion
|
||||
sd_info=$(ssh "$remote" "lsblk -d -o NAME,SIZE,MODEL $sd_device 2>/dev/null | tail -1" || true)
|
||||
|
||||
log_success "Auto-detected SD card: $sd_device ($sd_info)"
|
||||
SD_CARD_DEVICE="$sd_device"
|
||||
|
||||
# shellcheck disable=SC2029 # Intentional client-side expansion
|
||||
if ! ssh "$remote" "[[ -b '$SD_CARD_DEVICE' ]]" 2> /dev/null; then
|
||||
die "Device $SD_CARD_DEVICE does not exist on remote laptop"
|
||||
fi
|
||||
|
||||
auto_generate_pi_password
|
||||
log_success "Pi user '$PI_USER' password: $PI_PASSWORD"
|
||||
|
||||
local encrypted_password
|
||||
encrypted_password=$(echo "$PI_PASSWORD" | openssl passwd -6 -stdin)
|
||||
|
||||
save_config
|
||||
|
||||
log_info "Copying script to remote laptop..."
|
||||
scp "$0" "${remote}:/tmp/raspberry_pi_flash_sd.sh"
|
||||
|
||||
log_info "Executing flash on remote laptop..."
|
||||
log_warning "This will ERASE ALL DATA on ${SD_CARD_DEVICE} on the remote laptop!"
|
||||
log_info "Proceeding automatically in 5 seconds... (Ctrl+C to cancel)"
|
||||
sleep 5
|
||||
|
||||
ssh -tt "$remote" "sudo SD_CARD_DEVICE='$SD_CARD_DEVICE' PI_USER='$PI_USER' PI_HOSTNAME='$PI_HOSTNAME' bash /tmp/raspberry_pi_flash_sd.sh execute-remote '$encrypted_password'"
|
||||
|
||||
log_success "Flash complete!"
|
||||
echo
|
||||
log_info "Pi credentials:"
|
||||
log_info " User: $PI_USER"
|
||||
log_info " Password: $PI_PASSWORD"
|
||||
log_info " Hostname: $PI_HOSTNAME"
|
||||
echo
|
||||
log_info "Next steps:"
|
||||
log_info "1. Remove SD card from the laptop and insert into Raspberry Pi"
|
||||
log_info "2. Connect the Pi to power and network"
|
||||
log_info "3. Wait 2-3 minutes for first boot"
|
||||
}
|
||||
|
||||
# Called on the remote laptop by phase_flash_remote
|
||||
phase_execute_remote() {
|
||||
check_root
|
||||
|
||||
local encrypted_password="${1:-}"
|
||||
|
||||
log_info "=== Executing Flash on Remote Laptop ==="
|
||||
|
||||
if [[ -z $SD_CARD_DEVICE ]]; then
|
||||
die "SD_CARD_DEVICE not set"
|
||||
fi
|
||||
|
||||
local image_path
|
||||
image_path=$(download_raspberry_pi_os)
|
||||
|
||||
log_info "Unmounting partitions on $SD_CARD_DEVICE..."
|
||||
for partition in "${SD_CARD_DEVICE}"*; do
|
||||
if mountpoint -q "$partition" 2> /dev/null || mount | grep -q "$partition"; then
|
||||
umount "$partition" 2> /dev/null || true
|
||||
fi
|
||||
done
|
||||
|
||||
log_info "Flashing image to SD card..."
|
||||
dd if="$image_path" of="$SD_CARD_DEVICE" bs=4M status=progress conv=fsync
|
||||
sync
|
||||
log_success "Image flashed successfully!"
|
||||
|
||||
log_info "Configuring headless boot..."
|
||||
sleep 2
|
||||
partprobe "$SD_CARD_DEVICE" 2> /dev/null || true
|
||||
sleep 2
|
||||
|
||||
local boot_partition
|
||||
if [[ -b "${SD_CARD_DEVICE}1" ]]; then
|
||||
boot_partition="${SD_CARD_DEVICE}1"
|
||||
elif [[ -b "${SD_CARD_DEVICE}p1" ]]; then
|
||||
boot_partition="${SD_CARD_DEVICE}p1"
|
||||
else
|
||||
die "Could not find boot partition"
|
||||
fi
|
||||
|
||||
local boot_mount="/tmp/rpi-boot"
|
||||
mkdir -p "$boot_mount"
|
||||
mount "$boot_partition" "$boot_mount"
|
||||
|
||||
touch "$boot_mount/ssh"
|
||||
log_success "SSH enabled"
|
||||
|
||||
if [[ -n $encrypted_password ]]; then
|
||||
echo "${PI_USER}:${encrypted_password}" > "$boot_mount/userconf.txt"
|
||||
log_success "User '$PI_USER' configured"
|
||||
fi
|
||||
|
||||
local root_partition
|
||||
if [[ -b "${SD_CARD_DEVICE}2" ]]; then
|
||||
root_partition="${SD_CARD_DEVICE}2"
|
||||
elif [[ -b "${SD_CARD_DEVICE}p2" ]]; then
|
||||
root_partition="${SD_CARD_DEVICE}p2"
|
||||
fi
|
||||
|
||||
if [[ -n $root_partition ]]; then
|
||||
local root_mount="/tmp/rpi-root"
|
||||
mkdir -p "$root_mount"
|
||||
mount "$root_partition" "$root_mount"
|
||||
|
||||
echo "$PI_HOSTNAME" > "$root_mount/etc/hostname"
|
||||
sed -i "s/raspberrypi/$PI_HOSTNAME/g" "$root_mount/etc/hosts"
|
||||
|
||||
log_success "Hostname set to '$PI_HOSTNAME'"
|
||||
|
||||
umount "$root_mount"
|
||||
fi
|
||||
|
||||
umount "$boot_mount"
|
||||
sync
|
||||
|
||||
log_success "SD card configured for headless boot!"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Main
|
||||
# =============================================================================
|
||||
|
||||
show_help() {
|
||||
cat << 'EOF'
|
||||
Raspberry Pi SD Card Flash Script
|
||||
|
||||
Usage: ./raspberry_pi_flash_sd.sh <command>
|
||||
|
||||
Commands:
|
||||
local Flash SD card locally (requires root)
|
||||
remote Flash SD card on a remote laptop via SSH
|
||||
execute-remote Internal: executed on remote laptop
|
||||
help Show this help message
|
||||
|
||||
The script will:
|
||||
1. Auto-discover a remote laptop with an SD card (for remote mode)
|
||||
2. Download Raspberry Pi OS Lite (64-bit)
|
||||
3. Flash the image to the SD card
|
||||
4. Configure headless boot (SSH enabled, user created, hostname set)
|
||||
|
||||
Credentials are auto-generated and saved to .raspberry_pi.conf
|
||||
|
||||
Examples:
|
||||
# Flash locally (run as root)
|
||||
sudo ./raspberry_pi_flash_sd.sh local
|
||||
|
||||
# Flash on remote laptop
|
||||
./raspberry_pi_flash_sd.sh remote
|
||||
EOF
|
||||
}
|
||||
193
linux_configuration/scripts/single_use/features/lib/pi_flash_image.sh
Executable file
193
linux_configuration/scripts/single_use/features/lib/pi_flash_image.sh
Executable file
@ -0,0 +1,193 @@
|
||||
#!/bin/bash
|
||||
# OS image download and the local flash phase.
|
||||
#
|
||||
# Sourced by raspberry_pi_flash_sd.sh; split out to keep it under the 250-line
|
||||
# cap. Sourced rather than run, so it inherits the caller's strict mode
|
||||
# and the variables defined above the source line.
|
||||
|
||||
# =============================================================================
|
||||
# Download and Flash Functions
|
||||
# =============================================================================
|
||||
|
||||
download_raspberry_pi_os() {
|
||||
local download_dir="/tmp/rpi-image"
|
||||
local image_url="https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-2024-11-19/2024-11-19-raspios-bookworm-arm64-lite.img.xz"
|
||||
local image_file="$download_dir/raspios.img.xz"
|
||||
local extracted_image="$download_dir/raspios.img"
|
||||
local expected_size=459000608
|
||||
|
||||
mkdir -p "$download_dir"
|
||||
|
||||
if [[ -f $extracted_image ]]; then
|
||||
log_info "Using existing image at $extracted_image"
|
||||
echo "$extracted_image"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -f $image_file ]]; then
|
||||
local actual_size
|
||||
actual_size=$(stat -c%s "$image_file" 2> /dev/null || stat -f%z "$image_file" 2> /dev/null || echo 0)
|
||||
if [[ $actual_size -lt $expected_size ]]; then
|
||||
log_warning "Incomplete download detected ($actual_size < $expected_size bytes), re-downloading..."
|
||||
rm -f "$image_file"
|
||||
else
|
||||
log_info "Image archive already downloaded"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ! -f $image_file ]]; then
|
||||
log_info "Downloading Raspberry Pi OS Lite (64-bit)..."
|
||||
log_info "This may take a while depending on your internet connection..."
|
||||
|
||||
if command -v aria2c &> /dev/null; then
|
||||
aria2c -x 4 -c -d "$download_dir" --out="raspios.img.xz" "$image_url" >&2
|
||||
elif command -v wget &> /dev/null; then
|
||||
wget --continue --show-progress -O "$image_file" "$image_url" >&2
|
||||
elif command -v curl &> /dev/null; then
|
||||
curl -L -C - -o "$image_file" "$image_url" --progress-bar >&2
|
||||
else
|
||||
die "No download tool available. Install wget, curl, or aria2c"
|
||||
fi
|
||||
|
||||
local actual_size
|
||||
actual_size=$(stat -c%s "$image_file" 2> /dev/null || stat -f%z "$image_file" 2> /dev/null || echo 0)
|
||||
if [[ $actual_size -lt $expected_size ]]; then
|
||||
die "Download incomplete: got $actual_size bytes, expected $expected_size"
|
||||
fi
|
||||
log_success "Download complete: $actual_size bytes"
|
||||
fi
|
||||
|
||||
log_info "Extracting image..."
|
||||
xz -dk "$image_file"
|
||||
|
||||
if [[ ! -f $extracted_image ]]; then
|
||||
die "Failed to extract image"
|
||||
fi
|
||||
|
||||
echo "$extracted_image"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Local Flash
|
||||
# =============================================================================
|
||||
|
||||
phase_flash_local() {
|
||||
check_root
|
||||
|
||||
log_info "=== Flash Raspberry Pi OS to SD Card (Local) ==="
|
||||
|
||||
# Detect SD card
|
||||
log_info "Detecting removable storage devices..."
|
||||
local devices
|
||||
devices=$(lsblk -d -o NAME,SIZE,TYPE,RM,TRAN | grep -E "disk.*1.*usb|disk.*1.*mmc" | awk '{print "/dev/"$1" ("$2")"}')
|
||||
|
||||
if [[ -z $devices ]]; then
|
||||
log_warning "No removable devices detected automatically."
|
||||
lsblk -d -o NAME,SIZE,TYPE,RM,TRAN
|
||||
read -r -p "Enter the SD card device path (e.g., /dev/sdb): " SD_CARD_DEVICE
|
||||
else
|
||||
echo "Detected removable devices:"
|
||||
echo "$devices"
|
||||
read -r -p "Enter the SD card device path from above (e.g., /dev/sdb): " SD_CARD_DEVICE
|
||||
fi
|
||||
|
||||
if [[ ! -b $SD_CARD_DEVICE ]]; then
|
||||
die "Device $SD_CARD_DEVICE does not exist or is not a block device"
|
||||
fi
|
||||
|
||||
local root_device
|
||||
root_device=$(findmnt -n -o SOURCE / | sed 's/[0-9]*$//' | sed 's/p[0-9]*$//')
|
||||
if [[ $SD_CARD_DEVICE == "$root_device" ]]; then
|
||||
die "Cannot flash to the system drive!"
|
||||
fi
|
||||
|
||||
auto_generate_pi_password
|
||||
|
||||
local encrypted_password
|
||||
encrypted_password=$(echo "$PI_PASSWORD" | openssl passwd -6 -stdin)
|
||||
|
||||
save_config
|
||||
|
||||
local image_path
|
||||
image_path=$(download_raspberry_pi_os)
|
||||
|
||||
log_warning "This will ERASE ALL DATA on $SD_CARD_DEVICE"
|
||||
read -r -p "Are you sure you want to continue? (yes/no): " confirm
|
||||
|
||||
if [[ $confirm != "yes" ]]; then
|
||||
die "Aborted by user"
|
||||
fi
|
||||
|
||||
log_info "Unmounting partitions on $SD_CARD_DEVICE..."
|
||||
for partition in "${SD_CARD_DEVICE}"*; do
|
||||
if mountpoint -q "$partition" 2> /dev/null || mount | grep -q "$partition"; then
|
||||
umount "$partition" 2> /dev/null || true
|
||||
fi
|
||||
done
|
||||
|
||||
log_info "Flashing image to SD card..."
|
||||
dd if="$image_path" of="$SD_CARD_DEVICE" bs=4M status=progress conv=fsync
|
||||
sync
|
||||
log_success "Image flashed successfully!"
|
||||
|
||||
# Configure headless boot
|
||||
log_info "Configuring headless boot..."
|
||||
sleep 2
|
||||
partprobe "$SD_CARD_DEVICE" 2> /dev/null || true
|
||||
sleep 2
|
||||
|
||||
local boot_partition
|
||||
if [[ -b "${SD_CARD_DEVICE}1" ]]; then
|
||||
boot_partition="${SD_CARD_DEVICE}1"
|
||||
elif [[ -b "${SD_CARD_DEVICE}p1" ]]; then
|
||||
boot_partition="${SD_CARD_DEVICE}p1"
|
||||
else
|
||||
die "Could not find boot partition"
|
||||
fi
|
||||
|
||||
local boot_mount="/tmp/rpi-boot"
|
||||
mkdir -p "$boot_mount"
|
||||
mount "$boot_partition" "$boot_mount"
|
||||
|
||||
touch "$boot_mount/ssh"
|
||||
log_success "SSH enabled"
|
||||
|
||||
echo "${PI_USER}:${encrypted_password}" > "$boot_mount/userconf.txt"
|
||||
log_success "User '$PI_USER' configured"
|
||||
|
||||
local root_partition
|
||||
if [[ -b "${SD_CARD_DEVICE}2" ]]; then
|
||||
root_partition="${SD_CARD_DEVICE}2"
|
||||
elif [[ -b "${SD_CARD_DEVICE}p2" ]]; then
|
||||
root_partition="${SD_CARD_DEVICE}p2"
|
||||
fi
|
||||
|
||||
if [[ -n $root_partition ]]; then
|
||||
local root_mount="/tmp/rpi-root"
|
||||
mkdir -p "$root_mount"
|
||||
mount "$root_partition" "$root_mount"
|
||||
|
||||
echo "$PI_HOSTNAME" > "$root_mount/etc/hostname"
|
||||
sed -i "s/raspberrypi/$PI_HOSTNAME/g" "$root_mount/etc/hosts"
|
||||
|
||||
log_success "Hostname set to '$PI_HOSTNAME'"
|
||||
|
||||
umount "$root_mount"
|
||||
fi
|
||||
|
||||
umount "$boot_mount"
|
||||
sync
|
||||
|
||||
log_success "SD card configured for headless boot!"
|
||||
log_success "Flash complete!"
|
||||
echo
|
||||
log_info "Pi credentials:"
|
||||
log_info " User: $PI_USER"
|
||||
log_info " Password: $PI_PASSWORD"
|
||||
log_info " Hostname: $PI_HOSTNAME"
|
||||
echo
|
||||
log_info "Next steps:"
|
||||
log_info "1. Remove SD card and insert into Raspberry Pi"
|
||||
log_info "2. Connect the Pi to power and network"
|
||||
log_info "3. Wait 2-3 minutes for first boot"
|
||||
}
|
||||
141
linux_configuration/scripts/single_use/features/lib/pi_flash_remote.sh
Executable file
141
linux_configuration/scripts/single_use/features/lib/pi_flash_remote.sh
Executable file
@ -0,0 +1,141 @@
|
||||
#!/bin/bash
|
||||
# Remote laptop discovery and SSH key setup.
|
||||
#
|
||||
# Sourced by raspberry_pi_flash_sd.sh; split out to keep it under the 250-line
|
||||
# cap. Sourced rather than run, so it inherits the caller's strict mode
|
||||
# and the variables defined above the source line.
|
||||
|
||||
discover_remote_laptop() {
|
||||
log_info "Auto-discovering remote laptop on local network..."
|
||||
|
||||
ensure_dependencies
|
||||
|
||||
local my_ip
|
||||
my_ip=$(ip -4 addr show | grep -oP '(?<=inet\s)(?!127\.)\d+(\.\d+){3}' | head -1)
|
||||
|
||||
local gateway
|
||||
gateway=$(ip route | grep default | awk '{print $3}' | head -1)
|
||||
local network="${gateway%.*}.0/24"
|
||||
|
||||
log_info "Local IP: $my_ip, Gateway: $gateway, Network: $network"
|
||||
log_info "Scanning network for SSH-enabled devices (using nmap)..."
|
||||
|
||||
local ssh_hosts
|
||||
nmap -sn -T4 "$network" &> /dev/null || true
|
||||
ssh_hosts=$(nmap -p 22 --open -sT -T4 "$network" 2> /dev/null | grep "Nmap scan report" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -vw "$my_ip" | sort -u)
|
||||
|
||||
if [[ -z $ssh_hosts ]]; then
|
||||
die "No SSH-enabled devices found on network"
|
||||
fi
|
||||
|
||||
local host_count
|
||||
host_count=$(echo "$ssh_hosts" | wc -l)
|
||||
log_info "Found $host_count SSH-enabled device(s): $(echo "$ssh_hosts" | tr '\n' ' ')"
|
||||
|
||||
local common_users=("$REMOTE_LAPTOP_USER" "kuchy" "kuhy" "$(whoami)" "pi" "user" "admin")
|
||||
local users=()
|
||||
for u in "${common_users[@]}"; do
|
||||
local is_dup=0
|
||||
for existing in "${users[@]}"; do
|
||||
if [[ $u == "$existing" ]]; then
|
||||
is_dup=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ $is_dup -eq 0 ]]; then
|
||||
users+=("$u")
|
||||
fi
|
||||
done
|
||||
|
||||
log_info "Will try usernames: ${users[*]}"
|
||||
|
||||
local found_laptop=""
|
||||
local found_user=""
|
||||
local idx=0
|
||||
|
||||
for ip in $ssh_hosts; do
|
||||
idx=$((idx + 1))
|
||||
|
||||
if [[ $ip == "$gateway" ]]; then
|
||||
log_info "[$idx/$host_count] Skipping $ip (gateway)"
|
||||
continue
|
||||
fi
|
||||
|
||||
log_info "[$idx/$host_count] $ip - Trying SSH key access with common usernames..."
|
||||
|
||||
for try_user in "${users[@]}"; do
|
||||
if ssh -o BatchMode=yes -o ConnectTimeout=2 -o StrictHostKeyChecking=accept-new "${try_user}@${ip}" "echo ok" 2> /dev/null | grep -q "ok"; then
|
||||
log_success "[$idx/$host_count] $ip - SSH key access confirmed with user '$try_user'!"
|
||||
found_user="$try_user"
|
||||
|
||||
log_info "[$idx/$host_count] $ip - Checking for SD card..."
|
||||
local has_sd
|
||||
has_sd=$(ssh -o BatchMode=yes -o ConnectTimeout=2 "${try_user}@${ip}" "lsblk -d -o NAME,RM,TRAN 2>/dev/null | grep -E '1.*(usb|mmc)' | head -1" 2> /dev/null || true)
|
||||
|
||||
if [[ -n $has_sd ]]; then
|
||||
log_success "[$idx/$host_count] $ip - Found SD card: $has_sd"
|
||||
found_laptop="$ip"
|
||||
break 2
|
||||
else
|
||||
log_warning "[$idx/$host_count] $ip - No SD card detected, saving as fallback..."
|
||||
if [[ -z $found_laptop ]]; then
|
||||
found_laptop="$ip"
|
||||
fi
|
||||
fi
|
||||
break
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
if [[ -z $found_laptop ]] || [[ -z $found_user ]]; then
|
||||
log_warning "No device with passwordless SSH found using common usernames."
|
||||
|
||||
found_laptop=$(echo "$ssh_hosts" | grep -vw "$gateway" | head -1)
|
||||
|
||||
if [[ -z $found_laptop ]]; then
|
||||
die "Could not find any suitable SSH-enabled device"
|
||||
fi
|
||||
|
||||
log_info "Found SSH host at $found_laptop but need credentials."
|
||||
read -r -p "Enter username for $found_laptop: " found_user
|
||||
|
||||
if [[ -z $found_user ]]; then
|
||||
die "No username provided"
|
||||
fi
|
||||
fi
|
||||
|
||||
REMOTE_LAPTOP_IP="$found_laptop"
|
||||
REMOTE_LAPTOP_USER="$found_user"
|
||||
log_success "Selected remote laptop: ${REMOTE_LAPTOP_USER}@${REMOTE_LAPTOP_IP}"
|
||||
|
||||
save_config
|
||||
}
|
||||
|
||||
setup_ssh_key_to_remote() {
|
||||
local remote_host="$1"
|
||||
local remote_user="$2"
|
||||
|
||||
if ssh -o BatchMode=yes -o ConnectTimeout=5 "${remote_user}@${remote_host}" "echo 'SSH key works'" 2> /dev/null; then
|
||||
log_success "SSH key authentication to ${remote_user}@${remote_host} already configured"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Setting up SSH key authentication to ${remote_user}@${remote_host}..."
|
||||
|
||||
if [[ ! -f ~/.ssh/id_rsa.pub ]] && [[ ! -f ~/.ssh/id_ed25519.pub ]]; then
|
||||
log_info "Generating SSH key..."
|
||||
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N "" -q
|
||||
fi
|
||||
|
||||
log_info "Copying SSH key to remote host (you may be prompted for password)..."
|
||||
|
||||
if command -v ssh-copy-id &> /dev/null; then
|
||||
ssh-copy-id -o StrictHostKeyChecking=no "${remote_user}@${remote_host}"
|
||||
else
|
||||
local pub_key
|
||||
pub_key=$(cat ~/.ssh/id_ed25519.pub 2> /dev/null || cat ~/.ssh/id_rsa.pub)
|
||||
ssh -o StrictHostKeyChecking=no "${remote_user}@${remote_host}" "mkdir -p ~/.ssh && echo '$pub_key' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
|
||||
fi
|
||||
|
||||
log_success "SSH key authentication configured"
|
||||
}
|
||||
@ -131,508 +131,13 @@ ensure_dependencies() {
|
||||
fi
|
||||
}
|
||||
|
||||
discover_remote_laptop() {
|
||||
log_info "Auto-discovering remote laptop on local network..."
|
||||
# shellcheck source=lib/pi_flash_remote.sh
|
||||
source "$SCRIPT_DIR/lib/pi_flash_remote.sh"
|
||||
# shellcheck source=lib/pi_flash_image.sh
|
||||
source "$SCRIPT_DIR/lib/pi_flash_image.sh"
|
||||
# shellcheck source=lib/pi_flash_exec.sh
|
||||
source "$SCRIPT_DIR/lib/pi_flash_exec.sh"
|
||||
|
||||
ensure_dependencies
|
||||
|
||||
local my_ip
|
||||
my_ip=$(ip -4 addr show | grep -oP '(?<=inet\s)(?!127\.)\d+(\.\d+){3}' | head -1)
|
||||
|
||||
local gateway
|
||||
gateway=$(ip route | grep default | awk '{print $3}' | head -1)
|
||||
local network="${gateway%.*}.0/24"
|
||||
|
||||
log_info "Local IP: $my_ip, Gateway: $gateway, Network: $network"
|
||||
log_info "Scanning network for SSH-enabled devices (using nmap)..."
|
||||
|
||||
local ssh_hosts
|
||||
nmap -sn -T4 "$network" &> /dev/null || true
|
||||
ssh_hosts=$(nmap -p 22 --open -sT -T4 "$network" 2> /dev/null | grep "Nmap scan report" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -vw "$my_ip" | sort -u)
|
||||
|
||||
if [[ -z $ssh_hosts ]]; then
|
||||
die "No SSH-enabled devices found on network"
|
||||
fi
|
||||
|
||||
local host_count
|
||||
host_count=$(echo "$ssh_hosts" | wc -l)
|
||||
log_info "Found $host_count SSH-enabled device(s): $(echo "$ssh_hosts" | tr '\n' ' ')"
|
||||
|
||||
local common_users=("$REMOTE_LAPTOP_USER" "kuchy" "kuhy" "$(whoami)" "pi" "user" "admin")
|
||||
local users=()
|
||||
for u in "${common_users[@]}"; do
|
||||
local is_dup=0
|
||||
for existing in "${users[@]}"; do
|
||||
if [[ $u == "$existing" ]]; then
|
||||
is_dup=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ $is_dup -eq 0 ]]; then
|
||||
users+=("$u")
|
||||
fi
|
||||
done
|
||||
|
||||
log_info "Will try usernames: ${users[*]}"
|
||||
|
||||
local found_laptop=""
|
||||
local found_user=""
|
||||
local idx=0
|
||||
|
||||
for ip in $ssh_hosts; do
|
||||
idx=$((idx + 1))
|
||||
|
||||
if [[ $ip == "$gateway" ]]; then
|
||||
log_info "[$idx/$host_count] Skipping $ip (gateway)"
|
||||
continue
|
||||
fi
|
||||
|
||||
log_info "[$idx/$host_count] $ip - Trying SSH key access with common usernames..."
|
||||
|
||||
for try_user in "${users[@]}"; do
|
||||
if ssh -o BatchMode=yes -o ConnectTimeout=2 -o StrictHostKeyChecking=accept-new "${try_user}@${ip}" "echo ok" 2> /dev/null | grep -q "ok"; then
|
||||
log_success "[$idx/$host_count] $ip - SSH key access confirmed with user '$try_user'!"
|
||||
found_user="$try_user"
|
||||
|
||||
log_info "[$idx/$host_count] $ip - Checking for SD card..."
|
||||
local has_sd
|
||||
has_sd=$(ssh -o BatchMode=yes -o ConnectTimeout=2 "${try_user}@${ip}" "lsblk -d -o NAME,RM,TRAN 2>/dev/null | grep -E '1.*(usb|mmc)' | head -1" 2> /dev/null || true)
|
||||
|
||||
if [[ -n $has_sd ]]; then
|
||||
log_success "[$idx/$host_count] $ip - Found SD card: $has_sd"
|
||||
found_laptop="$ip"
|
||||
break 2
|
||||
else
|
||||
log_warning "[$idx/$host_count] $ip - No SD card detected, saving as fallback..."
|
||||
if [[ -z $found_laptop ]]; then
|
||||
found_laptop="$ip"
|
||||
fi
|
||||
fi
|
||||
break
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
if [[ -z $found_laptop ]] || [[ -z $found_user ]]; then
|
||||
log_warning "No device with passwordless SSH found using common usernames."
|
||||
|
||||
found_laptop=$(echo "$ssh_hosts" | grep -vw "$gateway" | head -1)
|
||||
|
||||
if [[ -z $found_laptop ]]; then
|
||||
die "Could not find any suitable SSH-enabled device"
|
||||
fi
|
||||
|
||||
log_info "Found SSH host at $found_laptop but need credentials."
|
||||
read -r -p "Enter username for $found_laptop: " found_user
|
||||
|
||||
if [[ -z $found_user ]]; then
|
||||
die "No username provided"
|
||||
fi
|
||||
fi
|
||||
|
||||
REMOTE_LAPTOP_IP="$found_laptop"
|
||||
REMOTE_LAPTOP_USER="$found_user"
|
||||
log_success "Selected remote laptop: ${REMOTE_LAPTOP_USER}@${REMOTE_LAPTOP_IP}"
|
||||
|
||||
save_config
|
||||
}
|
||||
|
||||
setup_ssh_key_to_remote() {
|
||||
local remote_host="$1"
|
||||
local remote_user="$2"
|
||||
|
||||
if ssh -o BatchMode=yes -o ConnectTimeout=5 "${remote_user}@${remote_host}" "echo 'SSH key works'" 2> /dev/null; then
|
||||
log_success "SSH key authentication to ${remote_user}@${remote_host} already configured"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Setting up SSH key authentication to ${remote_user}@${remote_host}..."
|
||||
|
||||
if [[ ! -f ~/.ssh/id_rsa.pub ]] && [[ ! -f ~/.ssh/id_ed25519.pub ]]; then
|
||||
log_info "Generating SSH key..."
|
||||
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N "" -q
|
||||
fi
|
||||
|
||||
log_info "Copying SSH key to remote host (you may be prompted for password)..."
|
||||
|
||||
if command -v ssh-copy-id &> /dev/null; then
|
||||
ssh-copy-id -o StrictHostKeyChecking=no "${remote_user}@${remote_host}"
|
||||
else
|
||||
local pub_key
|
||||
pub_key=$(cat ~/.ssh/id_ed25519.pub 2> /dev/null || cat ~/.ssh/id_rsa.pub)
|
||||
ssh -o StrictHostKeyChecking=no "${remote_user}@${remote_host}" "mkdir -p ~/.ssh && echo '$pub_key' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
|
||||
fi
|
||||
|
||||
log_success "SSH key authentication configured"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Download and Flash Functions
|
||||
# =============================================================================
|
||||
|
||||
download_raspberry_pi_os() {
|
||||
local download_dir="/tmp/rpi-image"
|
||||
local image_url="https://downloads.raspberrypi.com/raspios_lite_arm64/images/raspios_lite_arm64-2024-11-19/2024-11-19-raspios-bookworm-arm64-lite.img.xz"
|
||||
local image_file="$download_dir/raspios.img.xz"
|
||||
local extracted_image="$download_dir/raspios.img"
|
||||
local expected_size=459000608
|
||||
|
||||
mkdir -p "$download_dir"
|
||||
|
||||
if [[ -f $extracted_image ]]; then
|
||||
log_info "Using existing image at $extracted_image"
|
||||
echo "$extracted_image"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -f $image_file ]]; then
|
||||
local actual_size
|
||||
actual_size=$(stat -c%s "$image_file" 2> /dev/null || stat -f%z "$image_file" 2> /dev/null || echo 0)
|
||||
if [[ $actual_size -lt $expected_size ]]; then
|
||||
log_warning "Incomplete download detected ($actual_size < $expected_size bytes), re-downloading..."
|
||||
rm -f "$image_file"
|
||||
else
|
||||
log_info "Image archive already downloaded"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ! -f $image_file ]]; then
|
||||
log_info "Downloading Raspberry Pi OS Lite (64-bit)..."
|
||||
log_info "This may take a while depending on your internet connection..."
|
||||
|
||||
if command -v aria2c &> /dev/null; then
|
||||
aria2c -x 4 -c -d "$download_dir" --out="raspios.img.xz" "$image_url" >&2
|
||||
elif command -v wget &> /dev/null; then
|
||||
wget --continue --show-progress -O "$image_file" "$image_url" >&2
|
||||
elif command -v curl &> /dev/null; then
|
||||
curl -L -C - -o "$image_file" "$image_url" --progress-bar >&2
|
||||
else
|
||||
die "No download tool available. Install wget, curl, or aria2c"
|
||||
fi
|
||||
|
||||
local actual_size
|
||||
actual_size=$(stat -c%s "$image_file" 2> /dev/null || stat -f%z "$image_file" 2> /dev/null || echo 0)
|
||||
if [[ $actual_size -lt $expected_size ]]; then
|
||||
die "Download incomplete: got $actual_size bytes, expected $expected_size"
|
||||
fi
|
||||
log_success "Download complete: $actual_size bytes"
|
||||
fi
|
||||
|
||||
log_info "Extracting image..."
|
||||
xz -dk "$image_file"
|
||||
|
||||
if [[ ! -f $extracted_image ]]; then
|
||||
die "Failed to extract image"
|
||||
fi
|
||||
|
||||
echo "$extracted_image"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Local Flash
|
||||
# =============================================================================
|
||||
|
||||
phase_flash_local() {
|
||||
check_root
|
||||
|
||||
log_info "=== Flash Raspberry Pi OS to SD Card (Local) ==="
|
||||
|
||||
# Detect SD card
|
||||
log_info "Detecting removable storage devices..."
|
||||
local devices
|
||||
devices=$(lsblk -d -o NAME,SIZE,TYPE,RM,TRAN | grep -E "disk.*1.*usb|disk.*1.*mmc" | awk '{print "/dev/"$1" ("$2")"}')
|
||||
|
||||
if [[ -z $devices ]]; then
|
||||
log_warning "No removable devices detected automatically."
|
||||
lsblk -d -o NAME,SIZE,TYPE,RM,TRAN
|
||||
read -r -p "Enter the SD card device path (e.g., /dev/sdb): " SD_CARD_DEVICE
|
||||
else
|
||||
echo "Detected removable devices:"
|
||||
echo "$devices"
|
||||
read -r -p "Enter the SD card device path from above (e.g., /dev/sdb): " SD_CARD_DEVICE
|
||||
fi
|
||||
|
||||
if [[ ! -b $SD_CARD_DEVICE ]]; then
|
||||
die "Device $SD_CARD_DEVICE does not exist or is not a block device"
|
||||
fi
|
||||
|
||||
local root_device
|
||||
root_device=$(findmnt -n -o SOURCE / | sed 's/[0-9]*$//' | sed 's/p[0-9]*$//')
|
||||
if [[ $SD_CARD_DEVICE == "$root_device" ]]; then
|
||||
die "Cannot flash to the system drive!"
|
||||
fi
|
||||
|
||||
auto_generate_pi_password
|
||||
|
||||
local encrypted_password
|
||||
encrypted_password=$(echo "$PI_PASSWORD" | openssl passwd -6 -stdin)
|
||||
|
||||
save_config
|
||||
|
||||
local image_path
|
||||
image_path=$(download_raspberry_pi_os)
|
||||
|
||||
log_warning "This will ERASE ALL DATA on $SD_CARD_DEVICE"
|
||||
read -r -p "Are you sure you want to continue? (yes/no): " confirm
|
||||
|
||||
if [[ $confirm != "yes" ]]; then
|
||||
die "Aborted by user"
|
||||
fi
|
||||
|
||||
log_info "Unmounting partitions on $SD_CARD_DEVICE..."
|
||||
for partition in "${SD_CARD_DEVICE}"*; do
|
||||
if mountpoint -q "$partition" 2> /dev/null || mount | grep -q "$partition"; then
|
||||
umount "$partition" 2> /dev/null || true
|
||||
fi
|
||||
done
|
||||
|
||||
log_info "Flashing image to SD card..."
|
||||
dd if="$image_path" of="$SD_CARD_DEVICE" bs=4M status=progress conv=fsync
|
||||
sync
|
||||
log_success "Image flashed successfully!"
|
||||
|
||||
# Configure headless boot
|
||||
log_info "Configuring headless boot..."
|
||||
sleep 2
|
||||
partprobe "$SD_CARD_DEVICE" 2> /dev/null || true
|
||||
sleep 2
|
||||
|
||||
local boot_partition
|
||||
if [[ -b "${SD_CARD_DEVICE}1" ]]; then
|
||||
boot_partition="${SD_CARD_DEVICE}1"
|
||||
elif [[ -b "${SD_CARD_DEVICE}p1" ]]; then
|
||||
boot_partition="${SD_CARD_DEVICE}p1"
|
||||
else
|
||||
die "Could not find boot partition"
|
||||
fi
|
||||
|
||||
local boot_mount="/tmp/rpi-boot"
|
||||
mkdir -p "$boot_mount"
|
||||
mount "$boot_partition" "$boot_mount"
|
||||
|
||||
touch "$boot_mount/ssh"
|
||||
log_success "SSH enabled"
|
||||
|
||||
echo "${PI_USER}:${encrypted_password}" > "$boot_mount/userconf.txt"
|
||||
log_success "User '$PI_USER' configured"
|
||||
|
||||
local root_partition
|
||||
if [[ -b "${SD_CARD_DEVICE}2" ]]; then
|
||||
root_partition="${SD_CARD_DEVICE}2"
|
||||
elif [[ -b "${SD_CARD_DEVICE}p2" ]]; then
|
||||
root_partition="${SD_CARD_DEVICE}p2"
|
||||
fi
|
||||
|
||||
if [[ -n $root_partition ]]; then
|
||||
local root_mount="/tmp/rpi-root"
|
||||
mkdir -p "$root_mount"
|
||||
mount "$root_partition" "$root_mount"
|
||||
|
||||
echo "$PI_HOSTNAME" > "$root_mount/etc/hostname"
|
||||
sed -i "s/raspberrypi/$PI_HOSTNAME/g" "$root_mount/etc/hosts"
|
||||
|
||||
log_success "Hostname set to '$PI_HOSTNAME'"
|
||||
|
||||
umount "$root_mount"
|
||||
fi
|
||||
|
||||
umount "$boot_mount"
|
||||
sync
|
||||
|
||||
log_success "SD card configured for headless boot!"
|
||||
log_success "Flash complete!"
|
||||
echo
|
||||
log_info "Pi credentials:"
|
||||
log_info " User: $PI_USER"
|
||||
log_info " Password: $PI_PASSWORD"
|
||||
log_info " Hostname: $PI_HOSTNAME"
|
||||
echo
|
||||
log_info "Next steps:"
|
||||
log_info "1. Remove SD card and insert into Raspberry Pi"
|
||||
log_info "2. Connect the Pi to power and network"
|
||||
log_info "3. Wait 2-3 minutes for first boot"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Remote Flash
|
||||
# =============================================================================
|
||||
|
||||
phase_flash_remote() {
|
||||
log_info "=== Flash Raspberry Pi OS to SD Card on Remote Laptop ==="
|
||||
|
||||
discover_remote_laptop
|
||||
|
||||
setup_ssh_key_to_remote "$REMOTE_LAPTOP_IP" "$REMOTE_LAPTOP_USER"
|
||||
|
||||
local remote="${REMOTE_LAPTOP_USER}@${REMOTE_LAPTOP_IP}"
|
||||
|
||||
log_info "Checking for SD card on remote laptop..."
|
||||
echo "Block devices on ${remote}:"
|
||||
ssh "$remote" "lsblk -d -o NAME,SIZE,TYPE,RM,TRAN,MODEL" || true
|
||||
echo
|
||||
|
||||
log_info "Auto-detecting SD card on remote laptop..."
|
||||
local sd_device
|
||||
sd_device=$(ssh "$remote" "lsblk -d -o NAME,RM,TRAN | grep -E '1.*(usb|mmc)' | awk '{print \"/dev/\"\$1}' | head -1" 2> /dev/null || true)
|
||||
|
||||
if [[ -z $sd_device ]]; then
|
||||
die "No SD card detected on remote laptop. Please insert an SD card and try again."
|
||||
fi
|
||||
|
||||
local sd_info
|
||||
# shellcheck disable=SC2029 # Intentional client-side expansion
|
||||
sd_info=$(ssh "$remote" "lsblk -d -o NAME,SIZE,MODEL $sd_device 2>/dev/null | tail -1" || true)
|
||||
|
||||
log_success "Auto-detected SD card: $sd_device ($sd_info)"
|
||||
SD_CARD_DEVICE="$sd_device"
|
||||
|
||||
# shellcheck disable=SC2029 # Intentional client-side expansion
|
||||
if ! ssh "$remote" "[[ -b '$SD_CARD_DEVICE' ]]" 2> /dev/null; then
|
||||
die "Device $SD_CARD_DEVICE does not exist on remote laptop"
|
||||
fi
|
||||
|
||||
auto_generate_pi_password
|
||||
log_success "Pi user '$PI_USER' password: $PI_PASSWORD"
|
||||
|
||||
local encrypted_password
|
||||
encrypted_password=$(echo "$PI_PASSWORD" | openssl passwd -6 -stdin)
|
||||
|
||||
save_config
|
||||
|
||||
log_info "Copying script to remote laptop..."
|
||||
scp "$0" "${remote}:/tmp/raspberry_pi_flash_sd.sh"
|
||||
|
||||
log_info "Executing flash on remote laptop..."
|
||||
log_warning "This will ERASE ALL DATA on ${SD_CARD_DEVICE} on the remote laptop!"
|
||||
log_info "Proceeding automatically in 5 seconds... (Ctrl+C to cancel)"
|
||||
sleep 5
|
||||
|
||||
ssh -tt "$remote" "sudo SD_CARD_DEVICE='$SD_CARD_DEVICE' PI_USER='$PI_USER' PI_HOSTNAME='$PI_HOSTNAME' bash /tmp/raspberry_pi_flash_sd.sh execute-remote '$encrypted_password'"
|
||||
|
||||
log_success "Flash complete!"
|
||||
echo
|
||||
log_info "Pi credentials:"
|
||||
log_info " User: $PI_USER"
|
||||
log_info " Password: $PI_PASSWORD"
|
||||
log_info " Hostname: $PI_HOSTNAME"
|
||||
echo
|
||||
log_info "Next steps:"
|
||||
log_info "1. Remove SD card from the laptop and insert into Raspberry Pi"
|
||||
log_info "2. Connect the Pi to power and network"
|
||||
log_info "3. Wait 2-3 minutes for first boot"
|
||||
}
|
||||
|
||||
# Called on the remote laptop by phase_flash_remote
|
||||
phase_execute_remote() {
|
||||
check_root
|
||||
|
||||
local encrypted_password="${1:-}"
|
||||
|
||||
log_info "=== Executing Flash on Remote Laptop ==="
|
||||
|
||||
if [[ -z $SD_CARD_DEVICE ]]; then
|
||||
die "SD_CARD_DEVICE not set"
|
||||
fi
|
||||
|
||||
local image_path
|
||||
image_path=$(download_raspberry_pi_os)
|
||||
|
||||
log_info "Unmounting partitions on $SD_CARD_DEVICE..."
|
||||
for partition in "${SD_CARD_DEVICE}"*; do
|
||||
if mountpoint -q "$partition" 2> /dev/null || mount | grep -q "$partition"; then
|
||||
umount "$partition" 2> /dev/null || true
|
||||
fi
|
||||
done
|
||||
|
||||
log_info "Flashing image to SD card..."
|
||||
dd if="$image_path" of="$SD_CARD_DEVICE" bs=4M status=progress conv=fsync
|
||||
sync
|
||||
log_success "Image flashed successfully!"
|
||||
|
||||
log_info "Configuring headless boot..."
|
||||
sleep 2
|
||||
partprobe "$SD_CARD_DEVICE" 2> /dev/null || true
|
||||
sleep 2
|
||||
|
||||
local boot_partition
|
||||
if [[ -b "${SD_CARD_DEVICE}1" ]]; then
|
||||
boot_partition="${SD_CARD_DEVICE}1"
|
||||
elif [[ -b "${SD_CARD_DEVICE}p1" ]]; then
|
||||
boot_partition="${SD_CARD_DEVICE}p1"
|
||||
else
|
||||
die "Could not find boot partition"
|
||||
fi
|
||||
|
||||
local boot_mount="/tmp/rpi-boot"
|
||||
mkdir -p "$boot_mount"
|
||||
mount "$boot_partition" "$boot_mount"
|
||||
|
||||
touch "$boot_mount/ssh"
|
||||
log_success "SSH enabled"
|
||||
|
||||
if [[ -n $encrypted_password ]]; then
|
||||
echo "${PI_USER}:${encrypted_password}" > "$boot_mount/userconf.txt"
|
||||
log_success "User '$PI_USER' configured"
|
||||
fi
|
||||
|
||||
local root_partition
|
||||
if [[ -b "${SD_CARD_DEVICE}2" ]]; then
|
||||
root_partition="${SD_CARD_DEVICE}2"
|
||||
elif [[ -b "${SD_CARD_DEVICE}p2" ]]; then
|
||||
root_partition="${SD_CARD_DEVICE}p2"
|
||||
fi
|
||||
|
||||
if [[ -n $root_partition ]]; then
|
||||
local root_mount="/tmp/rpi-root"
|
||||
mkdir -p "$root_mount"
|
||||
mount "$root_partition" "$root_mount"
|
||||
|
||||
echo "$PI_HOSTNAME" > "$root_mount/etc/hostname"
|
||||
sed -i "s/raspberrypi/$PI_HOSTNAME/g" "$root_mount/etc/hosts"
|
||||
|
||||
log_success "Hostname set to '$PI_HOSTNAME'"
|
||||
|
||||
umount "$root_mount"
|
||||
fi
|
||||
|
||||
umount "$boot_mount"
|
||||
sync
|
||||
|
||||
log_success "SD card configured for headless boot!"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Main
|
||||
# =============================================================================
|
||||
|
||||
show_help() {
|
||||
cat << 'EOF'
|
||||
Raspberry Pi SD Card Flash Script
|
||||
|
||||
Usage: ./raspberry_pi_flash_sd.sh <command>
|
||||
|
||||
Commands:
|
||||
local Flash SD card locally (requires root)
|
||||
remote Flash SD card on a remote laptop via SSH
|
||||
execute-remote Internal: executed on remote laptop
|
||||
help Show this help message
|
||||
|
||||
The script will:
|
||||
1. Auto-discover a remote laptop with an SD card (for remote mode)
|
||||
2. Download Raspberry Pi OS Lite (64-bit)
|
||||
3. Flash the image to the SD card
|
||||
4. Configure headless boot (SSH enabled, user created, hostname set)
|
||||
|
||||
Credentials are auto-generated and saved to .raspberry_pi.conf
|
||||
|
||||
Examples:
|
||||
# Flash locally (run as root)
|
||||
sudo ./raspberry_pi_flash_sd.sh local
|
||||
|
||||
# Flash on remote laptop
|
||||
./raspberry_pi_flash_sd.sh remote
|
||||
EOF
|
||||
}
|
||||
|
||||
main() {
|
||||
local command="${1:-help}"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user