Lint / Shell (shellcheck) (push) Successful in 17s
The four front ports were identical 2-char stubs under one lumped "GbE/USB" label, so they read as anonymous blobs. Draw them as the hardware actually is and give each connector type its own silhouette: RJ45 ██▄██ solid jack with a notch cut into the top centre (latch slot) USB-A █▀▀▀█ side walls plus a tongue bar filling the upper half mDP ╱██╲ chamfered slot DC (█) round barrel with a centre pin The 2x GbE and 2x USB now share ONE housing with the ethernet row sitting directly on top of the USB row and no divider between them, matching the physical stacked connector module. Each jack stays one row tall by using half-block glyphs to get two vertical pixels per cell. GbE and USB get separate labels; mDP and DC 12V sit beneath their own connectors. Front-face height HC grows 3 -> 5 for the room. Knock-on: HC feeds FB, which decides where the left side face stops being a vertical edge, so the PCIe row now lands on the last vertical row and its card edge makes a real T-junction into the chassis (▐▓▓┤) instead of floating. HC and the PCIe row are therefore coupled if this is tuned further. Still a fixed 66-column canvas, identical display width on every line in monochrome and colour, SAFE width-1 glyphs only. 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" '█' H ;;
|
||
1) _zp "$2" "$3" '▓' H ;;
|
||
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
|