From 028e7bf1bb8602216ffe64cb0b227e3df4161816 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Wed, 15 Jul 2026 22:34:15 -0400 Subject: [PATCH] feat: add pbs (Intel NUC5i5RYB) drive map + USB-NVMe SMART & root support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a `nuc` chassis type and a `pbs` mapping: one internal 2.5" SATA (OS/boot) plus two rear USB 3.0 ports (stacked top/bottom) carrying USB-attached NVMe. The two JMicron USB bridges report an identical serial, so the drives are keyed by USB *port path* (usb-0:N), not by-id. Physical top/bottom was confirmed with an LED blink test: TOP = port 3-4 (usb-0:4), BOTTOM = port 3-3 (usb-0:3). Supporting changes so it works on the NUC: - maybe_sudo(): run privileged commands directly when already root (PBS/minimal Debian may not have sudo installed at all). - smart_collect(): auto-select a smartctl -d for USB bridges (sntjmicron/ sntrealtek/sntasmedia/sat); plain calls can't read NVMe behind a bridge and were mislabeling the drives (wrong type, false failed-health ✗). - Detect NVMe reported via a USB bridge (device is sdX) for correct TYPE. - Render usb-* bays in the drive table; align the nuc controller line. Verified live on pbs: correct top/bottom map + full SMART (type/temp/health/ model/serial) for the internal SATA and both USB NVMe drives. Co-Authored-By: Claude Opus 4.8 --- driveAtlas.sh | 128 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 121 insertions(+), 7 deletions(-) diff --git a/driveAtlas.sh b/driveAtlas.sh index d46b7c9..471dc1d 100644 --- a/driveAtlas.sh +++ b/driveAtlas.sh @@ -36,6 +36,54 @@ trap cleanup EXIT INT TERM #------------------------------------------------------------------------------ readonly DISK_BY_PATH="/dev/disk/by-path" +#------------------------------------------------------------------------------ +# maybe_sudo +# +# Runs a privileged command. When already root (e.g. PBS/minimal Debian, which +# may not even have sudo installed), runs it directly; otherwise prefixes sudo. +# Args: the command and its arguments. +#------------------------------------------------------------------------------ +maybe_sudo() { + if [[ $EUID -eq 0 ]]; then + "$@" + elif command -v sudo &>/dev/null; then + sudo "$@" + else + "$@" + fi +} + +#------------------------------------------------------------------------------ +# smart_collect DEVICE OUTFILE +# +# Runs smartctl and writes the raw output to OUTFILE, auto-selecting a device +# type. USB-attached drives (e.g. NVMe behind a JMicron/Realtek/ASMedia bridge, +# as on the NUC-based pbs) are not readable with a plain call and need an +# explicit -d; try common bridge types and keep the first that yields a real +# identity/health line so we don't mislabel the drive (wrong type / false ✗). +#------------------------------------------------------------------------------ +smart_collect() { + local dev="$1" out="$2" + local tran raw d + local -a dtypes=("") + tran="$(lsblk -dn -o TRAN "/dev/$dev" 2>/dev/null | tr -d '[:space:]')" + if [[ "$tran" == "usb" ]]; then + dtypes+=("sntjmicron" "sntrealtek" "sntasmedia" "sat") + fi + for d in "${dtypes[@]}"; do + if [[ -z "$d" ]]; then + raw="$(maybe_sudo smartctl -A -i -H "/dev/$dev" 2>/dev/null)" + else + raw="$(maybe_sudo smartctl -d "$d" -A -i -H "/dev/$dev" 2>/dev/null)" + fi + if printf '%s' "$raw" | grep -qiE 'Serial Number:|Device Model:|Model Number:|SMART overall-health'; then + printf '%s\n' "$raw" > "$out" + return 0 + fi + done + printf '%s\n' "${raw:-}" > "$out" +} + #------------------------------------------------------------------------------ # show_usage # @@ -320,8 +368,8 @@ check_dependencies() { fi # Check for sudo access (needed for smartctl) - if command -v smartctl &>/dev/null && ! sudo -n true 2>/dev/null; then - echo "Note: SMART data requires sudo access. Run with sudo for full functionality." >&2 + if command -v smartctl &>/dev/null && [[ $EUID -ne 0 ]] && ! sudo -n true 2>/dev/null; then + echo "Note: SMART data requires root. Run with sudo (or as root) for full functionality." >&2 fi } @@ -515,6 +563,48 @@ generate_micro_layout() { printf "└─────────────────────────────────────────────────────────────┘\n" } +#------------------------------------------------------------------------------ +# generate_nuc_layout +# +# Generates ASCII art for an Intel NUC (e.g., pbs = NUC5i5RYB): one internal +# 2.5"/M.2 SATA drive plus two rear USB 3.0 ports (stacked top/bottom), used +# here for USB-attached NVMe. Bays: int-os, usb-top, usb-bot. +# +# Args: +# $1 - Hostname to display in the layout header +# +# Side effects: Calls build_drive_map() to populate DRIVE_MAP +#------------------------------------------------------------------------------ +generate_nuc_layout() { + local hostname="$1" + build_drive_map + + local nuc_model + nuc_model="$(maybe_sudo dmidecode -s baseboard-product-name 2>/dev/null | head -n1)" + [[ -z "$nuc_model" ]] && nuc_model="Intel NUC" + + printf "┌─────────────────────────────────────────────────────────────┐\n" + printf "│ %-57s │\n" "$hostname - $nuc_model (Mini PC)" + printf "│ │\n" + printf "│ Storage Controllers: │\n" + while IFS= read -r ctrl; do + [[ -n "$ctrl" ]] && printf "│ %-59.59s│\n" "$ctrl" + done < <(get_storage_controllers) + printf "│ │\n" + printf "│ Internal 2.5\" SATA (OS / boot): │\n" + printf "│ ┌────────────────┐ │\n" + printf "│ │ %-14s │ │\n" "${DRIVE_MAP[int-os]:-EMPTY}" + printf "│ └────────────────┘ │\n" + printf "│ │\n" + printf "│ Rear USB 3.0 (stacked): │\n" + printf "│ ┌─────────────────────────┐ │\n" + printf "│ │ TOP (3-4): %-9s │ │\n" "${DRIVE_MAP[usb-top]:-EMPTY}" + printf "│ ├─────────────────────────┤ │\n" + printf "│ │ BOTTOM (3-3): %-9s │ │\n" "${DRIVE_MAP[usb-bot]:-EMPTY}" + printf "│ └─────────────────────────┘ │\n" + printf "└─────────────────────────────────────────────────────────────┘\n" +} + #------------------------------------------------------------------------------ # generate_large1_layout # @@ -684,6 +774,23 @@ declare -A SERVER_MAPPINGS=( # SATA controller would be at a specific PCI address when drives connected ["monitor-02"]=" " + + # pbs + # Intel NUC5i5RYB - Proxmox Backup Server + # int-os : internal 2.5\" SATA SSD (Samsung MZNLN128) - OS/boot, controller 00:1f.2 + # usb-top : TOP rear USB 3.0 NVMe (Patriot P300 / JMicron bridge) - halfTbNVMeMirror + # usb-bot : BOTTOM rear USB 3.0 NVMe (Patriot P300 / JMicron bridge) - halfTbNVMeMirror + # + # The two USB bridges report an IDENTICAL serial, so these drives are keyed by USB + # *port path* (usb-0:N = kernel bus3 port N), NOT by-id/serial. Physical top/bottom + # confirmed by an LED blink test on 2026-07-15: + # TOP rear jack = port 3-4 (usb-0:4) = fw serial P300WCBA24090617615 (label 022411042502235) + # BOTTOM rear jack = port 3-3 (usb-0:3) = fw serial P300WCBA24090617536 + ["pbs"]=" + pci-0000:00:1f.2-ata-4 int-os + pci-0000:00:14.0-usb-0:4:1.0-scsi-0:0:0:0 usb-top + pci-0000:00:14.0-usb-0:3:1.0-scsi-0:0:0:0 usb-bot + " ) declare -A CHASSIS_TYPES=( @@ -693,6 +800,7 @@ declare -A CHASSIS_TYPES=( ["large1"]="large1" ["micro1"]="micro" # ZimaBoard 832 ["monitor-02"]="micro" # ZimaBoard 832 + ["pbs"]="nuc" # Intel NUC5i5RYB (1 internal SATA + 2 rear USB-NVMe) ) #------------------------------------------------------------------------------ @@ -903,6 +1011,9 @@ parse_smart_data() { # Priority: 1) NVMe by name, 2) Rotation Rate field, 3) Model name hints, 4) Default HDD if [[ "$device" == nvme* ]]; then type="NVMe" + elif echo "$smart_info" | grep -qiE "NVMe Version:|Number of Namespaces:"; then + # NVMe behind a USB bridge (device is sdX, but smartctl reports NVMe info) + type="NVMe" elif echo "$smart_info" | grep -qE "Rotation Rate:"; then # Check the Rotation Rate field value (may have leading whitespace) local rotation_rate @@ -1014,7 +1125,7 @@ get_drive_smart_info() { local device="$1" local smart_info - smart_info="$(sudo smartctl -A -i -H "/dev/$device" 2>/dev/null)" + smart_info="$(maybe_sudo smartctl -A -i -H "/dev/$device" 2>/dev/null)" parse_smart_data "$device" "$smart_info" } @@ -1041,6 +1152,9 @@ case "$CHASSIS_TYPE" in "micro") generate_micro_layout "$HOSTNAME" ;; + "nuc") + generate_nuc_layout "$HOSTNAME" + ;; *) echo "┌─────────────────────────────────────────────────────────┐" echo "│ Unknown server: $HOSTNAME" @@ -1080,7 +1194,7 @@ done # Sort drives by bay position (numeric bays first, then m2 slots) # Combine numeric bays (sorted numerically) with m2 slots (sorted alphanumerically) -all_bays="$(printf '%s\n' "${!DRIVE_MAP[@]}" | grep -E '^[0-9]+$' | sort -n; printf '%s\n' "${!DRIVE_MAP[@]}" | grep -E '^m2-' | sort; printf '%s\n' "${!DRIVE_MAP[@]}" | grep -E '^int-' | sort)" +all_bays="$(printf '%s\n' "${!DRIVE_MAP[@]}" | grep -E '^[0-9]+$' | sort -n; printf '%s\n' "${!DRIVE_MAP[@]}" | grep -E '^m2-' | sort; printf '%s\n' "${!DRIVE_MAP[@]}" | grep -E '^int-' | sort; printf '%s\n' "${!DRIVE_MAP[@]}" | grep -E '^usb' | sort)" # Cache lsblk data to reduce redundant calls # Get device sizes (whole disk only) @@ -1123,8 +1237,8 @@ if [[ "$SKIP_SMART" != true ]]; then for bay in $all_bays; do device="${DRIVE_MAP[$bay]}" if [[ -n "$device" && "$device" != "EMPTY" && -b "/dev/$device" ]]; then - # Launch background job to collect raw smartctl data - (sudo smartctl -A -i -H "/dev/$device" 2>/dev/null | sudo tee "$SMART_CACHE_DIR/${device}.raw" > /dev/null) & + # Launch background job to collect raw smartctl data (auto-picks -d for USB) + smart_collect "$device" "$SMART_CACHE_DIR/${device}.raw" & ((job_count++)) if ((job_count >= max_parallel_jobs)); then wait -n 2>/dev/null || wait # wait -n requires bash 4.3+, fall back to wait @@ -1246,7 +1360,7 @@ if [[ -n "$nvme_devices" ]]; then [[ -z "$name" ]] && continue device="/dev/$name" # Get model and serial from smartctl for accuracy - smart_info="$(sudo smartctl -i "$device" 2>/dev/null)" + smart_info="$(maybe_sudo smartctl -i "$device" 2>/dev/null)" model="$(echo "$smart_info" | grep "Model Number" | cut -d: -f2 | xargs)" serial="$(echo "$smart_info" | grep "Serial Number" | cut -d: -f2 | xargs)" [[ -z "$model" ]] && model="-"