Compare commits

...

2 Commits

Author SHA1 Message Date
juvdiaz 04d5b259c0 Update docker root dir in rpi 2026-07-02 09:02:52 -06:00
juvdiaz 06941a8454 Repair Pimox worker SSH readiness 2026-07-01 19:33:23 -06:00
9 changed files with 194 additions and 27 deletions

View File

@ -685,8 +685,8 @@ Deploy or refresh them from the Debian server with:
``` ```
`./jeannie up` also runs this step unless `./jeannie up` also runs this step unless
`LAB_RPI_SERVICES_DEPLOY=false` is set. The bootstrap prefers Docker data under `LAB_RPI_SERVICES_DEPLOY=false` is set. Docker data for the Raspberry Pi is
`/nvme-storage/docker` only when that path is an active writable mount point. If expected under `/nvme-storage/docker` when that path is an active writable mount point. If
the NVMe is missing or resets, it rewrites Docker's `data-root` to the NVMe is missing or resets, it rewrites Docker's `data-root` to
`/var/lib/docker` and restarts Docker so DNS can still come up from the `/var/lib/docker` and restarts Docker so DNS can still come up from the
SD/root filesystem. SD/root filesystem.
@ -706,7 +706,8 @@ Uptime Kuma monitor seeds live in
Kuma user exists. Kuma user exists.
`./jeannie status` and `./jeannie doctor-rpi` test Pi-hole DNS, `./jeannie status` and `./jeannie doctor-rpi` test Pi-hole DNS,
direct Unbound DNS, public fallback resolver readiness, Uptime Kuma HTTP, and direct Unbound DNS, public fallback resolver readiness, Uptime Kuma HTTP, and
whether Docker is currently using the NVMe path or `/var/lib/docker`. whether Docker is currently using `/nvme-storage/docker` or has fallen back to
`/var/lib/docker`.
## Adding Nodes ## Adding Nodes
@ -744,7 +745,7 @@ Worker NIC MAC addresses default to the VMID-derived deterministic format used
by the original working Pimox worker path. Set `LAB_PIMOX_WORKER_MAC_MODE=auto` by the original working Pimox worker path. Set `LAB_PIMOX_WORKER_MAC_MODE=auto`
only when you explicitly want Pimox to generate the MAC. only when you explicitly want Pimox to generate the MAC.
Worker creation fails faster than template creation: static guest-network Worker creation fails faster than template creation: static guest-network
configuration waits up to `LAB_PIMOX_GUEST_AGENT_CONFIG_TIMEOUT_SECONDS=120`, configuration waits up to `LAB_PIMOX_GUEST_AGENT_CONFIG_TIMEOUT_SECONDS=300`,
worker IP/SSH readiness waits up to `LAB_PIMOX_WORKER_WAIT_TIMEOUT_SECONDS=600`, worker IP/SSH readiness waits up to `LAB_PIMOX_WORKER_WAIT_TIMEOUT_SECONDS=600`,
and SSH after an IP appears waits up to `LAB_PIMOX_GUEST_SSH_TIMEOUT_SECONDS=600`. and SSH after an IP appears waits up to `LAB_PIMOX_GUEST_SSH_TIMEOUT_SECONDS=600`.
Static guest-network validation skips the extra LAN ICMP probe by default Static guest-network validation skips the extra LAN ICMP probe by default

View File

@ -685,8 +685,8 @@ Deploy or refresh them from the Debian server with:
``` ```
`./{{ main_script }} up` also runs this step unless `./{{ main_script }} up` also runs this step unless
`LAB_RPI_SERVICES_DEPLOY=false` is set. The bootstrap prefers Docker data under `LAB_RPI_SERVICES_DEPLOY=false` is set. Docker data for the Raspberry Pi is
`/nvme-storage/docker` only when that path is an active writable mount point. If expected under `/nvme-storage/docker` when that path is an active writable mount point. If
the NVMe is missing or resets, it rewrites Docker's `data-root` to the NVMe is missing or resets, it rewrites Docker's `data-root` to
`/var/lib/docker` and restarts Docker so DNS can still come up from the `/var/lib/docker` and restarts Docker so DNS can still come up from the
SD/root filesystem. SD/root filesystem.
@ -706,7 +706,8 @@ Uptime Kuma monitor seeds live in
Kuma user exists. Kuma user exists.
`./{{ main_script }} status` and `./{{ main_script }} doctor-rpi` test Pi-hole DNS, `./{{ main_script }} status` and `./{{ main_script }} doctor-rpi` test Pi-hole DNS,
direct Unbound DNS, public fallback resolver readiness, Uptime Kuma HTTP, and direct Unbound DNS, public fallback resolver readiness, Uptime Kuma HTTP, and
whether Docker is currently using the NVMe path or `/var/lib/docker`. whether Docker is currently using `/nvme-storage/docker` or has fallen back to
`/var/lib/docker`.
## Adding Nodes ## Adding Nodes
@ -744,7 +745,7 @@ Worker NIC MAC addresses default to the VMID-derived deterministic format used
by the original working Pimox worker path. Set `LAB_PIMOX_WORKER_MAC_MODE=auto` by the original working Pimox worker path. Set `LAB_PIMOX_WORKER_MAC_MODE=auto`
only when you explicitly want Pimox to generate the MAC. only when you explicitly want Pimox to generate the MAC.
Worker creation fails faster than template creation: static guest-network Worker creation fails faster than template creation: static guest-network
configuration waits up to `LAB_PIMOX_GUEST_AGENT_CONFIG_TIMEOUT_SECONDS=120`, configuration waits up to `LAB_PIMOX_GUEST_AGENT_CONFIG_TIMEOUT_SECONDS=300`,
worker IP/SSH readiness waits up to `LAB_PIMOX_WORKER_WAIT_TIMEOUT_SECONDS=600`, worker IP/SSH readiness waits up to `LAB_PIMOX_WORKER_WAIT_TIMEOUT_SECONDS=600`,
and SSH after an IP appears waits up to `LAB_PIMOX_GUEST_SSH_TIMEOUT_SECONDS=600`. and SSH after an IP appears waits up to `LAB_PIMOX_GUEST_SSH_TIMEOUT_SECONDS=600`.
Static guest-network validation skips the extra LAN ICMP probe by default Static guest-network validation skips the extra LAN ICMP probe by default

View File

@ -160,7 +160,7 @@ Worker NIC MAC addresses default to the VMID-derived deterministic format used
by the original working Pimox worker path. Set `LAB_PIMOX_WORKER_MAC_MODE=auto` by the original working Pimox worker path. Set `LAB_PIMOX_WORKER_MAC_MODE=auto`
only when Pimox-generated MAC addresses are preferred. only when Pimox-generated MAC addresses are preferred.
Worker clone failures are intentionally faster than template build failures: Worker clone failures are intentionally faster than template build failures:
`LAB_PIMOX_GUEST_AGENT_CONFIG_TIMEOUT_SECONDS=120`, `LAB_PIMOX_GUEST_AGENT_CONFIG_TIMEOUT_SECONDS=300`,
`LAB_PIMOX_WORKER_WAIT_TIMEOUT_SECONDS=600`, and `LAB_PIMOX_WORKER_WAIT_TIMEOUT_SECONDS=600`, and
`LAB_PIMOX_GUEST_SSH_TIMEOUT_SECONDS=600` by default. The broader `LAB_PIMOX_GUEST_SSH_TIMEOUT_SECONDS=600` by default. The broader
`LAB_PIMOX_GUEST_TIMEOUT_SECONDS` still controls template preparation. `LAB_PIMOX_GUEST_TIMEOUT_SECONDS` still controls template preparation.

View File

@ -47,6 +47,7 @@ hosts:
user: jv user: jv
lan_ip: 192.168.100.89 lan_ip: 192.168.100.89
tailscale_ip: 100.77.80.72 tailscale_ip: 100.77.80.72
docker_root: /nvme-storage/docker
docker_nvme_root: /nvme-storage/docker docker_nvme_root: /nvme-storage/docker
docker_fallback_root: /var/lib/docker docker_fallback_root: /var/lib/docker
opi5_pimox: opi5_pimox:

View File

@ -16,10 +16,11 @@ For Pi-hole v6, the stack sets `FTLCONF_dns_interface=ALL` and
`PIHOLE_DNS_LISTENING_MODE` so LAN clients such as the Debian control host are `PIHOLE_DNS_LISTENING_MODE` so LAN clients such as the Debian control host are
not rejected as non-local networks. not rejected as non-local networks.
The bootstrap script prefers Docker data under `/nvme-storage/docker` only when Docker data for the Raspberry Pi is expected under `/nvme-storage/docker` when
that path is an active mount point and writable. If the NVMe is missing or that path is an active mount point and writable. If the NVMe is missing or
reset, it rewrites Docker's `data-root` to `/var/lib/docker` and restarts reset, the bootstrap rewrites Docker's `data-root` to `/var/lib/docker` and
Docker, allowing the services to come up from the SD/root filesystem instead. restarts Docker, allowing the services to come up from the SD/root filesystem
instead.
Service config lives under `/opt/homelab-rpi-services/data` by default. Backup Service config lives under `/opt/homelab-rpi-services/data` by default. Backup
storage is intentionally not placed on the NVMe until the disk has proven storage is intentionally not placed on the NVMe until the disk has proven

View File

@ -4,7 +4,7 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
INSTALL_DIR="${LAB_RPI_SERVICES_INSTALL_DIR:-/opt/homelab-rpi-services}" INSTALL_DIR="${LAB_RPI_SERVICES_INSTALL_DIR:-/opt/homelab-rpi-services}"
DATA_DIR="${LAB_RPI_SERVICES_DATA_DIR:-${INSTALL_DIR}/data}" DATA_DIR="${LAB_RPI_SERVICES_DATA_DIR:-${INSTALL_DIR}/data}"
DOCKER_NVME_ROOT="${LAB_RPI_DOCKER_NVME_ROOT:-/nvme-storage/docker}" DOCKER_NVME_ROOT="${LAB_RPI_DOCKER_ROOT:-${LAB_RPI_DOCKER_NVME_ROOT:-/nvme-storage/docker}}"
DOCKER_FALLBACK_ROOT="${LAB_RPI_DOCKER_FALLBACK_ROOT:-/var/lib/docker}" DOCKER_FALLBACK_ROOT="${LAB_RPI_DOCKER_FALLBACK_ROOT:-/var/lib/docker}"
STOP_LEGACY_PIHOLE="${LAB_RPI_STOP_LEGACY_PIHOLE:-true}" STOP_LEGACY_PIHOLE="${LAB_RPI_STOP_LEGACY_PIHOLE:-true}"
PIHOLE_WEBPASSWORD="${PIHOLE_WEBPASSWORD:-}" PIHOLE_WEBPASSWORD="${PIHOLE_WEBPASSWORD:-}"

170
jeannie
View File

@ -52,6 +52,7 @@ mapping = {
"hosts.rpi4.user": "LAB_RPI_USER", "hosts.rpi4.user": "LAB_RPI_USER",
"hosts.rpi4.lan_ip": "LAB_RPI_HOST", "hosts.rpi4.lan_ip": "LAB_RPI_HOST",
"hosts.rpi4.tailscale_ip": "LAB_RPI_TAILSCALE_IP", "hosts.rpi4.tailscale_ip": "LAB_RPI_TAILSCALE_IP",
"hosts.rpi4.docker_root": "LAB_RPI_DOCKER_ROOT",
"hosts.rpi4.docker_nvme_root": "LAB_RPI_DOCKER_NVME_ROOT", "hosts.rpi4.docker_nvme_root": "LAB_RPI_DOCKER_NVME_ROOT",
"hosts.rpi4.docker_fallback_root": "LAB_RPI_DOCKER_FALLBACK_ROOT", "hosts.rpi4.docker_fallback_root": "LAB_RPI_DOCKER_FALLBACK_ROOT",
"hosts.opi5_pimox.user": "LAB_PIMOX_USER", "hosts.opi5_pimox.user": "LAB_PIMOX_USER",
@ -1076,7 +1077,40 @@ sudo '${qm_bin}' status '${vmid}'
echo 'Pimox VM ${vmid} config summary:' echo 'Pimox VM ${vmid} config summary:'
sudo '${qm_bin}' config '${vmid}' | grep -E '^(agent|boot|net0|scsi0|virtio0|sata0|ide0|ide2|efidisk0):' || true sudo '${qm_bin}' config '${vmid}' | grep -E '^(agent|boot|net0|scsi0|virtio0|sata0|ide0|ide2|efidisk0):' || true
echo 'Pimox VM ${vmid} guest-agent network-get-interfaces:' echo 'Pimox VM ${vmid} guest-agent network-get-interfaces:'
sudo '${qm_bin}' guest cmd '${vmid}' network-get-interfaces" >&2 || true sudo '${qm_bin}' guest cmd '${vmid}' network-get-interfaces || true
echo 'Pimox VM ${vmid} guest SSH diagnostics:'
sudo '${qm_bin}' guest exec '${vmid}' -- bash -lc 'set +e
echo "-- units --"
systemctl is-active ssh sshd 2>/dev/null || true
echo "-- listeners --"
ss -ltnp 2>/dev/null | grep :22 || ss -ltn 2>/dev/null | grep :22 || true
echo "-- sshd config test --"
sudo sshd -t 2>&1 || true
echo "-- recent ssh logs --"
journalctl -u ssh -u sshd --no-pager -n 40 2>/dev/null || true
' || true" >&2 || true
}
pimox_worker_guest_agent_recovery_hint() {
local host="$1"
local user="$2"
local vmid="$3"
local qm_bin="$4"
cat >&2 <<EOF
QEMU guest agent did not become available for Pimox VM ${vmid}.
Fast checks:
ssh ${user}@${host} 'sudo ${qm_bin} status ${vmid}; sudo ${qm_bin} config ${vmid}; sudo ${qm_bin} guest cmd ${vmid} network-get-interfaces'
Recovery options:
1. If the clone is still booting slowly, rerun the Pimox stage with a longer agent wait:
LAB_RPI_SERVICES_DEPLOY=false LAB_PIMOX_GUEST_AGENT_CONFIG_TIMEOUT_SECONDS=600 ./jeannie up
2. If the same VM still reports "QEMU guest agent is not running", recreate the worker clone:
LAB_RPI_SERVICES_DEPLOY=false LAB_PIMOX_WORKER_REPLACE_EXISTING=true ./jeannie up
3. If a recreated worker still has no guest agent, rebuild the template and worker:
LAB_RPI_SERVICES_DEPLOY=false LAB_PIMOX_TEMPLATE_REPLACE_EXISTING=true LAB_PIMOX_WORKER_REPLACE_EXISTING=true ./jeannie up
EOF
} }
pimox_guest_exec_exitcode() { pimox_guest_exec_exitcode() {
@ -1099,6 +1133,73 @@ except Exception:
PY PY
} }
pimox_worker_guest_ssh_repair() {
local host="$1"
local user="$2"
local key_path="$3"
local vmid="$4"
local qm_bin="$5"
local script
local encoded_script
local repair_output
local repair_exitcode
script="$(cat <<'EOF'
set -eu
ssh_unit=""
if systemctl list-unit-files ssh.service 2>/dev/null | grep -q '^ssh[.]service'; then
ssh_unit=ssh.service
elif systemctl list-unit-files sshd.service 2>/dev/null | grep -q '^sshd[.]service'; then
ssh_unit=sshd.service
else
echo "Neither ssh.service nor sshd.service exists in the guest" >&2
exit 1
fi
sudo ssh-keygen -A
sudo install -d -m 0755 /run/sshd
sudo mkdir -p /etc/ssh/sshd_config.d
sudo tee /etc/ssh/sshd_config.d/99-homelab-worker-listen.conf >/dev/null <<'SSHD_CONFIG'
Port 22
ListenAddress 0.0.0.0
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
UsePAM yes
SSHD_CONFIG
sudo sshd -t
sudo systemctl unmask ssh.service ssh.socket sshd.service sshd.socket >/dev/null 2>&1 || true
sudo systemctl disable --now ssh.socket sshd.socket >/dev/null 2>&1 || true
sudo systemctl enable "$ssh_unit" >/dev/null
sudo systemctl restart "$ssh_unit"
sleep 2
sudo systemctl is-active "$ssh_unit"
if command -v ss >/dev/null 2>&1; then
ss -ltnp 2>/dev/null || ss -ltn
ss -ltn | awk '$4 == "0.0.0.0:22" { found = 1 } END { exit found ? 0 : 1 }'
fi
EOF
)"
encoded_script="$(printf '%s' "${script}" | base64 | tr -d '\n')"
if ! repair_output="$(pimox_ssh "${host}" "${user}" "${key_path}" "sudo '${qm_bin}' guest exec '${vmid}' -- bash -lc \"printf '%s' '${encoded_script}' | base64 -d | sudo bash\"" 2>&1)"; then
echo "Could not run guest SSH repair through qemu-guest-agent for VM ${vmid}." >&2
printf '%s\n' "${repair_output}" | sed 's/^/ guest-ssh-repair: /' >&2
return 1
fi
printf '%s\n' "${repair_output}" | sed 's/^/ guest-ssh-repair: /' >&2
if repair_exitcode="$(pimox_guest_exec_exitcode "${repair_output}")" && [[ "${repair_exitcode}" == "0" ]]; then
return 0
fi
echo "Guest SSH repair failed inside Pimox VM ${vmid}." >&2
if [[ -n "${repair_exitcode:-}" ]]; then
echo "Guest exit code: ${repair_exitcode}" >&2
fi
return 1
}
wait_for_pimox_guest_ssh() { wait_for_pimox_guest_ssh() {
local host="$1" local host="$1"
local user="$2" local user="$2"
@ -1124,6 +1225,9 @@ wait_for_pimox_guest_ssh() {
local ssh_deadline=0 local ssh_deadline=0
local ssh_output local ssh_output
local ssh_timeout_seconds="${LAB_PIMOX_GUEST_SSH_TIMEOUT_SECONDS:-600}" local ssh_timeout_seconds="${LAB_PIMOX_GUEST_SSH_TIMEOUT_SECONDS:-600}"
local ssh_refused_since=0
local ssh_refused_timeout_seconds="${LAB_PIMOX_GUEST_SSH_REFUSED_TIMEOUT_SECONDS:-120}"
local ssh_repair_attempted=false
ip_filter_description="matching prefix ${ip_prefix}" ip_filter_description="matching prefix ${ip_prefix}"
if [[ -z "${ip_prefix}" ]]; then if [[ -z "${ip_prefix}" ]]; then
@ -1133,6 +1237,10 @@ wait_for_pimox_guest_ssh() {
echo "LAB_PIMOX_GUEST_SSH_TIMEOUT_SECONDS must be a positive integer." >&2 echo "LAB_PIMOX_GUEST_SSH_TIMEOUT_SECONDS must be a positive integer." >&2
return 1 return 1
fi fi
if ! [[ "${ssh_refused_timeout_seconds}" =~ ^[0-9]+$ ]] || ((ssh_refused_timeout_seconds == 0)); then
echo "LAB_PIMOX_GUEST_SSH_REFUSED_TIMEOUT_SECONDS must be a positive integer." >&2
return 1
fi
if ! truthy "${require_host_probe}" && ! disabled_value "${require_host_probe}"; then if ! truthy "${require_host_probe}" && ! disabled_value "${require_host_probe}"; then
echo "LAB_PIMOX_WORKER_REQUIRE_HOST_PROBE must be true or false." >&2 echo "LAB_PIMOX_WORKER_REQUIRE_HOST_PROBE must be true or false." >&2
return 1 return 1
@ -1181,6 +1289,23 @@ wait_for_pimox_guest_ssh() {
return 0 return 0
fi fi
last_ssh_output="${ssh_output}" last_ssh_output="${ssh_output}"
if [[ "${ssh_output}" == *"Connection refused"* ]]; then
if ((ssh_refused_since == 0)); then
ssh_refused_since="${SECONDS}"
fi
if [[ "${ssh_repair_attempted}" == "false" ]]; then
echo "SSH on worker VM ${vmid} at ${guest_ip} is refusing connections; attempting qemu-guest-agent SSH repair..." >&2
pimox_worker_guest_ssh_repair "${host}" "${user}" "${key_path}" "${vmid}" "${qm_bin}" || true
ssh_repair_attempted=true
elif ((SECONDS - ssh_refused_since >= ssh_refused_timeout_seconds)); then
echo "Worker VM ${vmid} at ${guest_ip} is reachable but still refuses SSH after ${ssh_refused_timeout_seconds}s." >&2
echo "This usually means sshd is missing, broken, or not listening on IPv4 inside the worker template." >&2
pimox_worker_vm_debug "${host}" "${user}" "${key_path}" "${vmid}" "${qm_bin}"
return 1
fi
else
ssh_refused_since=0
fi
fi fi
if ((SECONDS >= next_log)); then if ((SECONDS >= next_log)); then
@ -1278,7 +1403,7 @@ configure_pimox_worker_guest_network() {
local gateway="$7" local gateway="$7"
local dns_servers="$8" local dns_servers="$8"
local interface_name="$9" local interface_name="$9"
local timeout_seconds="${LAB_PIMOX_GUEST_AGENT_CONFIG_TIMEOUT_SECONDS:-120}" local timeout_seconds="${LAB_PIMOX_GUEST_AGENT_CONFIG_TIMEOUT_SECONDS:-300}"
local deadline local deadline
local script local script
local encoded_script local encoded_script
@ -1360,15 +1485,36 @@ sudo ip addr add ${worker_ip} dev ${interface_name}
sudo ip route replace default via ${gateway} dev ${interface_name} sudo ip route replace default via ${gateway} dev ${interface_name}
printf '%s\n' ${dns_servers} | awk '{ for (i = 1; i <= NF; i++) print "nameserver " \$i }' | sudo tee /etc/resolv.conf >/dev/null printf '%s\n' ${dns_servers} | awk '{ for (i = 1; i <= NF; i++) print "nameserver " \$i }' | sudo tee /etc/resolv.conf >/dev/null
sudo ifup --force ${interface_name} 2>/dev/null || true sudo ifup --force ${interface_name} 2>/dev/null || true
sudo systemctl restart ssh 2>/dev/null || true sudo ssh-keygen -A
sudo install -d -m 0755 /run/sshd
sudo mkdir -p /etc/ssh/sshd_config.d
sudo tee /etc/ssh/sshd_config.d/99-homelab-worker-listen.conf >/dev/null <<'SSHD_CONFIG'
Port 22
ListenAddress 0.0.0.0
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
UsePAM yes
SSHD_CONFIG
sudo sshd -t
sudo systemctl unmask ssh.service ssh.socket sshd.service sshd.socket >/dev/null 2>&1 || true
sudo systemctl disable --now ssh.socket sshd.socket >/dev/null 2>&1 || true
ssh_unit=ssh.service
if ! systemctl list-unit-files ssh.service 2>/dev/null | grep -q '^ssh[.]service'; then
ssh_unit=sshd.service
fi
sudo systemctl enable "\$ssh_unit" >/dev/null
sudo systemctl restart "\$ssh_unit"
ip -br addr show ${interface_name} ip -br addr show ${interface_name}
ip route ip route
ip -4 addr show dev ${interface_name} | grep -q '${worker_address}/' ip -4 addr show dev ${interface_name} | grep -q '${worker_address}/'
${lan_probe_block} ${lan_probe_block}
echo 'guest-check: ssh service' echo 'guest-check: ssh service'
sudo systemctl is-active ssh sudo systemctl is-active "\$ssh_unit"
if command -v ss >/dev/null 2>&1; then if command -v ss >/dev/null 2>&1; then
ss -ltn | grep -Eq '(^|[[:space:]])0\.0\.0\.0:22[[:space:]]|(^|[[:space:]])\[::\]:22[[:space:]]|(^|[[:space:]])\*:22[[:space:]]' ss -ltnp 2>/dev/null || ss -ltn
ss -ltn | awk '\$4 == "0.0.0.0:22" { found = 1 } END { exit found ? 0 : 1 }'
fi fi
EOF EOF
)" )"
@ -1394,6 +1540,7 @@ EOF
if [[ -n "${network_output:-}" ]]; then if [[ -n "${network_output:-}" ]]; then
printf '%s\n' "${network_output}" | sed 's/^/ /' >&2 printf '%s\n' "${network_output}" | sed 's/^/ /' >&2
fi fi
pimox_worker_guest_agent_recovery_hint "${pimox_host}" "${pimox_user}" "${vmid}" "${qm_bin}"
return 1 return 1
} }
@ -1974,7 +2121,7 @@ check_rpi_docker_root_state() {
local rpi_host="${LAB_RPI_HOST:-${LAB_RASPBERRY_HOST:-192.168.100.89}}" local rpi_host="${LAB_RPI_HOST:-${LAB_RASPBERRY_HOST:-192.168.100.89}}"
local rpi_user="${LAB_RPI_USER:-${LAB_RASPBERRY_USER:-jv}}" local rpi_user="${LAB_RPI_USER:-${LAB_RASPBERRY_USER:-jv}}"
local rpi_key="${LAB_RPI_SSH_KEY_PATH:-${LAB_RASPBERRY_SSH_KEY_PATH:-/home/jv/.ssh/id_ed25519}}" local rpi_key="${LAB_RPI_SSH_KEY_PATH:-${LAB_RASPBERRY_SSH_KEY_PATH:-/home/jv/.ssh/id_ed25519}}"
local nvme_root="${LAB_RPI_DOCKER_NVME_ROOT:-/nvme-storage/docker}" local nvme_root="${LAB_RPI_DOCKER_ROOT:-${LAB_RPI_DOCKER_NVME_ROOT:-/nvme-storage/docker}}"
local fallback_root="${LAB_RPI_DOCKER_FALLBACK_ROOT:-/var/lib/docker}" local fallback_root="${LAB_RPI_DOCKER_FALLBACK_ROOT:-/var/lib/docker}"
ssh -i "${rpi_key}" -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${rpi_user}@${rpi_host}" "set -eu ssh -i "${rpi_key}" -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${rpi_user}@${rpi_host}" "set -eu
@ -3538,7 +3685,7 @@ deploy_rpi_services() {
local rpi_key="${LAB_RPI_SSH_KEY_PATH:-${LAB_RASPBERRY_SSH_KEY_PATH:-/home/jv/.ssh/id_ed25519}}" local rpi_key="${LAB_RPI_SSH_KEY_PATH:-${LAB_RASPBERRY_SSH_KEY_PATH:-/home/jv/.ssh/id_ed25519}}"
local install_dir="${LAB_RPI_SERVICES_INSTALL_DIR:-/opt/homelab-rpi-services}" local install_dir="${LAB_RPI_SERVICES_INSTALL_DIR:-/opt/homelab-rpi-services}"
local data_dir="${LAB_RPI_SERVICES_DATA_DIR:-${install_dir}/data}" local data_dir="${LAB_RPI_SERVICES_DATA_DIR:-${install_dir}/data}"
local docker_nvme_root="${LAB_RPI_DOCKER_NVME_ROOT:-/nvme-storage/docker}" local docker_nvme_root="${LAB_RPI_DOCKER_ROOT:-${LAB_RPI_DOCKER_NVME_ROOT:-/nvme-storage/docker}}"
local docker_fallback_root="${LAB_RPI_DOCKER_FALLBACK_ROOT:-/var/lib/docker}" local docker_fallback_root="${LAB_RPI_DOCKER_FALLBACK_ROOT:-/var/lib/docker}"
local stop_legacy_pihole="${LAB_RPI_STOP_LEGACY_PIHOLE:-true}" local stop_legacy_pihole="${LAB_RPI_STOP_LEGACY_PIHOLE:-true}"
local install_docker="${LAB_RPI_INSTALL_DOCKER:-false}" local install_docker="${LAB_RPI_INSTALL_DOCKER:-false}"
@ -3583,6 +3730,7 @@ deploy_rpi_services() {
ssh -i "${rpi_key}" -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${rpi_user}@${rpi_host}" " ssh -i "${rpi_key}" -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${rpi_user}@${rpi_host}" "
LAB_RPI_SERVICES_INSTALL_DIR='${install_dir}' \ LAB_RPI_SERVICES_INSTALL_DIR='${install_dir}' \
LAB_RPI_SERVICES_DATA_DIR='${data_dir}' \ LAB_RPI_SERVICES_DATA_DIR='${data_dir}' \
LAB_RPI_DOCKER_ROOT='${docker_nvme_root}' \
LAB_RPI_DOCKER_NVME_ROOT='${docker_nvme_root}' \ LAB_RPI_DOCKER_NVME_ROOT='${docker_nvme_root}' \
LAB_RPI_DOCKER_FALLBACK_ROOT='${docker_fallback_root}' \ LAB_RPI_DOCKER_FALLBACK_ROOT='${docker_fallback_root}' \
LAB_RPI_STOP_LEGACY_PIHOLE='${stop_legacy_pihole}' \ LAB_RPI_STOP_LEGACY_PIHOLE='${stop_legacy_pihole}' \
@ -6024,7 +6172,7 @@ status_rpi_services() {
local rpi_user="${LAB_RPI_USER:-${LAB_RASPBERRY_USER:-jv}}" local rpi_user="${LAB_RPI_USER:-${LAB_RASPBERRY_USER:-jv}}"
local rpi_key="${LAB_RPI_SSH_KEY_PATH:-${LAB_RASPBERRY_SSH_KEY_PATH:-/home/jv/.ssh/id_ed25519}}" local rpi_key="${LAB_RPI_SSH_KEY_PATH:-${LAB_RASPBERRY_SSH_KEY_PATH:-/home/jv/.ssh/id_ed25519}}"
local install_dir="${LAB_RPI_SERVICES_INSTALL_DIR:-/opt/homelab-rpi-services}" local install_dir="${LAB_RPI_SERVICES_INSTALL_DIR:-/opt/homelab-rpi-services}"
local docker_nvme_root="${LAB_RPI_DOCKER_NVME_ROOT:-/nvme-storage/docker}" local docker_nvme_root="${LAB_RPI_DOCKER_ROOT:-${LAB_RPI_DOCKER_NVME_ROOT:-/nvme-storage/docker}}"
local docker_fallback_root="${LAB_RPI_DOCKER_FALLBACK_ROOT:-/var/lib/docker}" local docker_fallback_root="${LAB_RPI_DOCKER_FALLBACK_ROOT:-/var/lib/docker}"
local pihole_container="${PIHOLE_CONTAINER_NAME:-homelab-pihole}" local pihole_container="${PIHOLE_CONTAINER_NAME:-homelab-pihole}"
local unbound_container="${UNBOUND_CONTAINER_NAME:-homelab-unbound}" local unbound_container="${UNBOUND_CONTAINER_NAME:-homelab-unbound}"
@ -6092,7 +6240,7 @@ if [ \"\$docker_root\" = '${docker_nvme_root}' ]; then
printf '%-34s warn - configured for nvme but mountpoint is missing\n' 'Docker root class' printf '%-34s warn - configured for nvme but mountpoint is missing\n' 'Docker root class'
fi fi
elif [ \"\$docker_root\" = '${docker_fallback_root}' ]; then elif [ \"\$docker_root\" = '${docker_fallback_root}' ]; then
printf '%-34s ok - fallback root active\n' 'Docker root class' printf '%-34s warn - fallback root active; expected ${docker_nvme_root}\n' 'Docker root class'
else else
printf '%-34s warn - unexpected docker root\n' 'Docker root class' printf '%-34s warn - unexpected docker root\n' 'Docker root class'
fi fi
@ -6404,7 +6552,7 @@ doctor_rpi() {
cat <<'EOF' cat <<'EOF'
Next steps: Next steps:
- If Docker root is /var/lib/docker, the NVMe mount is unavailable or failed the writable check. - Docker root should be /nvme-storage/docker; if it is /var/lib/docker, the NVMe mount is unavailable or failed the writable check.
- If Pi-hole works but Unbound fails, DNS should still resolve through configured public fallbacks. - If Pi-hole works but Unbound fails, DNS should still resolve through configured public fallbacks.
- If Pi-hole DNS fails, clients using the RPi as DNS will be affected. - If Pi-hole DNS fails, clients using the RPi as DNS will be affected.
- Reapply the stack with ./jeannie rpi-services. - Reapply the stack with ./jeannie rpi-services.
@ -6414,7 +6562,7 @@ EOF
Doctor command: doctor-rpi Doctor command: doctor-rpi
RPi LAN host: ${LAB_RPI_HOST:-${LAB_RASPBERRY_HOST:-192.168.100.89}} RPi LAN host: ${LAB_RPI_HOST:-${LAB_RASPBERRY_HOST:-192.168.100.89}}
RPi Tailscale IP: ${LAB_RPI_TAILSCALE_IP:-unknown} RPi Tailscale IP: ${LAB_RPI_TAILSCALE_IP:-unknown}
Docker NVMe root: ${LAB_RPI_DOCKER_NVME_ROOT:-/nvme-storage/docker} Docker root: ${LAB_RPI_DOCKER_ROOT:-${LAB_RPI_DOCKER_NVME_ROOT:-/nvme-storage/docker}}
Docker fallback root: ${LAB_RPI_DOCKER_FALLBACK_ROOT:-/var/lib/docker} Docker fallback root: ${LAB_RPI_DOCKER_FALLBACK_ROOT:-/var/lib/docker}
Services: Pi-hole, Unbound, Uptime Kuma Services: Pi-hole, Unbound, Uptime Kuma
EOF EOF

View File

@ -158,14 +158,15 @@ rpi_capacity() {
section "RPi4" section "RPi4"
ssh -i "${rpi_key}" -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${rpi_user}@${rpi_host}" ' ssh -i "${rpi_key}" -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${rpi_user}@${rpi_host}" '
set +e set +e
docker_root="$(sudo docker info --format "{{.DockerRootDir}}" 2>/dev/null || true)"
echo "-- memory" echo "-- memory"
free -h free -h
echo echo
echo "-- disk" echo "-- disk"
df -h / /nvme-storage /var/lib/docker 2>/dev/null || df -h df -h / /nvme-storage "${docker_root:-/nvme-storage/docker}" 2>/dev/null || df -h
echo echo
echo "-- docker root" echo "-- docker root"
sudo docker info --format "{{.DockerRootDir}}" 2>/dev/null || true printf "%s\n" "${docker_root:-unknown}"
echo echo
echo "-- docker disk usage" echo "-- docker disk usage"
sudo docker system df 2>/dev/null || true sudo docker system df 2>/dev/null || true
@ -345,6 +346,8 @@ compact_rpi_capacity() {
local rpi_host="${LAB_RPI_HOST:-${LAB_RASPBERRY_HOST:-192.168.100.89}}" local rpi_host="${LAB_RPI_HOST:-${LAB_RASPBERRY_HOST:-192.168.100.89}}"
local rpi_user="${LAB_RPI_USER:-${LAB_RASPBERRY_USER:-jv}}" local rpi_user="${LAB_RPI_USER:-${LAB_RASPBERRY_USER:-jv}}"
local rpi_key="${LAB_RPI_SSH_KEY_PATH:-${LAB_RASPBERRY_SSH_KEY_PATH:-${HOME}/.ssh/id_ed25519}}" local rpi_key="${LAB_RPI_SSH_KEY_PATH:-${LAB_RASPBERRY_SSH_KEY_PATH:-${HOME}/.ssh/id_ed25519}}"
local expected_docker_root="${LAB_RPI_DOCKER_ROOT:-${LAB_RPI_DOCKER_NVME_ROOT:-/nvme-storage/docker}}"
local fallback_docker_root="${LAB_RPI_DOCKER_FALLBACK_ROOT:-/var/lib/docker}"
local output local output
output="$(ssh -i "${rpi_key}" -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${rpi_user}@${rpi_host}" ' output="$(ssh -i "${rpi_key}" -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${rpi_user}@${rpi_host}" '
@ -352,9 +355,11 @@ set +e
docker_root="$(sudo docker info --format "{{.DockerRootDir}}" 2>/dev/null || true)" docker_root="$(sudo docker info --format "{{.DockerRootDir}}" 2>/dev/null || true)"
root_used="$(df -P / 2>/dev/null | awk "NR == 2 { print \$5 }")" root_used="$(df -P / 2>/dev/null | awk "NR == 2 { print \$5 }")"
nvme_used="$(df -P /nvme-storage 2>/dev/null | awk "NR == 2 { print \$5 }")" nvme_used="$(df -P /nvme-storage 2>/dev/null | awk "NR == 2 { print \$5 }")"
docker_root_used="$(df -P "${docker_root:-/nvme-storage/docker}" 2>/dev/null | awk "NR == 2 { print \$5 }")"
echo "docker_root=${docker_root:-unknown}" echo "docker_root=${docker_root:-unknown}"
echo "root_used=${root_used:-unknown}" echo "root_used=${root_used:-unknown}"
echo "nvme_used=${nvme_used:-unknown}" echo "nvme_used=${nvme_used:-unknown}"
echo "docker_root_used=${docker_root_used:-unknown}"
' 2>/dev/null || true)" ' 2>/dev/null || true)"
if [[ -z "${output}" ]]; then if [[ -z "${output}" ]]; then
@ -365,10 +370,18 @@ echo "nvme_used=${nvme_used:-unknown}"
local docker_root local docker_root
local root_used local root_used
local nvme_used local nvme_used
local docker_root_used
docker_root="$(printf '%s\n' "${output}" | awk -F= '/docker_root=/ { print $2 }')" docker_root="$(printf '%s\n' "${output}" | awk -F= '/docker_root=/ { print $2 }')"
root_used="$(printf '%s\n' "${output}" | awk -F= '/root_used=/ { print $2 }')" root_used="$(printf '%s\n' "${output}" | awk -F= '/root_used=/ { print $2 }')"
nvme_used="$(printf '%s\n' "${output}" | awk -F= '/nvme_used=/ { print $2 }')" nvme_used="$(printf '%s\n' "${output}" | awk -F= '/nvme_used=/ { print $2 }')"
emit ok "RPi4" "Docker root" "${docker_root:-unknown}" docker_root_used="$(printf '%s\n' "${output}" | awk -F= '/docker_root_used=/ { print $2 }')"
if [[ "${docker_root}" == "${expected_docker_root}" ]]; then
emit ok "RPi4" "Docker root" "${docker_root}" "used=${docker_root_used:-unknown}"
elif [[ "${docker_root}" == "${fallback_docker_root}" ]]; then
emit warn "RPi4" "Docker root" "${docker_root}" "expected ${expected_docker_root}" "./jeannie doctor-rpi"
else
emit warn "RPi4" "Docker root" "${docker_root:-unknown}" "expected ${expected_docker_root}" "./jeannie doctor-rpi"
fi
emit ok "RPi4" "Root disk" "${root_used:-unknown}" emit ok "RPi4" "Root disk" "${root_used:-unknown}"
if [[ "${nvme_used:-unknown}" = "unknown" ]]; then if [[ "${nvme_used:-unknown}" = "unknown" ]]; then
emit warn "RPi4" "NVMe mount" "unknown" "df /nvme-storage failed" "./jeannie doctor-rpi" emit warn "RPi4" "NVMe mount" "unknown" "df /nvme-storage failed" "./jeannie doctor-rpi"

View File

@ -64,6 +64,7 @@ else:
"hosts.debian.docker_root", "hosts.debian.docker_root",
"hosts.rpi4.lan_ip", "hosts.rpi4.lan_ip",
"hosts.rpi4.tailscale_ip", "hosts.rpi4.tailscale_ip",
"hosts.rpi4.docker_root",
"hosts.opi5_pimox.lan_ip", "hosts.opi5_pimox.lan_ip",
"hosts.opi5_pimox.worker_storage", "hosts.opi5_pimox.worker_storage",
"hosts.oci_edge.public_ip", "hosts.oci_edge.public_ip",
@ -155,6 +156,7 @@ required_paths = %w[
hosts.debian.docker_root hosts.debian.docker_root
hosts.rpi4.lan_ip hosts.rpi4.lan_ip
hosts.rpi4.tailscale_ip hosts.rpi4.tailscale_ip
hosts.rpi4.docker_root
hosts.opi5_pimox.lan_ip hosts.opi5_pimox.lan_ip
hosts.opi5_pimox.worker_storage hosts.opi5_pimox.worker_storage
hosts.oci_edge.public_ip hosts.oci_edge.public_ip