Merge pull request 'feat: add pbs (Intel NUC5i5RYB) drive map + USB-NVMe SMART & root support' (#26) from feature/pbs-nuc-drive-map into main
Lint / Shell (shellcheck) (push) Successful in 7s
Lint / Shell (shellcheck) (push) Successful in 7s
Reviewed-on: #26
This commit was merged in pull request #26.
This commit is contained in:
+121
-7
@@ -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="-"
|
||||
|
||||
Reference in New Issue
Block a user