Lint / Shell (shellcheck) (push) Successful in 28s
The fin ramp alternated colour keys every two glyphs (█▓ bright, ▒░ mid), so the run-length colour encoder emitted a new escape roughly every 2.6 characters: 25 escapes per fin line, inflating a 66-glyph line to 334 bytes and 206 escapes for the drawing as a whole. The █▓▒░ ramp already carries the fin relief through glyph ink density, so alternating colour across it adds nothing visible. Paint the whole fin field one steel key instead: fin lines drop to 5 escapes / 114 bytes and the drawing to 134 escapes total, with byte-identical monochrome output and no visual change in colour. Gentler on terminals that cope poorly with dense escape/multibyte interleaving. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1786 lines
78 KiB
Bash
1786 lines
78 KiB
Bash
#!/bin/bash
|
||
|
||
#==============================================================================
|
||
# Drive Atlas - Server Drive Mapping Tool
|
||
# Maps physical drive bays to logical device names using PCI paths
|
||
#==============================================================================
|
||
|
||
# Shell safety options:
|
||
# -o pipefail: Exit status of pipe is rightmost non-zero exit code
|
||
# Note: Not using -e (errexit) to allow graceful degradation when tools fail
|
||
# Note: Not using -u (nounset) as script uses ${var:-default} patterns
|
||
set -o pipefail
|
||
|
||
# Require bash 4.2+ for declare -g -A (global associative arrays)
|
||
if ((BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 2))); then
|
||
echo "ERROR: This script requires Bash 4.2 or higher (current: $BASH_VERSION)" >&2
|
||
exit 1
|
||
fi
|
||
|
||
VERSION="1.1.0"
|
||
|
||
#------------------------------------------------------------------------------
|
||
# Cleanup Trap
|
||
# Ensures temporary directories are removed on exit or interruption
|
||
#------------------------------------------------------------------------------
|
||
cleanup() {
|
||
if [[ -n "${SMART_CACHE_DIR:-}" && -d "$SMART_CACHE_DIR" ]]; then
|
||
rm -rf "$SMART_CACHE_DIR"
|
||
fi
|
||
}
|
||
trap cleanup EXIT INT TERM
|
||
|
||
#------------------------------------------------------------------------------
|
||
# Path Constants
|
||
# Centralized path definitions to avoid hardcoding throughout the script
|
||
#------------------------------------------------------------------------------
|
||
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
|
||
#
|
||
# Displays help message with usage information and available options.
|
||
#------------------------------------------------------------------------------
|
||
show_usage() {
|
||
cat << EOF
|
||
Drive Atlas v${VERSION} - Server Drive Mapping Tool
|
||
|
||
Maps physical drive bays to logical device names using PCI paths.
|
||
Displays visual chassis layouts and comprehensive drive information.
|
||
|
||
USAGE:
|
||
$(basename "$0") [OPTIONS]
|
||
|
||
OPTIONS:
|
||
-h, --help Show this help message and exit
|
||
-v, --version Show version information
|
||
-d, --debug Enable debug output (show drive mappings)
|
||
-s, --skip-smart Skip SMART data collection (faster)
|
||
-c, --color Enable colored output
|
||
--verbose Show detailed error messages and warnings
|
||
--no-ceph Skip Ceph OSD information
|
||
--show-pci Show PCI paths in output
|
||
--diagnose Show all PCI paths and block devices (for mapping new servers)
|
||
|
||
EXAMPLES:
|
||
$(basename "$0") # Normal run with all features
|
||
$(basename "$0") --skip-smart # Fast run without SMART data
|
||
$(basename "$0") --color # Run with colored output
|
||
$(basename "$0") --verbose # Show all errors and warnings
|
||
$(basename "$0") --debug # Show mapping debug info
|
||
$(basename "$0") --diagnose # Gather PCI paths for new server setup
|
||
|
||
ENVIRONMENT VARIABLES:
|
||
DEBUG=1 Same as --debug flag
|
||
|
||
For more information, see: https://code.lotusguild.org/LotusGuild/driveAtlas
|
||
EOF
|
||
}
|
||
|
||
#------------------------------------------------------------------------------
|
||
# Command Line Argument Parsing
|
||
#------------------------------------------------------------------------------
|
||
SKIP_SMART=false
|
||
SKIP_CEPH=false
|
||
SHOW_PCI=false
|
||
USE_COLOR=false
|
||
VERBOSE=false
|
||
RUN_DIAGNOSE=false
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
-h|--help)
|
||
show_usage
|
||
exit 0
|
||
;;
|
||
-v|--version)
|
||
echo "Drive Atlas v${VERSION}"
|
||
exit 0
|
||
;;
|
||
-d|--debug)
|
||
DEBUG=1
|
||
shift
|
||
;;
|
||
-s|--skip-smart)
|
||
SKIP_SMART=true
|
||
shift
|
||
;;
|
||
--no-ceph)
|
||
SKIP_CEPH=true
|
||
shift
|
||
;;
|
||
--show-pci)
|
||
SHOW_PCI=true
|
||
shift
|
||
;;
|
||
-c|--color)
|
||
USE_COLOR=true
|
||
shift
|
||
;;
|
||
--verbose)
|
||
VERBOSE=true
|
||
shift
|
||
;;
|
||
--diagnose)
|
||
RUN_DIAGNOSE=true
|
||
shift
|
||
;;
|
||
*)
|
||
echo "Unknown option: $1" >&2
|
||
echo "Use --help for usage information." >&2
|
||
exit 1
|
||
;;
|
||
esac
|
||
done
|
||
|
||
#------------------------------------------------------------------------------
|
||
# Color Definitions
|
||
# ANSI escape codes for terminal colors
|
||
#------------------------------------------------------------------------------
|
||
if [[ "$USE_COLOR" == true ]]; then
|
||
COLOR_RESET='\033[0m'
|
||
COLOR_RED='\033[0;31m'
|
||
COLOR_GREEN='\033[0;32m'
|
||
COLOR_YELLOW='\033[0;33m'
|
||
COLOR_BLUE='\033[0;34m'
|
||
COLOR_CYAN='\033[0;36m'
|
||
COLOR_BOLD='\033[1m'
|
||
else
|
||
COLOR_RESET=''
|
||
COLOR_RED=''
|
||
COLOR_GREEN=''
|
||
COLOR_YELLOW=''
|
||
COLOR_BLUE=''
|
||
COLOR_CYAN=''
|
||
COLOR_BOLD=''
|
||
fi
|
||
|
||
#------------------------------------------------------------------------------
|
||
# colorize_health
|
||
#
|
||
# Returns health indicator with appropriate color
|
||
# Args: $1 - health status (✓ or ✗)
|
||
#------------------------------------------------------------------------------
|
||
colorize_health() {
|
||
local health="$1"
|
||
if [[ "$USE_COLOR" == true ]]; then
|
||
if [[ "$health" == "✓" ]]; then
|
||
printf '%b%s%b' "$COLOR_GREEN" "$health" "$COLOR_RESET"
|
||
else
|
||
printf '%b%s%b' "$COLOR_RED" "$health" "$COLOR_RESET"
|
||
fi
|
||
else
|
||
printf '%s' "$health"
|
||
fi
|
||
}
|
||
|
||
#------------------------------------------------------------------------------
|
||
# colorize_temp
|
||
#
|
||
# Returns temperature with color based on value
|
||
# Args: $1 - temperature string (e.g., "45°C")
|
||
#------------------------------------------------------------------------------
|
||
colorize_temp() {
|
||
local temp_str="$1"
|
||
local temp_val
|
||
|
||
if [[ "$USE_COLOR" != true || "$temp_str" == "-" ]]; then
|
||
echo "$temp_str"
|
||
return
|
||
fi
|
||
|
||
# Extract numeric value
|
||
temp_val="${temp_str%°C}"
|
||
if [[ "$temp_val" =~ ^[0-9]+$ ]]; then
|
||
if [[ "$temp_val" -ge 60 ]]; then
|
||
printf '%b%s%b' "$COLOR_RED" "$temp_str" "$COLOR_RESET"
|
||
elif [[ "$temp_val" -ge 50 ]]; then
|
||
printf '%b%s%b' "$COLOR_YELLOW" "$temp_str" "$COLOR_RESET"
|
||
else
|
||
printf '%b%s%b' "$COLOR_GREEN" "$temp_str" "$COLOR_RESET"
|
||
fi
|
||
else
|
||
printf '%s' "$temp_str"
|
||
fi
|
||
}
|
||
|
||
#------------------------------------------------------------------------------
|
||
# colorize_header
|
||
#
|
||
# Returns header text in blue/bold
|
||
# Args: $1 - header text
|
||
#------------------------------------------------------------------------------
|
||
colorize_header() {
|
||
if [[ "$USE_COLOR" == true ]]; then
|
||
printf '%b%b%s%b\n' "$COLOR_BLUE" "$COLOR_BOLD" "$1" "$COLOR_RESET"
|
||
else
|
||
printf '%s\n' "$1"
|
||
fi
|
||
}
|
||
|
||
#------------------------------------------------------------------------------
|
||
# log_error
|
||
#
|
||
# Logs an error message to stderr. Always shown regardless of verbose mode.
|
||
# Args: $1 - error message
|
||
#------------------------------------------------------------------------------
|
||
log_error() {
|
||
if [[ "$USE_COLOR" == true ]]; then
|
||
printf '%bERROR:%b %s\n' "$COLOR_RED" "$COLOR_RESET" "$1" >&2
|
||
else
|
||
printf 'ERROR: %s\n' "$1" >&2
|
||
fi
|
||
}
|
||
|
||
#------------------------------------------------------------------------------
|
||
# log_warn
|
||
#
|
||
# Logs a warning message to stderr. Only shown in verbose mode.
|
||
# Args: $1 - warning message
|
||
#------------------------------------------------------------------------------
|
||
log_warn() {
|
||
if [[ "$VERBOSE" == true ]]; then
|
||
if [[ "$USE_COLOR" == true ]]; then
|
||
printf '%bWARN:%b %s\n' "$COLOR_YELLOW" "$COLOR_RESET" "$1" >&2
|
||
else
|
||
printf 'WARN: %s\n' "$1" >&2
|
||
fi
|
||
fi
|
||
}
|
||
|
||
#------------------------------------------------------------------------------
|
||
# log_info
|
||
#
|
||
# Logs an informational message to stderr. Only shown in verbose mode.
|
||
# Args: $1 - info message
|
||
#------------------------------------------------------------------------------
|
||
log_info() {
|
||
if [[ "$VERBOSE" == true ]]; then
|
||
if [[ "$USE_COLOR" == true ]]; then
|
||
printf '%bINFO:%b %s\n' "$COLOR_CYAN" "$COLOR_RESET" "$1" >&2
|
||
else
|
||
printf 'INFO: %s\n' "$1" >&2
|
||
fi
|
||
fi
|
||
}
|
||
|
||
#------------------------------------------------------------------------------
|
||
# Dependency Checks
|
||
# Verifies required commands are available before running
|
||
#------------------------------------------------------------------------------
|
||
|
||
# Required dependencies (script will not function without these)
|
||
REQUIRED_DEPS=(lsblk lspci readlink hostname)
|
||
|
||
# Optional dependencies (enhanced functionality)
|
||
OPTIONAL_DEPS=(smartctl ceph ceph-volume bc nvme)
|
||
|
||
FRESH_START_URL="http://10.10.10.63:3000/LotusGuild/freshStartScript/raw/branch/main/freshStart.sh"
|
||
|
||
#------------------------------------------------------------------------------
|
||
# check_dependencies
|
||
#
|
||
# Verifies required and optional commands are available.
|
||
# Exits with error if required dependencies are missing.
|
||
# Warns about missing optional dependencies.
|
||
#------------------------------------------------------------------------------
|
||
check_dependencies() {
|
||
local missing_required=()
|
||
local missing_optional=()
|
||
|
||
# Check required dependencies
|
||
for cmd in "${REQUIRED_DEPS[@]}"; do
|
||
if ! command -v "$cmd" &>/dev/null; then
|
||
missing_required+=("$cmd")
|
||
fi
|
||
done
|
||
|
||
# Check optional dependencies
|
||
for cmd in "${OPTIONAL_DEPS[@]}"; do
|
||
if ! command -v "$cmd" &>/dev/null; then
|
||
missing_optional+=("$cmd")
|
||
fi
|
||
done
|
||
|
||
# Report missing required dependencies and exit
|
||
if [[ ${#missing_required[@]} -gt 0 ]]; then
|
||
echo "ERROR: Missing required dependencies: ${missing_required[*]}" >&2
|
||
echo "" >&2
|
||
echo "Please install the missing packages or run the fresh start script:" >&2
|
||
echo " curl -s $FRESH_START_URL | bash" >&2
|
||
echo "" >&2
|
||
exit 1
|
||
fi
|
||
|
||
# Warn about missing optional dependencies
|
||
if [[ ${#missing_optional[@]} -gt 0 ]]; then
|
||
echo "Note: Some optional features unavailable. Missing: ${missing_optional[*]}" >&2
|
||
echo " Install them or run: curl -s $FRESH_START_URL | bash" >&2
|
||
echo "" >&2
|
||
fi
|
||
|
||
# Check for sudo access (needed for smartctl)
|
||
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
|
||
}
|
||
|
||
# Run dependency check at script start
|
||
check_dependencies
|
||
|
||
#------------------------------------------------------------------------------
|
||
# run_diagnose
|
||
#
|
||
# Displays all PCI disk paths, storage controllers, and block devices.
|
||
# Used to gather information needed when mapping a new server.
|
||
#------------------------------------------------------------------------------
|
||
run_diagnose() {
|
||
local hostname
|
||
hostname="$(hostname)"
|
||
|
||
echo "=== Server Information ==="
|
||
echo "Hostname: $hostname"
|
||
echo "Date: $(date)"
|
||
echo ""
|
||
|
||
echo "=== Storage Controllers ==="
|
||
lspci 2>/dev/null | grep -iE "SAS|SATA|RAID|Mass storage|NVMe"
|
||
echo ""
|
||
|
||
echo "=== All /dev/disk/by-path/ entries (whole disks only) ==="
|
||
for path in "${DISK_BY_PATH}"/*; do
|
||
[[ -L "$path" ]] || continue
|
||
# Skip partitions
|
||
[[ "$path" =~ -part[0-9]+$ ]] && continue
|
||
|
||
local basename_path target device size serial model
|
||
basename_path="$(basename "$path")"
|
||
target="$(readlink -f "$path")"
|
||
device="$(basename "$target")"
|
||
size="$(lsblk -d -n -o SIZE "$target" 2>/dev/null | xargs)"
|
||
|
||
printf " %-55s -> %-10s %s\n" "$basename_path" "$device" "${size:+($size)}"
|
||
done
|
||
echo ""
|
||
|
||
echo "=== Block Devices ==="
|
||
lsblk -d -o NAME,SIZE,TYPE,TRAN 2>/dev/null | grep -v "rbd\|loop"
|
||
echo ""
|
||
|
||
# Check if this server has a mapping
|
||
local sanitized
|
||
sanitized="$(echo "$hostname" | tr -cd '[:alnum:]-_.')"
|
||
if [[ -n "${SERVER_MAPPINGS[$sanitized]:-}" ]]; then
|
||
echo "=== Current Mapping for $sanitized ==="
|
||
echo "${SERVER_MAPPINGS[$sanitized]}" | while read -r pci_path bay; do
|
||
[[ -z "$pci_path" || -z "$bay" ]] && continue
|
||
if [[ -L "${DISK_BY_PATH}/$pci_path" ]]; then
|
||
local dev
|
||
dev="$(readlink -f "${DISK_BY_PATH}/$pci_path" | sed 's/.*\///')"
|
||
printf " Bay %-5s %-55s -> %s\n" "$bay" "$pci_path" "$dev"
|
||
else
|
||
printf " Bay %-5s %-55s -> (not connected)\n" "$bay" "$pci_path"
|
||
fi
|
||
done
|
||
else
|
||
echo "NOTE: No mapping exists yet for '$sanitized'."
|
||
echo "Use the PCI paths above to create a SERVER_MAPPINGS entry."
|
||
fi
|
||
|
||
exit 0
|
||
}
|
||
|
||
#------------------------------------------------------------------------------
|
||
# Chassis Layout Generator Functions
|
||
# These define the physical layout and display formatting for each chassis type
|
||
#------------------------------------------------------------------------------
|
||
|
||
#------------------------------------------------------------------------------
|
||
# generate_10bay_layout
|
||
#
|
||
# Generates ASCII art representation of a 10-bay hot-swap chassis (Sliger CX4712).
|
||
# Shows storage controllers, M.2 NVMe slot, and 10 front hot-swap bays.
|
||
#
|
||
# Args:
|
||
# $1 - Hostname to display in the layout header
|
||
#
|
||
# Side effects: Calls build_drive_map() to populate DRIVE_MAP
|
||
#------------------------------------------------------------------------------
|
||
generate_10bay_layout() {
|
||
local hostname="$1"
|
||
build_drive_map
|
||
|
||
# Box interior width = 136 (determined by 10 bay boxes: 4 + 10*13 + 2)
|
||
# Total box width = 138 (136 interior + 2 for │ borders)
|
||
|
||
# Main chassis section
|
||
printf "┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐\n"
|
||
printf "│ %-132s │\n" "$hostname - Sliger CX4712 (10x 3.5\" Hot-swap)"
|
||
printf "│%-136s│\n" ""
|
||
|
||
# Show storage controllers
|
||
printf "│ %-134s│\n" "Storage Controllers:"
|
||
while IFS= read -r ctrl; do
|
||
[[ -n "$ctrl" ]] && printf "│ %-134.134s│\n" "$ctrl"
|
||
done < <(get_storage_controllers)
|
||
printf "│%-136s│\n" ""
|
||
|
||
# M.2 NVMe slot if present
|
||
if [[ -n "${DRIVE_MAP[m2-1]}" ]]; then
|
||
printf "│ %-134s│\n" " M.2 NVMe: ${DRIVE_MAP[m2-1]}"
|
||
printf "│%-136s│\n" ""
|
||
fi
|
||
|
||
# Internal (non-bay) drives if present
|
||
local int_keys
|
||
int_keys="$(printf '%s\n' "${!DRIVE_MAP[@]}" | grep -E '^int-' | sort)"
|
||
if [[ -n "$int_keys" ]]; then
|
||
while IFS= read -r int_key; do
|
||
printf "│ %-134s│\n" " Internal (non-bay): ${DRIVE_MAP[$int_key]}"
|
||
done <<< "$int_keys"
|
||
printf "│%-136s│\n" ""
|
||
fi
|
||
|
||
printf "│ %-134s│\n" " Front Hot-swap Bays:"
|
||
printf "│%-136s│\n" ""
|
||
|
||
# Bay top borders
|
||
printf "│ "
|
||
for bay in {1..10}; do
|
||
printf "┌──────────┐ "
|
||
done
|
||
printf " │\n"
|
||
|
||
# Bay contents
|
||
printf "│ "
|
||
for bay in {1..10}; do
|
||
printf "│%-2d:%-7s│ " "$bay" "${DRIVE_MAP[$bay]:-EMPTY}"
|
||
done
|
||
printf " │\n"
|
||
|
||
# Bay bottom borders
|
||
printf "│ "
|
||
for bay in {1..10}; do
|
||
printf "└──────────┘ "
|
||
done
|
||
printf " │\n"
|
||
|
||
printf "└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\n"
|
||
}
|
||
|
||
#------------------------------------------------------------------------------
|
||
# generate_micro_layout
|
||
#
|
||
# Generates ASCII art representation of a micro SBC (e.g., ZimaBoard).
|
||
# Shows storage controllers, onboard eMMC (if present), and 2 SATA ports.
|
||
#
|
||
# Args:
|
||
# $1 - Hostname to display in the layout header
|
||
#
|
||
# Side effects: Calls build_drive_map() to populate DRIVE_MAP
|
||
#------------------------------------------------------------------------------
|
||
generate_micro_layout() {
|
||
local hostname="$1"
|
||
build_drive_map
|
||
|
||
# Check for eMMC storage
|
||
local emmc_device=""
|
||
if [[ -b /dev/mmcblk0 ]]; then
|
||
emmc_device="mmcblk0"
|
||
fi
|
||
|
||
printf "┌─────────────────────────────────────────────────────────────┐\n"
|
||
printf "│ %-57s │\n" "$hostname - Micro SBC"
|
||
printf "│ │\n"
|
||
printf "│ Storage Controllers: │\n"
|
||
while IFS= read -r ctrl; do
|
||
[[ -n "$ctrl" ]] && printf "│ %-57.57s│\n" "$ctrl"
|
||
done < <(get_storage_controllers)
|
||
printf "│ │\n"
|
||
|
||
# Show eMMC if present
|
||
if [[ -n "$emmc_device" ]]; then
|
||
local emmc_size
|
||
emmc_size=$(lsblk -d -n -o SIZE "/dev/$emmc_device" 2>/dev/null | xargs)
|
||
printf "│ ┌─────────────────────────────────────────────────────┐ │\n"
|
||
printf "│ │ Onboard eMMC: %-10s (%s) │ │\n" "$emmc_device" "$emmc_size"
|
||
printf "│ └─────────────────────────────────────────────────────┘ │\n"
|
||
printf "│ │\n"
|
||
fi
|
||
|
||
printf "│ SATA Ports (rear): │\n"
|
||
printf "│ ┌──────────────┐ ┌──────────────┐ │\n"
|
||
printf "│ │ 1: %-9s │ │ 2: %-9s │ │\n" "${DRIVE_MAP[1]:-EMPTY}" "${DRIVE_MAP[2]:-EMPTY}"
|
||
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
|
||
#------------------------------------------------------------------------------
|
||
# ══ driveAtlas :: color + glyph helpers ═══════════════════════════════════════
|
||
|
||
# da_repeat GLYPH N -- print GLYPH N times (multibyte-safe)
|
||
da_repeat() {
|
||
local out='' i
|
||
for ((i = 0; i < $2; i++)); do out+="$1"; done
|
||
printf '%s' "$out"
|
||
}
|
||
|
||
# da_color_init -- set the driveAtlas "hardware schematic" palette globals.
|
||
# Structure is steel grey; color encodes STATE only (green=ok, amber/yellow,
|
||
# red=hot, blue=cool). All vars are empty strings when color is off, so
|
||
# rendering is byte-identical to pure monochrome.
|
||
# USE_COLOR=true force on (depth still auto-picked)
|
||
# USE_COLOR=false force off
|
||
# unset/auto auto: off if NO_COLOR set, stdout not a tty,
|
||
# or terminal reports < 256 colors
|
||
# Depth: truecolor only when COLORTERM=truecolor/24bit, else 256, else 16.
|
||
da_color_init() {
|
||
DA_ST='' DA_HL='' DA_DIM='' DA_OK='' DA_YEL='' DA_HOT='' DA_BLU='' DA_CYN=''
|
||
DA_SR='' DA_RST=''
|
||
local mode="${USE_COLOR:-auto}" ncolors
|
||
ncolors="$(tput colors 2>/dev/null)"; ncolors="${ncolors:-0}"
|
||
case "$mode" in
|
||
false) return 0 ;;
|
||
auto)
|
||
[ -n "${NO_COLOR-}" ] && return 0
|
||
[ ! -t 1 ] && return 0
|
||
[ "$ncolors" -lt 256 ] 2>/dev/null && return 0
|
||
;;
|
||
esac
|
||
if [ "${COLORTERM-}" = "truecolor" ] || [ "${COLORTERM-}" = "24bit" ]; then
|
||
DA_ST=$'\033[38;2;138;144;151m' # steel mid #8a9097
|
||
DA_HL=$'\033[38;2;200;205;211m' # steel bright #c8cdd3
|
||
DA_DIM=$'\033[38;2;86;93;102m' # steel shadow #565d66
|
||
DA_OK=$'\033[38;2;51;209;122m' # LED green #33d17a
|
||
DA_YEL=$'\033[38;2;246;196;69m' # LED amber #f6c445
|
||
DA_HOT=$'\033[38;2;255;77;77m' # LED red #ff4d4d
|
||
DA_BLU=$'\033[38;2;77;184;255m' # cool blue #4db8ff
|
||
DA_CYN=$'\033[38;2;77;216;230m' # cyan accent #4dd8e6
|
||
elif [ "$ncolors" -ge 256 ] 2>/dev/null; then
|
||
DA_ST=$'\033[38;5;245m' DA_HL=$'\033[38;5;252m' DA_DIM=$'\033[38;5;238m'
|
||
DA_OK=$'\033[38;5;40m' DA_YEL=$'\033[38;5;214m' DA_HOT=$'\033[38;5;196m'
|
||
DA_BLU=$'\033[38;5;39m' DA_CYN=$'\033[38;5;51m'
|
||
else
|
||
DA_ST=$'\033[37m' DA_HL=$'\033[1;37m' DA_DIM=$'\033[90m'
|
||
DA_OK=$'\033[32m' DA_YEL=$'\033[33m' DA_HOT=$'\033[31m'
|
||
DA_BLU=$'\033[34m' DA_CYN=$'\033[36m'
|
||
fi
|
||
DA_RST=$'\033[0m'
|
||
DA_SR="${DA_RST}${DA_ST}" # "return to structure": reset attrs, steel mid
|
||
}
|
||
|
||
# da_gauge PCT WIDTH -- smooth eighth-block capacity gauge over a dim track,
|
||
# e.g. "da_gauge 62 15" -> "████████▋░░░░░░". Color it by level at call site
|
||
# (DA_OK < 70, DA_YEL < 90, DA_HOT >= 90); pad-then-color as usual.
|
||
da_gauge() {
|
||
local pct="${1:-0}" w="${2:-10}" full rem cells out='' i
|
||
local eighth=('' '▏' '▎' '▍' '▌' '▋' '▊' '▉')
|
||
[ "$pct" -lt 0 ] 2>/dev/null && pct=0
|
||
[ "$pct" -gt 100 ] 2>/dev/null && pct=100
|
||
full=$(( pct * w / 100 )); rem=$(( (pct * w * 8 / 100) % 8 )); cells=$full
|
||
for ((i = 0; i < full; i++)); do out+='█'; done
|
||
if [ "$rem" -gt 0 ] && [ "$full" -lt "$w" ]; then
|
||
out+="${eighth[rem]}"; cells=$((cells + 1))
|
||
fi
|
||
for ((i = cells; i < w; i++)); do out+='░'; done
|
||
printf '%s' "$out"
|
||
}
|
||
|
||
# ══ driveAtlas :: NUC5i5RYB/RYH chassis + drive-slot map ══════════════════════
|
||
# 88-col cabinet-projection rendering with live device names from the global
|
||
# associative array DRIVE_MAP (keys: int-os usb-fl usb-fr usb-rt usb-rb).
|
||
# Occupied ports: solid light boxes, green LED bar + device. EMPTY ports:
|
||
# dashed ghost boxes, dim. Alignment: every variable field is padded to its
|
||
# final width BEFORE color codes wrap it; borders are literal strings.
|
||
#
|
||
# The art printfs embed our own ANSI colour vars (DA_*) and literal box-drawing
|
||
# in the format string by design; they contain no '%' and no user data (device
|
||
# names are passed as %s args), so SC2059 does not apply here.
|
||
# shellcheck disable=SC2059
|
||
generate_nuc_layout() {
|
||
build_drive_map
|
||
local host="${1:-unknown}"
|
||
local model=""
|
||
if command -v maybe_sudo >/dev/null 2>&1; then
|
||
model="$(maybe_sudo dmidecode -s baseboard-product-name 2>/dev/null | head -n1)"
|
||
fi
|
||
[ -n "$model" ] || model="NUC5i5RYB/RYH"
|
||
|
||
da_color_init
|
||
|
||
# fixed-width header fields (pad/truncate so the frame never moves)
|
||
local hostp modelp
|
||
printf -v hostp '%-22.22s' "$host"
|
||
printf -v modelp '%-14.14s' "$model"
|
||
|
||
# per-USB-slot pieces: device field (6 cols: LED bar + 5-char name),
|
||
# box side glyph, label color, border dash -- ghost styling for EMPTY
|
||
local fl fr rt rb io fls frs rts rbs flc frc rtc rbc fld frd rtd rbd
|
||
local flt frt flb frb rtt rbb
|
||
local pre key col v p
|
||
for pre in fl:usb-fl:B fr:usb-fr:Y rt:usb-rt:B rb:usb-rb:B; do
|
||
key="${pre#*:}"; key="${key%:*}"; col="${pre##*:}"; pre="${pre%%:*}"
|
||
v="${DRIVE_MAP[$key]:-EMPTY}"
|
||
if [ "$v" = "EMPTY" ]; then
|
||
printf -v p ' %-5.5s' "$v"
|
||
printf -v "$pre" '%s' "${DA_DIM}${p}${DA_SR}"
|
||
printf -v "${pre}s" '%s' "${DA_DIM}╎${DA_SR}"
|
||
printf -v "${pre}c" '%s' "${DA_DIM}"
|
||
printf -v "${pre}d" '%s' '╌'
|
||
else
|
||
printf -v p '%-5.5s' "$v"
|
||
printf -v "$pre" '%s' "${DA_OK}▌${p}${DA_SR}"
|
||
printf -v "${pre}s" '%s' '│'
|
||
[ "$col" = "Y" ] && printf -v "${pre}c" '%s' "${DA_YEL}" \
|
||
|| printf -v "${pre}c" '%s' "${DA_BLU}"
|
||
printf -v "${pre}d" '%s' '─'
|
||
fi
|
||
done
|
||
# box edges (11 cols; front tops carry the callout stem junction)
|
||
flt="$(da_repeat "$fld" 5)┴$(da_repeat "$fld" 5)"
|
||
frt="┴$(da_repeat "$frd" 10)"
|
||
flb="$(da_repeat "$fld" 11)"; frb="$(da_repeat "$frd" 11)"
|
||
rtt="$(da_repeat "$rtd" 11)"; rbb="$(da_repeat "$rbd" 11)"
|
||
[ "$fld" = '╌' ] && { flt="${DA_DIM}${flt}${DA_SR}"; flb="${DA_DIM}${flb}${DA_SR}"; }
|
||
[ "$frd" = '╌' ] && { frt="${DA_DIM}${frt}${DA_SR}"; frb="${DA_DIM}${frb}${DA_SR}"; }
|
||
[ "$rtd" = '╌' ] && rtt="${DA_DIM}${rtt}${DA_SR}"
|
||
[ "$rbd" = '╌' ] && rbb="${DA_DIM}${rbb}${DA_SR}"
|
||
# internal 2.5" SATA (int-os)
|
||
v="${DRIVE_MAP[int-os]:-EMPTY}"
|
||
if [ "$v" = "EMPTY" ]; then
|
||
printf -v p ' %-5.5s' "$v"; io="${DA_DIM}${p}${DA_SR}"
|
||
else
|
||
printf -v p '%-5.5s' "$v"; io="${DA_OK}▌${p}${DA_SR}"
|
||
fi
|
||
|
||
printf "${DA_ST}${DA_HL}┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓${DA_SR}${DA_RST}\n"
|
||
printf "${DA_ST}${DA_HL}┃${DA_SR} ${DA_CYN}driveAtlas${DA_SR} :: ${DA_HL}%s${DA_SR} %s - 115x111x49 mm - tall (RYH) kit ${DA_HL}┃${DA_SR}${DA_RST}\n" "$hostp" "$modelp"
|
||
printf "${DA_ST}${DA_HL}┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛${DA_SR}${DA_RST}\n"
|
||
printf " \n"
|
||
printf "${DA_ST} FRONT 3/4 ── cool intake side REAR ── hot exhaust side ${DA_RST}\n"
|
||
printf " \n"
|
||
printf "${DA_ST} ${DA_DIM}▄▄▄▄▄▄ ▄▄▄▄▄▄${DA_SR} < CPU fan vents ${DA_RST}\n"
|
||
printf "${DA_ST} ${DA_HOT} v v v hot air${DA_SR} ${DA_RST}\n"
|
||
printf "${DA_ST} ${DA_HL}┌──────────────────────────┐${DA_SR} ${DA_HL}╭────────────────────────────────╮${DA_SR} ${DA_RST}\n"
|
||
printf "${DA_ST} ${DA_HL}╱${DA_SR}${DA_DIM}░░░─░░░░░░░─░░░░░░░░─░░░░░${DA_SR}${DA_HL}╱│${DA_SR} ${DA_HL}│${DA_SR} ┌%s┐ ${DA_HL}│${DA_SR} ${DA_RST}\n" "$rtt"
|
||
printf "${DA_ST} ${DA_HL}╱${DA_SR}${DA_DIM}░${DA_SR}${DA_OK}(o)${DA_SR} pwr ${DA_DIM}░░─░░░░░─░░░░░░░─${DA_SR}${DA_HL}╱${DA_SR}${DA_DIM}▒${DA_SR}${DA_HL}│${DA_SR} ${DA_HL}│${DA_SR} (o) ▐═▌ ▐╬▌ %s %s3-4 BLUE${DA_SR} %s ▐▄▌ ${DA_HL}│${DA_SR} ${DA_RST}\n" "$rts" "$rtc" "$rts"
|
||
printf "${DA_ST} ${DA_HL}┌──────────────────────────┐${DA_SR}${DA_DIM}▒▒${DA_SR}${DA_HL}│${DA_SR} ${DA_HL}│${DA_SR} DC mDP GbE %s %s %smHDMI${DA_HL}│${DA_SR} ${DA_RST}\n" "$rts" "$rt" "$rts"
|
||
printf "${DA_ST} ${DA_HL}│${DA_SR} ╌ brushed aluminum band ╌${DA_HL}│${DA_SR}${DA_DIM}▒${DA_SR}${DA_HL}╱${DA_SR} ${DA_HL}│${DA_SR} ├───────────┤ ${DA_HL}│${DA_SR} ${DA_RST}\n"
|
||
printf "${DA_ST} ${DA_HL}│${DA_SR} ${DA_BLU}▐███▌${DA_SR} ${DA_YEL}▐███▌${DA_SR} (o) . ${DA_HL}│╱${DA_SR} ${DA_HL}│${DA_SR} %s %s3-3 BLUE${DA_SR} %s ${DA_HL}│${DA_SR}${DA_HOT}< bakes in${DA_SR} ${DA_RST}\n" "$rbs" "$rbc" "$rbs"
|
||
printf "${DA_ST} ${DA_HL}└────┬───────┬─────────────┘${DA_SR} ${DA_HL}│${DA_SR} %s %s %s ${DA_HL}│${DA_SR}${DA_HOT} CPU exhaust${DA_SR}${DA_RST}\n" "$rbs" "$rb" "$rbs"
|
||
printf "${DA_ST} │ │ audio IR ${DA_HL}│${DA_SR} └%s┘ ${DA_HL}│${DA_SR} ${DA_RST}\n" "$rbb"
|
||
printf "${DA_ST} ┌%s┐┌%s┐ ${DA_HL}╰────────────────────────────────╯${DA_SR} ${DA_RST}\n" "$flt" "$frt"
|
||
printf "${DA_ST} %s %s3-2 BLUE${DA_SR} %s%s%s3-1? YELLOW${DA_SR}%s ${DA_DIM}▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀${DA_SR} ${DA_RST}\n" "$fls" "$flc" "$fls" "$frs" "$frc" "$frs"
|
||
printf "${DA_ST} %s %s %s%s %s %s ${DA_RST}\n" "$fls" "$fl" "$fls" "$frs" "$fr" "$frs"
|
||
printf "${DA_ST} %s USB3 data %s%s charging %s ${DA_RST}\n" "$fls" "$fls" "$frs" "$frs"
|
||
printf "${DA_ST} └%s┘└%s┘ ${DA_RST}\n" "$flb" "$frb"
|
||
printf " \n"
|
||
printf "${DA_ST} ${DA_HL}UNDERSIDE / INSIDE${DA_SR} ${DA_HL}NOTES${DA_SR} ${DA_RST}\n"
|
||
printf "${DA_ST} ┌──────────────────────────────────┐ ${DA_RST}\n"
|
||
printf "${DA_ST} │ internal 2.5\" SATA ── mounted │ - all 4 USB3 jacks hang off ONE xHCI ${DA_RST}\n"
|
||
printf "${DA_ST} │ INVERTED in the bottom lid │ controller ── one shared kernel bus ${DA_RST}\n"
|
||
printf "${DA_ST} │ %s < int-os - OS / boot │ - M.2 2280 slot ── flat on mainboard ${DA_RST}\n" "$io"
|
||
printf "${DA_ST} │${DA_DIM} ╌╌╌╌╌ flex cable to mainboard ╌╌ ${DA_SR}│ - \"?\" = port 3-1 id inferred (unverified) ${DA_RST}\n"
|
||
printf "${DA_ST} └──────────────────────────────────┘ ${DA_RST}\n"
|
||
printf "${DA_ST} ${DA_DIM}o feet o────── VESA ──────o feet o${DA_SR} ${DA_RST}\n"
|
||
|
||
# optional flourish: controller inventory from the existing helper
|
||
if command -v get_storage_controllers >/dev/null 2>&1; then
|
||
printf '\n %sSTORAGE CONTROLLERS%s\n' "${DA_HL}" "${DA_RST}"
|
||
get_storage_controllers 2>/dev/null | sed "s/^/ /"
|
||
fi
|
||
}
|
||
|
||
#------------------------------------------------------------------------------
|
||
# generate_large1_layout
|
||
#
|
||
# Generates ASCII art representation of a large1 chassis (Rosewill RSV-L4500U).
|
||
# Shows storage controllers, 2 M.2 NVMe slots, and 15 front bays in 3x5 grid.
|
||
#
|
||
# Args:
|
||
# $1 - Hostname to display in the layout header
|
||
#
|
||
# Side effects: Calls build_drive_map() to populate DRIVE_MAP
|
||
#------------------------------------------------------------------------------
|
||
generate_large1_layout() {
|
||
local hostname="$1"
|
||
build_drive_map
|
||
|
||
# large1 has 3 stacks of 5 bays at front (15 total) + 2 M.2 slots
|
||
# Physical bay mapping TBD - current mapping is by controller order
|
||
printf "┌─────────────────────────────────────────────────────────────────────────┐\n"
|
||
printf "│ %-69s │\n" "$hostname - Rosewill RSV-L4500U (15x 3.5\" Bays)"
|
||
printf "│ │\n"
|
||
printf "│ Storage Controllers: │\n"
|
||
while IFS= read -r ctrl; do
|
||
[[ -n "$ctrl" ]] && printf "│ %-69.69s│\n" "$ctrl"
|
||
done < <(get_storage_controllers)
|
||
printf "│ │\n"
|
||
printf "│ M.2 NVMe: M1: %-10s M2: %-10s │\n" "${DRIVE_MAP[m2-1]:-EMPTY}" "${DRIVE_MAP[m2-2]:-EMPTY}"
|
||
printf "│ │\n"
|
||
printf "│ Front Bays (3 stacks x 5 rows): [slot 5 of each stack MUST stay empty] │\n"
|
||
printf "│ Stack A Stack B Stack C │\n"
|
||
printf "│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │\n"
|
||
printf "│ │1:%-8s│ │2:%-8s│ │3:%-8s│ │\n" "${DRIVE_MAP[1]:-EMPTY}" "${DRIVE_MAP[2]:-EMPTY}" "${DRIVE_MAP[3]:-EMPTY}"
|
||
printf "│ ├──────────┤ ├──────────┤ ├──────────┤ │\n"
|
||
printf "│ │4:%-8s│ │5:%-8s│ │6:%-8s│ │\n" "${DRIVE_MAP[4]:-EMPTY}" "${DRIVE_MAP[5]:-EMPTY}" "${DRIVE_MAP[6]:-EMPTY}"
|
||
printf "│ ├──────────┤ ├──────────┤ ├──────────┤ │\n"
|
||
printf "│ │7:%-8s│ │8:%-8s│ │9:%-8s│ │\n" "${DRIVE_MAP[7]:-EMPTY}" "${DRIVE_MAP[8]:-EMPTY}" "${DRIVE_MAP[9]:-EMPTY}"
|
||
printf "│ ├──────────┤ ├──────────┤ ├──────────┤ │\n"
|
||
printf "│ │10:%-7s│ │11:%-7s│ │12:%-7s│ │\n" "${DRIVE_MAP[10]:-EMPTY}" "${DRIVE_MAP[11]:-EMPTY}" "${DRIVE_MAP[12]:-EMPTY}"
|
||
printf "│ ├──────────┤ ├──────────┤ ├──────────┤ │\n"
|
||
printf "│ │13:%-7s│ │14:%-7s│ │15:%-7s│ │\n" "${DRIVE_MAP[13]:-EMPTY}" "${DRIVE_MAP[14]:-EMPTY}" "${DRIVE_MAP[15]:-EMPTY}"
|
||
printf "│ └──────────┘ └──────────┘ └──────────┘ │\n"
|
||
printf "└─────────────────────────────────────────────────────────────────────────┘\n"
|
||
}
|
||
|
||
#------------------------------------------------------------------------------
|
||
# Server-Specific Drive Mappings
|
||
# Maps PCI paths to physical bay numbers for each server
|
||
# Format: "pci-path bay-number"
|
||
#------------------------------------------------------------------------------
|
||
|
||
declare -A SERVER_MAPPINGS=(
|
||
# compute-storage-01 (formerly medium2)
|
||
# Motherboard: B650D4U3-2Q/BCM with AMD SATA controller
|
||
# HBA: LSI SAS3008 at 01:00.0 (mini-SAS HD ports)
|
||
# Cable mapping from user notes:
|
||
# - Mobo SATA: top-right=bay1, bottom-right=bay2, bottom-left=bay3, top-left=bay4
|
||
# - HBA bottom mini-SAS: bays 5,6,7,8
|
||
# - HBA top mini-SAS: bays 9,10
|
||
["compute-storage-01"]="
|
||
pci-0000:0d:00.0-ata-2 1
|
||
pci-0000:0d:00.0-ata-1 2
|
||
pci-0000:0d:00.0-ata-3 3
|
||
pci-0000:0d:00.0-ata-4 4
|
||
pci-0000:01:00.0-sas-phy6-lun-0 5
|
||
pci-0000:01:00.0-sas-phy7-lun-0 6
|
||
pci-0000:01:00.0-sas-phy5-lun-0 7
|
||
pci-0000:01:00.0-sas-phy2-lun-0 8
|
||
pci-0000:01:00.0-sas-phy4-lun-0 9
|
||
pci-0000:01:00.0-sas-phy3-lun-0 10
|
||
pci-0000:0e:00.0-nvme-1 m2-1
|
||
"
|
||
|
||
# compute-storage-gpu-01
|
||
# Motherboard: ASUS PRIME B550-PLUS with AMD SATA controller at 05:00.1
|
||
# SATA ports run in reverse order for hot-swap bays (ata-5=bay2 .. ata-2=bay5)
|
||
# ata-1 is the boot SSD sitting loose inside the chassis (not in a hot-swap bay)
|
||
# sdf is USB/card reader - not mapped
|
||
["compute-storage-gpu-01"]="
|
||
pci-0000:05:00.1-ata-4 2
|
||
pci-0000:05:00.1-ata-5 3
|
||
pci-0000:05:00.1-ata-3 4
|
||
pci-0000:05:00.1-ata-2 5
|
||
pci-0000:05:00.1-ata-1 int-1
|
||
pci-0000:0c:00.0-nvme-1 m2-1
|
||
"
|
||
|
||
# storage-01
|
||
# Motherboard: ASRock A320M-HDV R4.0
|
||
# AMD SATA controller at 02:00.1 (bays 1-4)
|
||
# Mobo SATA physical layout:
|
||
# top-left=bay 1, bottom-left=bay 2, top-right=bay 3, bottom-right=bay 4
|
||
# HBA: LSI SAS3416 at 01:00.0 (4x Mini-SAS HD ports, top=C0 to bottom=C3)
|
||
# C0 (top): 4x SATA breakout → bays 5-8
|
||
# C1: 4x SATA breakout → bays 9-10 (2 of 4 ports used)
|
||
# C2: U.2 NVMe (serial ends in 0d66) → u2-1
|
||
# C3: U.2 NVMe (serial ends in 0d4f) → u2-2
|
||
# C0: phy9=bay5 verified; phy11 present (23.6T, bay TBD)
|
||
# C1: phy13=bay7 verified; bays 6,8,9,10 PHY mapping TBD
|
||
# C2: U.2 NVMe (serial ends in 0d66) → u2-1 (needs FW update)
|
||
# C3: U.2 NVMe (serial ends in 0d4f) → u2-2 (needs FW update)
|
||
# Also present: 09:00.0 AMD FCH SATA Controller [AHCI mode]
|
||
["storage-01"]="
|
||
pci-0000:02:00.1-ata-1 1
|
||
pci-0000:02:00.1-ata-2 2
|
||
pci-0000:02:00.1-ata-5 3
|
||
pci-0000:02:00.1-ata-6 4
|
||
pci-0000:01:00.0-sas-phy9-lun-0 5
|
||
pci-0000:01:00.0-sas-phy13-lun-0 7
|
||
"
|
||
|
||
# large1
|
||
# 3 stacks (A/B/C left-to-right) x 5 slots (1-5 top-to-bottom) = 15 bays
|
||
# Bay numbering: A1=1, B1=2, C1=3, A2=4, B2=5, C2=6, A3=7, B3=8, C3=9, ...
|
||
#
|
||
# *** SLOT 5 IN ALL STACKS MUST REMAIN EMPTY ***
|
||
# *** Drives are near-impossible to remove once inserted in slot 5 ***
|
||
# *** B5 (bay 14) already has a permanently stuck drive (WD-CA19PTMK) ***
|
||
# *** C5 (bay 15) has a permanently stuck but unplugged drive (unknown type) ***
|
||
#
|
||
# HBA: LSI SAS2008 at 10:00.0 -- 2x Slim-SAS to 4-port SATA breakout cables
|
||
# Slim-SAS #0 (BLUE cables, connector closest to bracket/mounting edge):
|
||
# Port 1 (phy3) -> C Slot 1 (bay 3) Port 2 (phy2) -> unconnected
|
||
# Port 3 (phy1) -> A Slot 1 (bay 1) Port 4 (phy0) -> A Slot 2 (bay 4)
|
||
# Slim-SAS #1 (SILVER cables, connector furthest from bracket):
|
||
# Port 1 (phy7) -> B Slot 2 (bay 5) -- cable present, no drive yet
|
||
# Port 2 (phy6) -> B Slot 5 (bay 14) -- STUCK, cannot remove
|
||
# Port 3 (phy5) -> B Slot 4 (bay 11)
|
||
# Port 4 (phy4) -> A Slot 3 (bay 7)
|
||
#
|
||
# Motherboard SATA (ORANGE cables) -- 6 ports in 3-column x 2-row cluster:
|
||
# ASMedia 25:00.0 (2 ports):
|
||
# ata-1 -> C Slot 3 (bay 9) [top-left orange port]
|
||
# ata-2 -> B Slot 1 (bay 2) [bottom-left orange port] -- cable present, no drive
|
||
# AMD 16:00.1 (3 of 8 ports connected to bays):
|
||
# ata-3 -> C Slot 4 (bay 12) [bottom-right orange port]
|
||
# ata-7 -> B Slot 3 (bay 8) [bottom-middle orange port] -- cable present, drive removed (was WD-CA2144YK)
|
||
# ata-8 -> C Slot 2 (bay 6) [top-middle orange port] -- cable present, drive removed (was WD-CA28XYHK)
|
||
# AMD FCH 31:00.0 / 31:00.1: no cables attached, ports unused
|
||
["large1"]="
|
||
pci-0000:10:00.0-sas-phy1-lun-0 1
|
||
pci-0000:25:00.0-ata-2 2
|
||
pci-0000:10:00.0-sas-phy3-lun-0 3
|
||
pci-0000:10:00.0-sas-phy0-lun-0 4
|
||
pci-0000:10:00.0-sas-phy7-lun-0 5
|
||
pci-0000:16:00.1-ata-8 6
|
||
pci-0000:10:00.0-sas-phy4-lun-0 7
|
||
pci-0000:16:00.1-ata-7 8
|
||
pci-0000:25:00.0-ata-1 9
|
||
pci-0000:10:00.0-sas-phy5-lun-0 11
|
||
pci-0000:16:00.1-ata-3 12
|
||
pci-0000:10:00.0-sas-phy6-lun-0 14
|
||
pci-0000:2a:00.0-nvme-1 m2-1
|
||
pci-0000:26:00.0-nvme-1 m2-2
|
||
"
|
||
|
||
# micro1 / monitor-02 - ZimaBoard 832 (Apollo Lake N3450), physical layout
|
||
# confirmed on-unit 2026-07-20:
|
||
# FRONT face : video-out (mDP) left, then 2x USB 3.0 (blue), then DC barrel;
|
||
# the 2x GbE sit on an UPPER tier directly ABOVE the two USB ports.
|
||
# LEFT side : open-ended PCIe 2.0 x4 slot (expansion path; no M.2/mSATA).
|
||
# REAR edge : 2x SATA-III (ata1/ata2 via AHCI 00:12.0) with the single
|
||
# mini-4-pin SATA power header in the CENTER between them
|
||
# (2 drives need a Y power cable).
|
||
# Onboard : 32G eMMC (mmcblk0) = OS/boot, soldered (no by-path).
|
||
["micro1"]="
|
||
pci-0000:00:12.0-ata-1 sata-1
|
||
pci-0000:00:12.0-ata-2 sata-2
|
||
"
|
||
["monitor-02"]="
|
||
pci-0000:00:12.0-ata-1 sata-1
|
||
pci-0000:00:12.0-ata-2 sata-2
|
||
"
|
||
|
||
# pbs
|
||
# Intel NUC5i5RYB - Proxmox Backup Server
|
||
# int-os : internal 2.5\" SATA SSD (Samsung MZNLN128) - OS/boot, controller 00:1f.2
|
||
# usb-fl : FRONT-LEFT USB 3.0 (blue) NVMe (Patriot P300 / JMicron bridge) - halfTbNVMeMirror
|
||
# usb-fr : FRONT-RIGHT USB 3.0 (yellow, always-on charging) - normally empty
|
||
# usb-rt : REAR-TOP USB 3.0 (blue) - now empty
|
||
# usb-rb : REAR-BOTTOM USB 3.0 (blue) 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.
|
||
# Port jacks (LED-blink verified 2026-07-15; front-left re-verified 2026-07-18):
|
||
# REAR-TOP jack = port 3-4 (usb-0:4) [blue]
|
||
# REAR-BOTTOM jack = port 3-3 (usb-0:3) [blue] = fw serial P300WCBA24090617536
|
||
# FRONT-LEFT jack = port 3-2 (usb-0:2) [blue] = fw serial P300WCBA24090617615
|
||
# FRONT-RIGHT jack = port 3-1 (usb-0:1) [yellow] (port # unverified - jack empty)
|
||
#
|
||
# 2026-07-18: after a power outage the ...615 drive dropped to 0B / hardware-error in the
|
||
# hot stacked REAR-TOP bay; moving it to FRONT-LEFT (3-2) revived it. Root cause was
|
||
# thermal - the two NVMe USB bridges sat flush against each other in the rear stack.
|
||
["pbs"]="
|
||
pci-0000:00:1f.2-ata-4 int-os
|
||
pci-0000:00:14.0-usb-0:2:1.0-scsi-0:0:0:0 usb-fl
|
||
pci-0000:00:14.0-usb-0:1:1.0-scsi-0:0:0:0 usb-fr
|
||
pci-0000:00:14.0-usb-0:4:1.0-scsi-0:0:0:0 usb-rt
|
||
pci-0000:00:14.0-usb-0:3:1.0-scsi-0:0:0:0 usb-rb
|
||
"
|
||
)
|
||
|
||
declare -A CHASSIS_TYPES=(
|
||
["compute-storage-01"]="10bay"
|
||
["compute-storage-gpu-01"]="10bay"
|
||
["storage-01"]="10bay"
|
||
["large1"]="large1"
|
||
["micro1"]="zimaboard" # ZimaBoard 832
|
||
["monitor-02"]="zimaboard" # ZimaBoard 832
|
||
["pbs"]="nuc" # Intel NUC5i5RYB (1 internal SATA + 2 rear USB-NVMe)
|
||
)
|
||
|
||
#------------------------------------------------------------------------------
|
||
# Core Functions
|
||
#------------------------------------------------------------------------------
|
||
|
||
# Cache for lspci output (populated on first call)
|
||
LSPCI_CACHE=""
|
||
|
||
#------------------------------------------------------------------------------
|
||
# get_storage_controllers
|
||
#
|
||
# Returns a formatted list of storage controllers found via lspci.
|
||
# Uses cached output if available to avoid redundant lspci calls.
|
||
#
|
||
# Output Format: " PCI_ADDR: DESCRIPTION" (one per line)
|
||
#------------------------------------------------------------------------------
|
||
get_storage_controllers() {
|
||
# Cache lspci output on first call
|
||
if [[ -z "$LSPCI_CACHE" ]]; then
|
||
LSPCI_CACHE="$(lspci 2>/dev/null | grep -iE "SAS|SATA|RAID|Mass storage|NVMe")"
|
||
fi
|
||
|
||
# Format and return cached output
|
||
echo "$LSPCI_CACHE" | while read -r line; do
|
||
[[ -z "$line" ]] && continue
|
||
pci_addr="$(echo "$line" | awk '{print $1}')"
|
||
# Get short description (strip PCI address)
|
||
desc="${line#*[0-9a-f:.] }"
|
||
echo " $pci_addr: $desc"
|
||
done
|
||
}
|
||
|
||
#------------------------------------------------------------------------------
|
||
# build_drive_map
|
||
#
|
||
# Builds a global associative array mapping physical bay numbers to device names.
|
||
# Uses PCI paths from SERVER_MAPPINGS to resolve current device assignments.
|
||
#
|
||
# Sets:
|
||
# DRIVE_MAP (global associative array)
|
||
# Keys: Bay identifiers (1, 2, ..., m2-1, m2-2, etc.)
|
||
# Values: Device names (sda, nvme0n1, etc.)
|
||
# BAY_TO_PCI_PATH (global associative array)
|
||
# Keys: Bay identifiers
|
||
# Values: PCI path strings (for --show-pci option)
|
||
#------------------------------------------------------------------------------
|
||
build_drive_map() {
|
||
local host
|
||
host="$(hostname | tr -cd '[:alnum:]-_.')"
|
||
local mapping="${SERVER_MAPPINGS[$host]}"
|
||
|
||
# Declare global arrays directly
|
||
declare -g -A DRIVE_MAP=()
|
||
declare -g -A BAY_TO_PCI_PATH=()
|
||
|
||
if [[ -z "$mapping" ]]; then
|
||
log_warn "No drive mapping found for host '$host'. Run with --diagnose to gather PCI path info."
|
||
return
|
||
fi
|
||
|
||
local mapped_count=0
|
||
local empty_count=0
|
||
while read -r path slot; do
|
||
[[ -z "$path" || -z "$slot" ]] && continue
|
||
|
||
BAY_TO_PCI_PATH[$slot]="$path"
|
||
if [[ -L "${DISK_BY_PATH}/$path" ]]; then
|
||
local drive
|
||
drive="$(readlink -f "${DISK_BY_PATH}/$path" | sed 's/.*\///')"
|
||
DRIVE_MAP[$slot]="$drive"
|
||
((mapped_count++))
|
||
else
|
||
log_info "Bay $slot: No device at PCI path $path"
|
||
((empty_count++))
|
||
fi
|
||
done <<< "$mapping"
|
||
|
||
log_info "Mapped $mapped_count drives, $empty_count empty bays"
|
||
}
|
||
|
||
#------------------------------------------------------------------------------
|
||
# build_ceph_cache
|
||
#
|
||
# Queries Ceph once and builds lookup tables for OSD information.
|
||
# This is much more efficient than querying ceph-volume per device.
|
||
#
|
||
# Sets global associative arrays:
|
||
# CEPH_DEVICE_TO_OSD - Maps device names to OSD IDs (e.g., sda -> osd.5)
|
||
# CEPH_OSD_STATUS - Maps OSD numbers to up/down status
|
||
# CEPH_OSD_IN - Maps OSD numbers to in/out status
|
||
#------------------------------------------------------------------------------
|
||
build_ceph_cache() {
|
||
declare -g -A CEPH_DEVICE_TO_OSD=()
|
||
declare -g -A CEPH_OSD_STATUS=()
|
||
declare -g -A CEPH_OSD_IN=()
|
||
|
||
# Skip if ceph-volume is not available
|
||
if ! command -v ceph-volume &>/dev/null; then
|
||
log_info "ceph-volume not found, skipping Ceph OSD detection"
|
||
return
|
||
fi
|
||
|
||
log_info "Querying Ceph OSD information..."
|
||
|
||
# Parse ceph-volume lvm list output
|
||
# Format: blocks starting with "====== osd.X =======" followed by device info
|
||
local current_osd=""
|
||
local osd_count=0
|
||
while IFS= read -r line; do
|
||
# Match OSD header: "====== osd.5 =======" or "====== osd.19 ======"
|
||
# Number of trailing equals varies based on OSD number length
|
||
if [[ "$line" =~ ======[[:space:]]+osd\.([0-9]+)[[:space:]]+====== ]]; then
|
||
current_osd="osd.${BASH_REMATCH[1]}"
|
||
# Match "devices" line which has the actual physical device: " devices /dev/sda"
|
||
# This is more reliable than "block device" which may show LVM paths
|
||
elif [[ -n "$current_osd" && "$line" =~ devices[[:space:]]+/dev/(sd[a-z]+|nvme[0-9]+n[0-9]+) ]]; then
|
||
local dev_name="${BASH_REMATCH[1]}"
|
||
CEPH_DEVICE_TO_OSD["$dev_name"]="$current_osd"
|
||
((osd_count++))
|
||
log_info "Found $current_osd on $dev_name"
|
||
current_osd="" # Reset to avoid duplicate matches
|
||
fi
|
||
done < <(ceph-volume lvm list 2>/dev/null)
|
||
log_info "Cached $osd_count Ceph OSDs"
|
||
|
||
# Skip if ceph command is not available
|
||
if ! command -v ceph &>/dev/null; then
|
||
log_info "ceph CLI not found, skipping OSD status detection"
|
||
return
|
||
fi
|
||
|
||
log_info "Querying Ceph OSD status..."
|
||
|
||
# Parse ceph osd tree for status
|
||
# Format: ID CLASS WEIGHT TYPE NAME STATUS REWEIGHT
|
||
while IFS= read -r line; do
|
||
# Match OSD lines: " 5 hdd 3.63660 osd.5 up 1.00000"
|
||
if [[ "$line" =~ ^[[:space:]]*([0-9]+)[[:space:]]+.*osd\.([0-9]+)[[:space:]]+(up|down)[[:space:]]+([0-9.]+) ]]; then
|
||
local osd_num="${BASH_REMATCH[1]}"
|
||
local status="${BASH_REMATCH[3]}"
|
||
local reweight="${BASH_REMATCH[4]}"
|
||
|
||
CEPH_OSD_STATUS[$osd_num]="$status"
|
||
|
||
# Determine in/out based on reweight
|
||
if awk "BEGIN {exit !($reweight > 0)}"; then
|
||
CEPH_OSD_IN[$osd_num]="in"
|
||
else
|
||
CEPH_OSD_IN[$osd_num]="out"
|
||
fi
|
||
fi
|
||
done < <(ceph osd tree 2>/dev/null)
|
||
}
|
||
|
||
# SMART warning thresholds
|
||
readonly SMART_TEMP_WARN=50 # Temperature warning threshold (°C)
|
||
readonly SMART_TEMP_CRIT=60 # Temperature critical threshold (°C)
|
||
readonly SMART_REALLOCATED_WARN=1 # Reallocated sectors warning threshold
|
||
readonly SMART_PENDING_WARN=1 # Pending sectors warning threshold
|
||
readonly SMART_CRC_ERROR_WARN=100 # UDMA CRC error warning threshold
|
||
readonly SMART_POWER_ON_HOURS_WARN=43800 # ~5 years of continuous use
|
||
|
||
#------------------------------------------------------------------------------
|
||
# parse_smart_data
|
||
#
|
||
# Parses raw SMART data and returns formatted info string.
|
||
#
|
||
# Args:
|
||
# $1 - Device name (e.g., sda, nvme0n1)
|
||
# $2 - Raw smartctl output string
|
||
#
|
||
# Returns: Pipe-delimited string: TYPE|TEMP|HEALTH|MODEL|SERIAL|WARNINGS
|
||
#------------------------------------------------------------------------------
|
||
parse_smart_data() {
|
||
local device="$1"
|
||
local smart_info="$2"
|
||
local temp="-"
|
||
local type="HDD"
|
||
local health="✗"
|
||
local model="-"
|
||
local serial="-"
|
||
local warnings=""
|
||
|
||
if [[ -z "$smart_info" ]]; then
|
||
echo "HDD|-|✗|-|-|"
|
||
return
|
||
fi
|
||
|
||
# Temperature parsing - handles multiple formats:
|
||
# - SATA: "194 Temperature_Celsius ... 26 (0 14 0 0 0)" (value before parenthetical)
|
||
# - SATA: "Temperature: 42 Celsius"
|
||
# - SATA: "Current Temperature: 35 Celsius"
|
||
# - SAS: "Current Drive Temperature: 35 C"
|
||
# - NVMe: "Temperature: 42 Celsius"
|
||
if echo "$smart_info" | grep -q "Temperature_Celsius"; then
|
||
# Strip parenthetical data like "(0 14 0 0 0)" before finding last number
|
||
temp="$(echo "$smart_info" | grep "Temperature_Celsius" | head -1 | sed 's/([^)]*)//g' | awk '{for(i=NF;i>0;i--) if($i ~ /^[0-9]+$/) {print $i; exit}}')"
|
||
elif echo "$smart_info" | grep -qE "Current Drive Temperature:"; then
|
||
# SAS drives: "Current Drive Temperature: 35 C"
|
||
temp="$(echo "$smart_info" | grep -E "Current Drive Temperature:" | head -1 | awk '{for(i=1;i<=NF;i++) if($i ~ /^[0-9]+$/) {print $i; exit}}')"
|
||
elif echo "$smart_info" | grep -qE "(Current )?Temperature:"; then
|
||
# SATA/NVMe: "Temperature: 42 Celsius" (may have leading whitespace)
|
||
temp="$(echo "$smart_info" | grep -E "(Current )?Temperature:" | head -1 | awk '{for(i=1;i<=NF;i++) if($i ~ /^[0-9]+$/) {print $i; exit}}')"
|
||
fi
|
||
|
||
# Device type detection - handles SSD, HDD, and NVMe
|
||
# 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
|
||
rotation_rate="$(echo "$smart_info" | grep -E "Rotation Rate:" | head -1)"
|
||
if echo "$rotation_rate" | grep -qiE "solid state"; then
|
||
type="SSD"
|
||
elif echo "$rotation_rate" | grep -qE "[0-9]+ rpm"; then
|
||
# Has actual RPM value (e.g., "7200 rpm") - it's an HDD
|
||
type="HDD"
|
||
else
|
||
# Unknown rotation rate, default to HDD
|
||
type="HDD"
|
||
fi
|
||
elif echo "$smart_info" | grep -qE "Device Model:.*SSD|Model Number:.*SSD"; then
|
||
# Match SSD in the model name field
|
||
type="SSD"
|
||
else
|
||
# Default to HDD for spinning rust
|
||
type="HDD"
|
||
fi
|
||
|
||
# Health status (basic SMART check)
|
||
if echo "$smart_info" | grep -q "SMART overall-health.*PASSED"; then
|
||
health="✓"
|
||
elif echo "$smart_info" | grep -q "SMART Health Status.*OK"; then
|
||
# NVMe format
|
||
health="✓"
|
||
fi
|
||
|
||
# Model - try multiple field names
|
||
model="$(echo "$smart_info" | grep -E "^(Device Model|Model Number|Product):" | head -1 | cut -d: -f2 | xargs)"
|
||
[[ -z "$model" ]] && model="-"
|
||
|
||
# Serial number - capture everything after the colon to handle spaces
|
||
serial="$(echo "$smart_info" | grep -E "^Serial [Nn]umber:" | head -1 | cut -d: -f2 | xargs)"
|
||
[[ -z "$serial" ]] && serial="-"
|
||
|
||
# SMART threshold warnings - check for concerning values
|
||
local warn_list=()
|
||
|
||
# Temperature thresholds
|
||
if [[ -n "$temp" && "$temp" =~ ^[0-9]+$ ]]; then
|
||
if [[ "$temp" -ge "$SMART_TEMP_CRIT" ]]; then
|
||
warn_list+=("TEMP_CRIT")
|
||
elif [[ "$temp" -ge "$SMART_TEMP_WARN" ]]; then
|
||
warn_list+=("TEMP_WARN")
|
||
fi
|
||
fi
|
||
|
||
# Reallocated sectors (SMART attribute 5)
|
||
local reallocated
|
||
reallocated="$(echo "$smart_info" | grep -E "^\s*5\s+Reallocated_Sector" | awk '{print $NF}')"
|
||
if [[ -n "$reallocated" && "$reallocated" =~ ^[0-9]+$ && "$reallocated" -ge "$SMART_REALLOCATED_WARN" ]]; then
|
||
warn_list+=("REALLOC:$reallocated")
|
||
fi
|
||
|
||
# Current pending sectors (SMART attribute 197)
|
||
local pending
|
||
pending="$(echo "$smart_info" | grep -E "^\s*197\s+Current_Pending" | awk '{print $NF}')"
|
||
if [[ -n "$pending" && "$pending" =~ ^[0-9]+$ && "$pending" -ge "$SMART_PENDING_WARN" ]]; then
|
||
warn_list+=("PENDING:$pending")
|
||
fi
|
||
|
||
# UDMA CRC errors (SMART attribute 199)
|
||
local crc_errors
|
||
crc_errors="$(echo "$smart_info" | grep -E "^\s*199\s+UDMA_CRC_Error" | awk '{print $NF}')"
|
||
if [[ -n "$crc_errors" && "$crc_errors" =~ ^[0-9]+$ && "$crc_errors" -ge "$SMART_CRC_ERROR_WARN" ]]; then
|
||
warn_list+=("CRC:$crc_errors")
|
||
fi
|
||
|
||
# Power-on hours (SMART attribute 9)
|
||
local power_hours
|
||
power_hours="$(echo "$smart_info" | grep -E "^\s*9\s+Power_On_Hours" | awk '{print $NF}')"
|
||
if [[ -n "$power_hours" && "$power_hours" =~ ^[0-9]+$ && "$power_hours" -ge "$SMART_POWER_ON_HOURS_WARN" ]]; then
|
||
warn_list+=("HOURS:$power_hours")
|
||
fi
|
||
|
||
# Join warnings
|
||
if [[ ${#warn_list[@]} -gt 0 ]]; then
|
||
warnings="$(IFS=','; echo "${warn_list[*]}")"
|
||
# Change health indicator to warning if SMART passed but has warnings
|
||
if [[ "$health" == "✓" ]]; then
|
||
health="⚠"
|
||
fi
|
||
fi
|
||
|
||
# Format temperature with unit if we have a value
|
||
local temp_display
|
||
if [[ -n "$temp" && "$temp" != "-" ]]; then
|
||
temp_display="${temp}°C"
|
||
else
|
||
temp_display="-"
|
||
fi
|
||
|
||
echo "${type}|${temp_display}|${health}|${model}|${serial}|${warnings}"
|
||
}
|
||
|
||
#------------------------------------------------------------------------------
|
||
# get_drive_smart_info
|
||
#
|
||
# Retrieves SMART data for a given device (fetches and parses).
|
||
#
|
||
# Args:
|
||
# $1 - Device name (e.g., sda, nvme0n1)
|
||
#
|
||
# Returns: Pipe-delimited string: TYPE|TEMP|HEALTH|MODEL|SERIAL|WARNINGS
|
||
#------------------------------------------------------------------------------
|
||
get_drive_smart_info() {
|
||
local device="$1"
|
||
local smart_info
|
||
|
||
smart_info="$(maybe_sudo smartctl -A -i -H "/dev/$device" 2>/dev/null)"
|
||
parse_smart_data "$device" "$smart_info"
|
||
}
|
||
|
||
#------------------------------------------------------------------------------
|
||
# Main Display Logic
|
||
#------------------------------------------------------------------------------
|
||
|
||
# Run diagnose mode if requested (exits after printing)
|
||
if [[ "$RUN_DIAGNOSE" == true ]]; then
|
||
run_diagnose
|
||
fi
|
||
|
||
#------------------------------------------------------------------------------
|
||
# generate_zimaboard_layout
|
||
#
|
||
# ASCII art for a ZimaBoard 832 (micro1 / monitor-02): fanless x86 SBC (Celeron
|
||
# N3450) whose entire lid is a finned aluminium heatsink. Physical layout
|
||
# confirmed against the actual unit:
|
||
# TOP : corrugated heatsink fins, rectangular notch at the FRONT-LEFT,
|
||
# logo badge inset into the front bevel.
|
||
# FRONT face : two tiers - 2x GbE sit directly ABOVE the 2x USB 3.0; the
|
||
# Mini-DisplayPort is LEFT of that block, DC 12V barrel is RIGHT.
|
||
# LEFT side : open-ended PCIe 2.0 x4 card edge protruding out the side.
|
||
# REAR edge : 2x SATA data with the single SATA power header in the CENTRE
|
||
# (SATA | PWR | SATA); no drive bay - drives cable out and lie
|
||
# beside the board, and two drives need a Y power cable.
|
||
# Onboard : 32G eMMC (mmcblk0) = OS/boot, soldered (never in DRIVE_MAP).
|
||
# Bays: sata-1, sata-2 (+ a direct /dev/mmcblk0 probe for the eMMC).
|
||
#
|
||
# Renders onto a fixed 66-column glyph/colour-key canvas and emits with
|
||
# run-length colour, so every line is exactly the same display width in both
|
||
# monochrome and colour. Only SAFE width-1 glyphs (box-drawing + block elements).
|
||
#
|
||
# Args:
|
||
# $1 - Hostname to display in the layout header
|
||
#
|
||
# Side effects: Calls build_drive_map() to populate DRIVE_MAP
|
||
#------------------------------------------------------------------------------
|
||
# shellcheck disable=SC2059
|
||
generate_zimaboard_layout() {
|
||
local host="${1:-$(hostname 2>/dev/null || echo host)}"
|
||
|
||
# Contract order: build the drive map first, then init colours.
|
||
build_drive_map 2>/dev/null || true
|
||
da_color_init
|
||
|
||
# --- storage discovery -------------------------------------------------
|
||
# eMMC (OS/boot) is soldered and never in DRIVE_MAP -> probe it directly.
|
||
local emmc_dev='' emmc_size='' emmc_on=0
|
||
if [ -b /dev/mmcblk0 ]; then
|
||
emmc_on=1; emmc_dev='mmcblk0'
|
||
emmc_size="$(lsblk -dno SIZE /dev/mmcblk0 2>/dev/null | tr -d ' ')"
|
||
[ -n "$emmc_size" ] || emmc_size='?'
|
||
fi
|
||
# Rear SATA ports come from DRIVE_MAP (guard set -u / never-built map).
|
||
local sata1='EMPTY' sata2='EMPTY'
|
||
if declare -p DRIVE_MAP >/dev/null 2>&1; then
|
||
sata1="${DRIVE_MAP[sata-1]:-EMPTY}"
|
||
sata2="${DRIVE_MAP[sata-2]:-EMPTY}"
|
||
fi
|
||
|
||
# --- canvas + geometry -------------------------------------------------
|
||
local WIDTH=66
|
||
local S=5 Wi=44 FL=16 # oblique depth, front interior width, front-left col
|
||
local BL=$((FL-S)) FR=$((FL+Wi+1))
|
||
local BR=$((FR-S)) # NB: separate statement; FR is not visible in its own `local`
|
||
local R0=2 HC=5 FE FB LEG HEIGHT
|
||
FE=$((R0+S)); FB=$((FE+HC+1)); LEG=$((FB+3)); HEIGHT=$((LEG+6))
|
||
|
||
# dual grid: Gc = glyphs, Gk = per-cell colour key ('.' = default)
|
||
local -a Gc Gk
|
||
local r c t blank kblank
|
||
printf -v blank '%*s' "$WIDTH" ''
|
||
printf -v kblank '%*s' "$WIDTH" ''; kblank=${kblank// /.}
|
||
for ((r=0;r<HEIGHT;r++)); do Gc[r]="$blank"; Gk[r]="$kblank"; done
|
||
|
||
# stamp glyph run at (row,col) carrying a single colour key
|
||
_zp() {
|
||
local rr=$1 cc=$2 s=$3 k=${4:-.} len=${#3} krun
|
||
(( cc<0 )) && return
|
||
Gc[rr]="${Gc[rr]:0:cc}${s}${Gc[rr]:cc+len}"
|
||
printf -v krun '%*s' "$len" ''; krun=${krun// /$k}
|
||
Gk[rr]="${Gk[rr]:0:cc}${krun}${Gk[rr]:cc+len}"
|
||
}
|
||
# reuse da_repeat when present, else fall back
|
||
_zrep() {
|
||
if declare -F da_repeat >/dev/null 2>&1; then da_repeat "$1" "$2"
|
||
else local g=$1 n=$2 o=''; while ((n-->0)); do o+="$g"; done; printf '%s' "$o"; fi
|
||
}
|
||
# heatsink fin ramp: period-4 ridge, lit from upper-left
|
||
_zfin() {
|
||
case $(( (($1%4)+4)%4 )) in
|
||
0) _zp "$2" "$3" '█' S ;;
|
||
1) _zp "$2" "$3" '▓' S ;;
|
||
2) _zp "$2" "$3" '▒' S ;;
|
||
3) _zp "$2" "$3" '░' S ;;
|
||
esac
|
||
}
|
||
|
||
local dashW; dashW="$(_zrep '─' "$Wi")"
|
||
|
||
# --- header ------------------------------------------------------------
|
||
_zp 0 0 'driveAtlas' H
|
||
_zp 0 11 ":: ${host}" S
|
||
_zp 0 $((WIDTH-13)) 'ZimaBoard 832' D
|
||
|
||
# --- left side face (shadow) + corrugated finned top -------------------
|
||
for ((r=1;r<S;r++)); do
|
||
local row=$((R0+r)) lc=$((BL+r)) rc=$((BR+r))
|
||
for ((c=BL+1;c<lc;c++)); do _zp "$row" "$c" '▒' D; done
|
||
for ((c=lc+1;c<rc;c++)); do _zfin $((c-row)) "$row" "$c"; done
|
||
done
|
||
_zp "$R0" "$BL" "┌${dashW}┐" H
|
||
for ((r=1;r<S;r++)); do
|
||
_zp $((R0+r)) $((BL+r)) '╲' H
|
||
_zp $((R0+r)) $((BR+r)) '╲' H
|
||
done
|
||
|
||
# --- front-top bevel carrying the logo badge ---------------------------
|
||
_zp "$FE" "$BL" '│' D
|
||
for ((c=BL+1;c<FL;c++)); do _zp "$FE" "$c" '▒' D; done
|
||
local badge=' ZIMA BOARD 832 ' bl lead
|
||
bl=${#badge}; lead=$(( (Wi-bl-2)/2 ))
|
||
_zp "$FE" "$FL" "┌$(_zrep '─' "$lead")" H
|
||
_zp "$FE" $((FL+1+lead)) '┤' G
|
||
_zp "$FE" $((FL+2+lead)) "$badge" H
|
||
_zp "$FE" $((FL+2+lead+bl)) '├' G
|
||
_zp "$FE" $((FL+3+lead+bl)) "$(_zrep '─' $((Wi-lead-bl-2)))┐" H
|
||
|
||
# --- front face + left side fill, then close the side ------------------
|
||
for ((r=FE+1;r<=FB;r++)); do
|
||
for ((c=BL+1;c<FL;c++)); do _zp "$r" "$c" '▒' D; done
|
||
_zp "$r" "$FL" '│' H
|
||
_zp "$r" "$FR" '│' H
|
||
done
|
||
_zp "$FB" "$FL" "└${dashW}┘" H
|
||
for ((r=R0+1;r<=FB-S;r++)); do _zp "$r" "$BL" '│' D; done
|
||
for ((t=1;t<S;t++)); do
|
||
for ((c=BL;c<BL+t;c++)); do _zp $((FB-S+t)) "$c" ' ' '.'; done
|
||
_zp $((FB-S+t)) $((BL+t)) '╲' D
|
||
done
|
||
for ((c=BL;c<FL;c++)); do _zp "$FB" "$c" ' ' '.'; done
|
||
|
||
# --- rectangular notch cut into the FRONT-LEFT of the fin block --------
|
||
_zp $((FE-2)) $((FL+2)) '┌─────┐' D
|
||
_zp $((FE-1)) $((FL+2)) '└─────┘' D
|
||
|
||
# --- front I/O ---------------------------------------------------------
|
||
# Centre: ONE shared housing with 2x RJ45 sitting directly on top of 2x USB.
|
||
# RJ45 ██▄██ solid jack, notch cut into the top centre = latch slot
|
||
# USB-A █▀▀▀█ side walls + tongue bar filling the upper half
|
||
local MX=32
|
||
_zp $((FE+1)) "$MX" '┌─────┬─────┐' H
|
||
_zp $((FE+2)) "$MX" '│' H
|
||
_zp $((FE+2)) $((MX+1)) '██▄██' C
|
||
_zp $((FE+2)) $((MX+6)) '│' H
|
||
_zp $((FE+2)) $((MX+7)) '██▄██' C
|
||
_zp $((FE+2)) $((MX+12)) '│' H
|
||
_zp $((FE+3)) "$MX" '│' H
|
||
_zp $((FE+3)) $((MX+1)) '█▀▀▀█' B
|
||
_zp $((FE+3)) $((MX+6)) '│' H
|
||
_zp $((FE+3)) $((MX+7)) '█▀▀▀█' B
|
||
_zp $((FE+3)) $((MX+12)) '│' H
|
||
_zp $((FE+4)) "$MX" '└─────┴─────┘' H
|
||
_zp $((FE+2)) $((MX-5)) 'GbE' D
|
||
_zp $((FE+3)) $((MX-5)) 'USB' D
|
||
# Mini-DisplayPort (left, lower tier): chamfered slot, not a plain box
|
||
_zp $((FE+3)) 20 '╱██╲' S
|
||
_zp $((FE+4)) 20 'mDP' D
|
||
# DC 12V barrel jack (right, lower tier): round housing + centre pin
|
||
_zp $((FE+3)) 52 '(█)' S
|
||
_zp $((FE+4)) 50 'DC 12V' D
|
||
|
||
# --- open-ended PCIe x4 card edge out the LEFT side --------------------
|
||
_zp $((FE+1)) 0 'PCIe x4 ' D
|
||
_zp $((FE+1)) 8 '▐▓▓┤' C
|
||
|
||
# --- rubber feet -------------------------------------------------------
|
||
_zp $((FB+1)) $((FL+1)) '╹' D
|
||
_zp $((FB+1)) $((FR-1)) '╹' D
|
||
|
||
# --- drive map: eMMC(OS) | rear SATA 1 | rear SATA 2 -------------------
|
||
# occupied = solid box + green ; EMPTY = dashed ghost + dim
|
||
local x0=2 x1=18 x2=34 bd gd
|
||
bd="$(_zrep '─' 13)"; gd="$(_zrep '┄' 13)"
|
||
_zp "$LEG" "$x0" 'eMMC (OS)' S
|
||
_zp "$LEG" "$x1" 'rear SATA 1' S
|
||
_zp "$LEG" "$x2" 'rear SATA 2' S
|
||
|
||
_zslot() { # $1=startcol $2=device-or-EMPTY $3=second-line text
|
||
local x=$1 dev=$2 sub=$3 k br f1 f2 vb
|
||
local r1=$((LEG+1)) r2=$((LEG+2)) r3=$((LEG+3)) r4=$((LEG+4))
|
||
if [ "$dev" = EMPTY ]; then
|
||
k=D; vb='┊'; br="$gd"
|
||
printf -v f1 '%-11.11s' 'empty'
|
||
printf -v f2 '%-11.11s' "$sub"
|
||
else
|
||
k=G; vb='│'; br="$bd"
|
||
printf -v f1 '%-11.11s' "$dev"
|
||
printf -v f2 '%-11.11s' "$sub"
|
||
fi
|
||
_zp "$r1" "$x" "┌${br}┐" "$k"
|
||
_zp "$r4" "$x" "└${br}┘" "$k"
|
||
_zp "$r2" "$x" "$vb" "$k"; _zp "$r2" $((x+1)) " $f1 " "$k"; _zp "$r2" $((x+14)) "$vb" "$k"
|
||
_zp "$r3" "$x" "$vb" "$k"; _zp "$r3" $((x+1)) " $f2 " "$k"; _zp "$r3" $((x+14)) "$vb" "$k"
|
||
}
|
||
|
||
if (( emmc_on )); then _zslot "$x0" "$emmc_dev" "${emmc_size} boot"
|
||
else _zslot "$x0" 'EMPTY' 'no eMMC'; fi
|
||
_zslot "$x1" "$sata1" 'cable-out'
|
||
_zslot "$x2" "$sata2" 'cable-out'
|
||
|
||
_zp $((LEG+5)) "$x0" 'rear edge: SATA │ PWR │ SATA' D
|
||
_zp $((LEG+5)) $((WIDTH-13)) 'Y-cable x2' D
|
||
|
||
# --- emit: run-length colour; identical display width mono & colour ----
|
||
local i cur ch key out
|
||
for ((r=0;r<HEIGHT;r++)); do
|
||
out=''; cur='.'
|
||
for ((i=0;i<WIDTH;i++)); do
|
||
key="${Gk[r]:i:1}"; ch="${Gc[r]:i:1}"
|
||
if [ "$key" != "$cur" ]; then
|
||
case "$key" in
|
||
H) out+="$DA_HL" ;; S) out+="$DA_ST" ;; D) out+="$DA_DIM" ;;
|
||
G) out+="$DA_OK" ;; B) out+="$DA_BLU" ;; C) out+="$DA_CYN" ;;
|
||
Y) out+="$DA_YEL" ;; *) out+="$DA_RST" ;;
|
||
esac
|
||
cur="$key"
|
||
fi
|
||
out+="$ch"
|
||
done
|
||
[ "$cur" != '.' ] && out+="$DA_RST"
|
||
printf '%s\n' "$out"
|
||
done
|
||
|
||
unset -f _zp _zrep _zfin _zslot 2>/dev/null || true
|
||
}
|
||
|
||
HOSTNAME=$(hostname | tr -cd '[:alnum:]-_.')
|
||
CHASSIS_TYPE=${CHASSIS_TYPES[$HOSTNAME]:-"unknown"}
|
||
|
||
# Display chassis layout
|
||
case "$CHASSIS_TYPE" in
|
||
"10bay")
|
||
generate_10bay_layout "$HOSTNAME"
|
||
;;
|
||
"large1")
|
||
generate_large1_layout "$HOSTNAME"
|
||
;;
|
||
"micro")
|
||
generate_micro_layout "$HOSTNAME"
|
||
;;
|
||
"zimaboard")
|
||
generate_zimaboard_layout "$HOSTNAME"
|
||
;;
|
||
"nuc")
|
||
generate_nuc_layout "$HOSTNAME"
|
||
;;
|
||
*)
|
||
echo "┌─────────────────────────────────────────────────────────┐"
|
||
echo "│ Unknown server: $HOSTNAME"
|
||
echo "│ No chassis mapping defined yet"
|
||
echo "│ Run with --diagnose to gather PCI path information"
|
||
echo "└─────────────────────────────────────────────────────────┘"
|
||
;;
|
||
esac
|
||
|
||
#------------------------------------------------------------------------------
|
||
# Drive Details Section
|
||
#------------------------------------------------------------------------------
|
||
|
||
# Build Ceph OSD cache (single query instead of per-device)
|
||
if [[ "$SKIP_CEPH" != true ]]; then
|
||
build_ceph_cache
|
||
fi
|
||
|
||
printf "\n"
|
||
colorize_header '=== Drive Details with SMART Status (by Bay Position) ==='
|
||
if [[ "$SHOW_PCI" == true ]]; then
|
||
printf "%-5s %-15s %-10s %-8s %-8s %-8s %-30s %-20s %-12s %-10s %-10s %-30s %-40s\n" "BAY" "DEVICE" "SIZE" "TYPE" "TEMP" "HEALTH" "MODEL" "SERIAL" "CEPH OSD" "STATUS" "USAGE" "WARNINGS" "PCI PATH"
|
||
echo "----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------"
|
||
else
|
||
printf "%-5s %-15s %-10s %-8s %-8s %-8s %-30s %-20s %-12s %-10s %-10s %-30s\n" "BAY" "DEVICE" "SIZE" "TYPE" "TEMP" "HEALTH" "MODEL" "SERIAL" "CEPH OSD" "STATUS" "USAGE" "WARNINGS"
|
||
echo "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
|
||
fi
|
||
|
||
# Build reverse map: device -> bay
|
||
declare -A DEVICE_TO_BAY
|
||
for bay in "${!DRIVE_MAP[@]}"; do
|
||
device="${DRIVE_MAP[$bay]}"
|
||
if [[ -n "$device" && "$device" != "EMPTY" ]]; then
|
||
DEVICE_TO_BAY["$device"]="$bay"
|
||
fi
|
||
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; printf '%s\n' "${!DRIVE_MAP[@]}" | grep -E '^usb' | sort)"
|
||
|
||
# Cache lsblk data to reduce redundant calls
|
||
# Get device sizes (whole disk only)
|
||
declare -A LSBLK_SIZE=()
|
||
declare -A LSBLK_MOUNTS=()
|
||
log_info "Caching block device information..."
|
||
|
||
# Get sizes for whole disks only
|
||
while read -r name size; do
|
||
[[ -z "$name" ]] && continue
|
||
LSBLK_SIZE["$name"]="$size"
|
||
done < <(lsblk -dn -o NAME,SIZE 2>/dev/null)
|
||
|
||
# Get mount points (including partitions) and map back to parent device
|
||
while read -r name mounts; do
|
||
[[ -z "$name" || -z "$mounts" ]] && continue
|
||
# Strip partition suffix (sda1 -> sda, nvme0n1p1 -> nvme0n1)
|
||
if [[ "$name" =~ ^(nvme[0-9]+n[0-9]+)p[0-9]+$ ]]; then
|
||
parent="${BASH_REMATCH[1]}"
|
||
elif [[ "$name" =~ ^([a-z]+)[0-9]+$ ]]; then
|
||
parent="${BASH_REMATCH[1]}"
|
||
else
|
||
parent="$name"
|
||
fi
|
||
if [[ -n "${LSBLK_MOUNTS[$parent]:-}" ]]; then
|
||
LSBLK_MOUNTS["$parent"]+=",${mounts}"
|
||
else
|
||
LSBLK_MOUNTS["$parent"]="$mounts"
|
||
fi
|
||
done < <(lsblk -rn -o NAME,MOUNTPOINT 2>/dev/null | grep -v '^ ')
|
||
|
||
# Parallel SMART data collection for faster execution
|
||
# Collect raw smartctl output in background jobs, parse later
|
||
if [[ "$SKIP_SMART" != true ]]; then
|
||
SMART_CACHE_DIR="$(mktemp -d)"
|
||
log_info "Collecting SMART data in parallel..."
|
||
|
||
max_parallel_jobs=10
|
||
job_count=0
|
||
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 (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
|
||
((job_count--))
|
||
fi
|
||
fi
|
||
done
|
||
|
||
# Wait for all remaining background SMART queries to complete
|
||
wait
|
||
log_info "SMART data collection complete"
|
||
fi
|
||
|
||
for bay in $all_bays; do
|
||
device="${DRIVE_MAP[$bay]}"
|
||
if [[ -n "$device" && "$device" != "EMPTY" && -b "/dev/$device" ]]; then
|
||
# Use cached lsblk data
|
||
size="${LSBLK_SIZE[$device]:-}"
|
||
|
||
# Get SMART info from cache (or defaults if skipped)
|
||
if [[ "$SKIP_SMART" == true ]]; then
|
||
type="-"
|
||
temp="-"
|
||
health="-"
|
||
model="-"
|
||
serial="-"
|
||
warnings=""
|
||
else
|
||
# Read from cached raw SMART data and parse it
|
||
raw_smart=""
|
||
if [[ -f "$SMART_CACHE_DIR/${device}.raw" ]]; then
|
||
raw_smart="$(cat "$SMART_CACHE_DIR/${device}.raw")"
|
||
fi
|
||
# Parse the raw data using get_drive_smart_info logic inline
|
||
if [[ -n "$raw_smart" ]]; then
|
||
smart_info="$(parse_smart_data "$device" "$raw_smart")"
|
||
IFS='|' read -r type temp health model serial warnings <<< "$smart_info"
|
||
else
|
||
type="-"
|
||
temp="-"
|
||
health="-"
|
||
model="-"
|
||
serial="-"
|
||
warnings=""
|
||
fi
|
||
fi
|
||
|
||
# Check for Ceph OSD using cached data
|
||
osd_id="-"
|
||
ceph_status="-"
|
||
if [[ "$SKIP_CEPH" != true ]]; then
|
||
osd_id="${CEPH_DEVICE_TO_OSD[$device]:-}"
|
||
if [[ -n "$osd_id" ]]; then
|
||
# Get status from cached OSD tree data
|
||
osd_num="${osd_id#osd.}"
|
||
up_status="${CEPH_OSD_STATUS[$osd_num]:-unknown}"
|
||
in_status="${CEPH_OSD_IN[$osd_num]:-out}"
|
||
ceph_status="${up_status}/${in_status}"
|
||
else
|
||
osd_id="-"
|
||
fi
|
||
fi
|
||
|
||
# Check mount points using cached lsblk data
|
||
# This includes both whole-device mounts and partition mounts
|
||
usage="-"
|
||
mount_points="${LSBLK_MOUNTS[$device]:-}"
|
||
# Limit to first 3 mount points for display
|
||
mount_points="$(echo "$mount_points" | tr ',' '\n' | head -3 | tr '\n' ',' | sed 's/,$//')"
|
||
if [[ -n "$mount_points" ]]; then
|
||
if [[ "$mount_points" == *"/"* && ! "$mount_points" == *"/boot"* && ! "$mount_points" == *"/home"* ]]; then
|
||
# Root filesystem mounted (but not just /boot or /home)
|
||
if echo "$mount_points" | grep -qE '^/,|^/$|,/$'; then
|
||
usage="BOOT"
|
||
else
|
||
usage="$mount_points"
|
||
fi
|
||
else
|
||
usage="$mount_points"
|
||
fi
|
||
fi
|
||
|
||
# Apply colors if enabled
|
||
colored_temp="$(colorize_temp "$temp")"
|
||
colored_health="$(colorize_health "$health")"
|
||
|
||
# Colorize warnings if present
|
||
colored_warnings="${warnings:--}"
|
||
if [[ "$USE_COLOR" == true && -n "$warnings" ]]; then
|
||
colored_warnings="${COLOR_YELLOW}${warnings}${COLOR_RESET}"
|
||
fi
|
||
|
||
if [[ "$SHOW_PCI" == true ]]; then
|
||
pci_path="${BAY_TO_PCI_PATH[$bay]:-}"
|
||
printf "%-5s %-15s %-10s %-8s %-8b %-8b %-30s %-20s %-12s %-10s %-10s %-30b %-40s\n" "$bay" "/dev/$device" "$size" "$type" "$colored_temp" "$colored_health" "$model" "$serial" "$osd_id" "$ceph_status" "$usage" "$colored_warnings" "$pci_path"
|
||
else
|
||
printf "%-5s %-15s %-10s %-8s %-8b %-8b %-30s %-20s %-12s %-10s %-10s %-30b\n" "$bay" "/dev/$device" "$size" "$type" "$colored_temp" "$colored_health" "$model" "$serial" "$osd_id" "$ceph_status" "$usage" "$colored_warnings"
|
||
fi
|
||
fi
|
||
done
|
||
|
||
# NVMe drives (only show unmapped ones - mapped NVMe drives appear in main table)
|
||
nvme_devices=$(lsblk -d -n -o NAME,SIZE | grep "^nvme" 2>/dev/null)
|
||
if [[ -n "$nvme_devices" ]]; then
|
||
# Filter out already-mapped NVMe devices
|
||
unmapped_nvme=""
|
||
while read -r name size; do
|
||
if [[ -z "${DEVICE_TO_BAY[$name]:-}" ]]; then
|
||
unmapped_nvme+="$name $size"$'\n'
|
||
fi
|
||
done <<< "$nvme_devices"
|
||
|
||
if [[ -n "$unmapped_nvme" ]]; then
|
||
printf "\n"
|
||
colorize_header '=== Unmapped NVMe Drives ==='
|
||
printf "%-15s %-10s %-10s %-40s %-25s\n" "DEVICE" "SIZE" "TYPE" "MODEL" "SERIAL"
|
||
echo "------------------------------------------------------------------------------------------------------"
|
||
echo "$unmapped_nvme" | while read -r name size; do
|
||
[[ -z "$name" ]] && continue
|
||
device="/dev/$name"
|
||
# Get model and serial from smartctl for accuracy
|
||
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="-"
|
||
[[ -z "$serial" ]] && serial="-"
|
||
printf "%-15s %-10s %-10s %-40s %-25s\n" "$device" "$size" "NVMe" "$model" "$serial"
|
||
done
|
||
fi
|
||
fi
|
||
|
||
#------------------------------------------------------------------------------
|
||
# Optional sections
|
||
#------------------------------------------------------------------------------
|
||
|
||
# Ceph RBD Devices
|
||
rbd_devices=$(lsblk -d -n -o NAME,SIZE,TYPE 2>/dev/null | grep "rbd" | sort -V)
|
||
if [ -n "$rbd_devices" ]; then
|
||
printf "\n"
|
||
colorize_header '=== Ceph RBD Devices ==='
|
||
printf "%-15s %-10s %-10s %-30s\n" "DEVICE" "SIZE" "TYPE" "MOUNTPOINT"
|
||
echo "------------------------------------------------------------"
|
||
echo "$rbd_devices" | while read -r name size type; do
|
||
# Get mountpoint if any
|
||
mountpoint=$(lsblk -n -o MOUNTPOINT "/dev/$name" 2>/dev/null | head -1)
|
||
[[ -z "$mountpoint" ]] && mountpoint="-"
|
||
printf "%-15s %-10s %-10s %-30s\n" "/dev/$name" "$size" "$type" "$mountpoint"
|
||
done
|
||
fi
|
||
|
||
# Show mapping diagnostic info if DEBUG is set
|
||
if [[ -n "$DEBUG" ]]; then
|
||
printf "\n"
|
||
colorize_header '=== DEBUG: Drive Mappings ==='
|
||
for key in "${!DRIVE_MAP[@]}"; do
|
||
echo "Bay $key: ${DRIVE_MAP[$key]}"
|
||
done | sort -n
|
||
fi
|