feat: add pbs (Intel NUC5i5RYB) drive map + USB-NVMe SMART & root support
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:
+121
-7
@@ -36,6 +36,54 @@ trap cleanup EXIT INT TERM
|
|||||||
#------------------------------------------------------------------------------
|
#------------------------------------------------------------------------------
|
||||||
readonly DISK_BY_PATH="/dev/disk/by-path"
|
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
|
# show_usage
|
||||||
#
|
#
|
||||||
@@ -320,8 +368,8 @@ check_dependencies() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Check for sudo access (needed for smartctl)
|
# Check for sudo access (needed for smartctl)
|
||||||
if command -v smartctl &>/dev/null && ! sudo -n true 2>/dev/null; then
|
if command -v smartctl &>/dev/null && [[ $EUID -ne 0 ]] && ! sudo -n true 2>/dev/null; then
|
||||||
echo "Note: SMART data requires sudo access. Run with sudo for full functionality." >&2
|
echo "Note: SMART data requires root. Run with sudo (or as root) for full functionality." >&2
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -515,6 +563,48 @@ generate_micro_layout() {
|
|||||||
printf "└─────────────────────────────────────────────────────────────┘\n"
|
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
|
# generate_large1_layout
|
||||||
#
|
#
|
||||||
@@ -684,6 +774,23 @@ declare -A SERVER_MAPPINGS=(
|
|||||||
# SATA controller would be at a specific PCI address when drives connected
|
# SATA controller would be at a specific PCI address when drives connected
|
||||||
["monitor-02"]="
|
["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=(
|
declare -A CHASSIS_TYPES=(
|
||||||
@@ -693,6 +800,7 @@ declare -A CHASSIS_TYPES=(
|
|||||||
["large1"]="large1"
|
["large1"]="large1"
|
||||||
["micro1"]="micro" # ZimaBoard 832
|
["micro1"]="micro" # ZimaBoard 832
|
||||||
["monitor-02"]="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
|
# Priority: 1) NVMe by name, 2) Rotation Rate field, 3) Model name hints, 4) Default HDD
|
||||||
if [[ "$device" == nvme* ]]; then
|
if [[ "$device" == nvme* ]]; then
|
||||||
type="NVMe"
|
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
|
elif echo "$smart_info" | grep -qE "Rotation Rate:"; then
|
||||||
# Check the Rotation Rate field value (may have leading whitespace)
|
# Check the Rotation Rate field value (may have leading whitespace)
|
||||||
local rotation_rate
|
local rotation_rate
|
||||||
@@ -1014,7 +1125,7 @@ get_drive_smart_info() {
|
|||||||
local device="$1"
|
local device="$1"
|
||||||
local smart_info
|
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"
|
parse_smart_data "$device" "$smart_info"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1041,6 +1152,9 @@ case "$CHASSIS_TYPE" in
|
|||||||
"micro")
|
"micro")
|
||||||
generate_micro_layout "$HOSTNAME"
|
generate_micro_layout "$HOSTNAME"
|
||||||
;;
|
;;
|
||||||
|
"nuc")
|
||||||
|
generate_nuc_layout "$HOSTNAME"
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo "┌─────────────────────────────────────────────────────────┐"
|
echo "┌─────────────────────────────────────────────────────────┐"
|
||||||
echo "│ Unknown server: $HOSTNAME"
|
echo "│ Unknown server: $HOSTNAME"
|
||||||
@@ -1080,7 +1194,7 @@ done
|
|||||||
|
|
||||||
# Sort drives by bay position (numeric bays first, then m2 slots)
|
# Sort drives by bay position (numeric bays first, then m2 slots)
|
||||||
# Combine numeric bays (sorted numerically) with m2 slots (sorted alphanumerically)
|
# 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
|
# Cache lsblk data to reduce redundant calls
|
||||||
# Get device sizes (whole disk only)
|
# Get device sizes (whole disk only)
|
||||||
@@ -1123,8 +1237,8 @@ if [[ "$SKIP_SMART" != true ]]; then
|
|||||||
for bay in $all_bays; do
|
for bay in $all_bays; do
|
||||||
device="${DRIVE_MAP[$bay]}"
|
device="${DRIVE_MAP[$bay]}"
|
||||||
if [[ -n "$device" && "$device" != "EMPTY" && -b "/dev/$device" ]]; then
|
if [[ -n "$device" && "$device" != "EMPTY" && -b "/dev/$device" ]]; then
|
||||||
# Launch background job to collect raw smartctl data
|
# Launch background job to collect raw smartctl data (auto-picks -d for USB)
|
||||||
(sudo smartctl -A -i -H "/dev/$device" 2>/dev/null | sudo tee "$SMART_CACHE_DIR/${device}.raw" > /dev/null) &
|
smart_collect "$device" "$SMART_CACHE_DIR/${device}.raw" &
|
||||||
((job_count++))
|
((job_count++))
|
||||||
if ((job_count >= max_parallel_jobs)); then
|
if ((job_count >= max_parallel_jobs)); then
|
||||||
wait -n 2>/dev/null || wait # wait -n requires bash 4.3+, fall back to wait
|
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
|
[[ -z "$name" ]] && continue
|
||||||
device="/dev/$name"
|
device="/dev/$name"
|
||||||
# Get model and serial from smartctl for accuracy
|
# 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)"
|
model="$(echo "$smart_info" | grep "Model Number" | cut -d: -f2 | xargs)"
|
||||||
serial="$(echo "$smart_info" | grep "Serial Number" | cut -d: -f2 | xargs)"
|
serial="$(echo "$smart_info" | grep "Serial Number" | cut -d: -f2 | xargs)"
|
||||||
[[ -z "$model" ]] && model="-"
|
[[ -z "$model" ]] && model="-"
|
||||||
|
|||||||
Reference in New Issue
Block a user