#!/bin/bash # Wake-on-LAN and shutdown script for f3s hosts (f0, f1, f2, f3) # and optional shutdown for Raspberry Pi nodes (pi0–pi3) # # Before any shutdown of f3s Beelink hosts (f0/f1/f2/f3), this script will # attempt to umount all NFS filesystems currently mounted on this machine # (earth). NFS mounts here are tunnelled via stunnel (127.0.0.1:2323 → # 192.168.1.138:2323 CARP VIP on f0/f1). If any NFS mount is active and # cannot be umounted, the shutdown is aborted to prevent data loss or a # hung filesystem. # # Usage: # wol-f3s # Wake f0, f1, and f2 # wol-f3s f0 # Wake only f0 # wol-f3s f1 # Wake only f1 # wol-f3s f2 # Wake only f2 # wol-f3s f3 # Wake only f3 # wol-f3s shutdown # Shutdown f0, f1, and f2 # wol-f3s shutdown-f3 # Shutdown only f3 # wol-f3s shutdown-pis # Shutdown all four Raspberry Pis # wol-f3s shutdown-all # Shutdown f0, f1, f2, and Pis # MAC addresses F0_MAC="e8:ff:1e:d7:1c:ac" # f0 (192.168.1.130) F1_MAC="e8:ff:1e:d7:1e:44" # f1 (192.168.1.131) F2_MAC="e8:ff:1e:d7:1c:a0" # f2 (192.168.1.132) F3_MAC="e8:ff:1e:d7:f3:d7" # f3 (192.168.1.133) # IP addresses F0_IP="192.168.1.130" F1_IP="192.168.1.131" F2_IP="192.168.1.132" F3_IP="192.168.1.133" # Raspberry Pi IP addresses (no WoL support, shutdown only) PI0_IP="192.168.1.125" PI1_IP="192.168.1.126" PI2_IP="192.168.1.127" PI3_IP="192.168.1.128" # Shelly Plug M Gen 3 controlling the rack fans (HTTP RPC API). # It is powered on when waking all hosts and powered off when shutting all # hosts down. The plug uses digest auth (user "admin"); the password is read # from ~/.shelly_plug if that file exists. # # The shutdown paths need root (to umount NFS), so this script is typically # run elevated. When elevated, $HOME becomes root's home, where ~/.shelly_plug # does not exist — so resolve the file relative to the invoking user's home # ($SUDO_USER under sudo on Linux, $DOAS_USER under doas on NetBSD Pis), # falling back to $HOME otherwise. Uses ~-expansion (portable) rather than # getent, which does not exist on NetBSD. SHELLY_IP="192.168.1.28" _shelly_user="${SUDO_USER:-${DOAS_USER:-}}" if [[ -n "$_shelly_user" ]]; then _shelly_home="$(eval echo "~${_shelly_user}")" SHELLY_PASS_FILE="${_shelly_home:-$HOME}/.shelly_plug" else SHELLY_PASS_FILE="${HOME}/.shelly_plug" fi # SSH user SSH_USER="paul" # Broadcast address for your LAN BROADCAST="192.168.1.255" wake() { local name=$1 local mac=$2 if [[ "$mac" == "XX:XX:XX:XX:XX:XX" ]]; then echo "⚠️ $name MAC address not configured yet" return 1 fi echo "Sending WoL packet to $name ($mac)..." wol -i "$BROADCAST" "$mac" } # shelly_set turns the rack-fan Shelly plug on or off. # $1 — "true" to power on, "false" to power off shelly_set() { local on=$1 local label="on" [[ "$on" == "false" ]] && label="off" local auth=() if [[ -r "$SHELLY_PASS_FILE" ]]; then local pass pass="$(head -n 1 "$SHELLY_PASS_FILE" | tr -d '\r\n')" [[ -n "$pass" ]] && auth=(--digest -u "admin:$pass") fi if [[ ${#auth[@]} -eq 0 ]]; then echo " ⚠️ No Shelly password found at $SHELLY_PASS_FILE" echo " — cannot switch plug $label (digest auth required)." return 1 fi echo "Turning rack-fan Shelly plug $label ($SHELLY_IP)..." # --fail is essential: without it curl returns 0 even on an HTTP 401 # (auth failure), which would falsely report success while the plug is # left untouched. if curl -s --fail --max-time 5 "${auth[@]}" \ "http://${SHELLY_IP}/rpc/Switch.Set?id=0&on=${on}" >/dev/null; then echo " ✓ Shelly plug switched $label" else echo " ✗ Failed to switch Shelly plug at $SHELLY_IP (auth or network error)" return 1 fi } # umount_nfs_mounts tries to umount all currently mounted NFS filesystems on # this local machine (earth). It reads /proc/mounts to find active nfs/nfs4 # mounts, attempts to umount each one, and returns 1 if any mount could not # be umounted. This prevents shutting down f3s servers while NFS is still in # use, which would leave the filesystem in a hung/stale state. umount_nfs_mounts() { # Collect all active NFS mount points from /proc/mounts (covers nfs and nfs4) local nfs_mounts=() while IFS= read -r mountpoint; do nfs_mounts+=("$mountpoint") done < <(awk '$3 ~ /^nfs/ { print $2 }' /proc/mounts) if [[ ${#nfs_mounts[@]} -eq 0 ]]; then echo " No NFS filesystems currently mounted — nothing to umount." return 0 fi local failed=0 for mp in "${nfs_mounts[@]}"; do echo " Umounting NFS filesystem: $mp ..." if umount "$mp" 2>/dev/null; then echo " ✓ Umounted $mp" else # If it's no longer listed in /proc/mounts, it's already gone. if ! grep -q "^[^ ]* $mp nfs" /proc/mounts; then echo " ✓ $mp was already unmounted" else echo " ✗ Failed to umount $mp (in use or already unmounted?)" failed=1 fi fi done if [[ $failed -ne 0 ]]; then echo "" echo "✗ One or more NFS filesystems could not be umounted." echo " Aborting shutdown to prevent data loss or a hung filesystem." echo " Please check running processes that may have open files on NFS," echo " then retry." return 1 fi return 0 } shutdown_host() { local name=$1 local ip=$2 echo "Shutting down $name ($ip)..." ssh -o ConnectTimeout=5 "$SSH_USER@$ip" "doas poweroff" 2>/dev/null && \ echo " ✓ Shutdown command sent to $name" || \ echo " ✗ Failed to reach $name (already down?)" } ACTION="${1:-all}" case "$ACTION" in f0) wake "f0" "$F0_MAC" ;; f1) wake "f1" "$F1_MAC" ;; f2) wake "f2" "$F2_MAC" ;; f3) wake "f3" "$F3_MAC" ;; all|"") # Power on the rack fans before booting the hosts. shelly_set "true" wake "f0" "$F0_MAC" wake "f1" "$F1_MAC" wake "f2" "$F2_MAC" ;; shutdown|poweroff|down) # Umount local NFS filesystems first; abort if any are stuck. # The NFS mounts on earth tunnel through stunnel to the f3s CARP VIP, # so they will hang or corrupt if the servers go down while mounted. echo "Checking for locally mounted NFS filesystems..." umount_nfs_mounts || exit 1 # Mute Gogios monitoring alerts for a day on both OpenBSD gateways ssh rex@blowfish.buetow.org touch /tmp/f3s_taken_down ssh rex@fishfinger.buetow.org touch /tmp/f3s_taken_down shutdown_host "f0" "$F0_IP" shutdown_host "f1" "$F1_IP" shutdown_host "f2" "$F2_IP" # Power off the rack fans once all hosts are shutting down. shelly_set "false" echo "" echo "✓ Shutdown commands sent to all Beelinks." exit 0 ;; shutdown-f3|poweroff-f3|down-f3) # Umount local NFS filesystems first; abort if any are stuck. # f3 does not host the NFS CARP VIP (that is f0/f1), but umounting # first is still the safe default to avoid stale state. echo "Checking for locally mounted NFS filesystems..." umount_nfs_mounts || exit 1 shutdown_host "f3" "$F3_IP" echo "" echo "✓ Shutdown command sent to f3." exit 0 ;; shutdown-pis) # Raspberry Pis do not serve NFS, so no umount check is needed here. shutdown_host "pi0" "$PI0_IP" shutdown_host "pi1" "$PI1_IP" shutdown_host "pi2" "$PI2_IP" shutdown_host "pi3" "$PI3_IP" echo "" echo "✓ Shutdown commands sent to all Pis." exit 0 ;; shutdown-all) # Umount local NFS filesystems first; abort if any are stuck. # The NFS mounts on earth tunnel through stunnel to the f3s CARP VIP, # so they will hang or corrupt if the servers go down while mounted. echo "Checking for locally mounted NFS filesystems..." umount_nfs_mounts || exit 1 # Mute Gogios monitoring alerts for a day on both OpenBSD gateways ssh rex@blowfish.buetow.org touch /tmp/f3s_taken_down ssh rex@fishfinger.buetow.org touch /tmp/f3s_taken_down shutdown_host "f0" "$F0_IP" shutdown_host "f1" "$F1_IP" shutdown_host "f2" "$F2_IP" shutdown_host "pi0" "$PI0_IP" shutdown_host "pi1" "$PI1_IP" shutdown_host "pi2" "$PI2_IP" shutdown_host "pi3" "$PI3_IP" # Power off the rack fans once all hosts are shutting down. shelly_set "false" echo "" echo "✓ Shutdown commands sent to all machines." exit 0 ;; *) echo "Usage: $0 [f0|f1|f2|f3|all|shutdown|shutdown-f3|shutdown-pis|shutdown-all]" exit 1 ;; esac echo "" echo "✓ WoL packets sent. Machines should boot in a few seconds." echo " Wait ~30 seconds, then try: ssh paul@192.168.1.130"