feat: add pbs (Intel NUC5i5RYB) drive map + USB-NVMe SMART & root support
Lint / Shell (shellcheck) (push) Successful in 7s
Lint / Shell (shellcheck) (pull_request) Successful in 7s

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 22:34:15 -04:00
co-authored by Claude Opus 4.8
parent f8046546cc
commit 028e7bf1bb
+121 -7
View File
@@ -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="-"