154 lines
5.8 KiB
Markdown
154 lines
5.8 KiB
Markdown
```bash
|
|
nano nvme_linkcheck.sh
|
|
```
|
|
```bash
|
|
chmod +x nvme_linkcheck.sh
|
|
```
|
|
```bash
|
|
./nvme_linkcheck.sh
|
|
```
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
# nvme_linkcheck.sh — сводка по PCIe-линкам NVMe (скорость, ширина, cap/sta) + авто-вердикт.
|
|
# Требуется: bash, awk, sed, grep -P, lspci (pciutils). Желательно доступ к /sys.
|
|
# Цель по умолчанию: Gen4 x4 (16.0 GT/s, x4). Переопределить: TARGET_SPEED_GT=8.0 TARGET_WIDTH=4
|
|
set -euo pipefail
|
|
|
|
# ---------- Конфиг ----------
|
|
TARGET_SPEED_GT="${TARGET_SPEED_GT:-16.0}" # требуемая скорость в GT/s
|
|
TARGET_WIDTH="${TARGET_WIDTH:-4}" # требуемая ширина (xN)
|
|
COLOR="${COLOR:-auto}" # auto|1|0 — цветной вывод
|
|
# -----------------------------
|
|
|
|
need() { command -v "$1" >/dev/null 2>&1 || { echo "ERROR: '$1' not found. Install it."; exit 2; }; }
|
|
need lspci
|
|
need awk
|
|
need sed
|
|
need grep
|
|
|
|
# Цвета
|
|
is_tty=0; [[ -t 1 ]] && is_tty=1
|
|
if { [[ "$COLOR" == "1" ]] || { [[ "$COLOR" == "auto" ]] && [[ $is_tty -eq 1 ]]; }; }; then
|
|
C_OK="\033[1;32m"; C_WARN="\033[1;33m"; C_ERR="\033[1;31m"; C_DIM="\033[2m"; C_CLR="\033[0m"
|
|
else
|
|
C_OK=""; C_WARN=""; C_ERR=""; C_DIM=""; C_CLR=""
|
|
fi
|
|
|
|
hr() { printf "%s\n" "--------------------------------------------------------------------------------"; }
|
|
fmt_pad() { printf "%-6s | %-28s | %-12s | %-12s | %-4s | %-26s | %-26s | %s\n" "$@"; }
|
|
|
|
# Точные парсеры из LnkCap/LnkSta
|
|
extract_speed_gt() { # "LnkSta: Speed 16GT/s, Width x4" -> 16 or 16.0
|
|
grep -Po 'Speed\s+\K[0-9]+(\.[0-9]+)?(?=GT/s)' <<<"$1" 2>/dev/null || true
|
|
}
|
|
extract_width() { # "LnkSta: Speed 16GT/s, Width x4" -> 4
|
|
grep -Po 'Width\s+x\K[0-9]+' <<<"$1" 2>/dev/null || true
|
|
}
|
|
|
|
# Безопасные парсеры из sysfs
|
|
parse_speed_num() { awk '{for(i=1;i<=NF;i++) if($i ~ /^[0-9]+(\.[0-9]+)?$/){print $i; exit}}' <<<"$1"; }
|
|
parse_width_num() { sed -E 's/.*x?([0-9]+).*/\1/' <<<"$1"; }
|
|
|
|
get_lnkcap_sta() {
|
|
local pci="$1"
|
|
lspci -s "$pci" -vv 2>/dev/null | awk '
|
|
/LnkCap:/ && cap=="" {cap=$0}
|
|
/LnkSta:/ && sta=="" {sta=$0}
|
|
END{print cap"\n"sta}'
|
|
}
|
|
|
|
problems=0
|
|
printf "NVMe PCIe link check (target: %s GT/s, x%s)\n" "$TARGET_SPEED_GT" "$TARGET_WIDTH"
|
|
hr
|
|
fmt_pad "NVMe" "Model" "PCI" "CurSpeed" "xW" "LnkCap" "LnkSta" "Verdict"
|
|
hr
|
|
|
|
shopt -s nullglob
|
|
found_any=0
|
|
for d in /sys/class/nvme/nvme*; do
|
|
found_any=1
|
|
ctrl="$(basename "$d")"
|
|
|
|
# Модель
|
|
model="n/a"
|
|
[[ -r "$d/model" ]] && model="$(tr -d '\n' < "$d/model" | sed -E 's/[[:space:]]+$//; s/[[:space:]]{2,}/ /g')"
|
|
|
|
# PCI-адрес
|
|
pci="$(readlink -f "$d/device" | awk -F/ '{print $NF}')"
|
|
if [[ -z "${pci}" ]]; then
|
|
fmt_pad "$ctrl" "$model" "?" "?" "?" "?" "?" "${C_ERR}MISSING${C_CLR}"
|
|
problems=$((problems+1))
|
|
continue
|
|
fi
|
|
|
|
# Текущие скорость/ширина из sysfs
|
|
cur_speed="$(cat "/sys/bus/pci/devices/$pci/current_link_speed" 2>/dev/null || echo "n/a")"
|
|
cur_width="$(cat "/sys/bus/pci/devices/$pci/current_link_width" 2>/dev/null || echo "n/a")"
|
|
cur_speed_num="$(parse_speed_num "$cur_speed" || true)"
|
|
cur_width_num="$(parse_width_num "$cur_width" || true)"
|
|
|
|
# LnkCap / LnkSta из lspci
|
|
readarray -t capsta < <(get_lnkcap_sta "$pci")
|
|
lnkcap="${capsta[0]:-n/a}"
|
|
lnksta="${capsta[1]:-n/a}"
|
|
cap_speed_num="$(extract_speed_gt "$lnkcap")"
|
|
cap_width_num="$(extract_width "$lnkcap")"
|
|
sta_speed_num="$(extract_speed_gt "$lnksta")"
|
|
sta_width_num="$(extract_width "$lnksta")"
|
|
|
|
# Статус (по умолчанию OK)
|
|
status="OK"
|
|
|
|
# LIMITED, если сам порт не поддерживает цель (cap ниже цели)
|
|
if [[ -z "$cap_speed_num" || -z "$cap_width_num" ]]; then
|
|
status="MISSING"
|
|
else
|
|
awk -v cap_s="$cap_speed_num" -v cap_w="$cap_width_num" -v tgt_s="$TARGET_SPEED_GT" -v tgt_w="$TARGET_WIDTH" '
|
|
BEGIN { if (cap_s+0.0 < tgt_s+0.0001 || cap_w+0 < tgt_w) exit 42; else exit 0 }' \
|
|
|| status="LIMITED"
|
|
fi
|
|
|
|
# DOWNGRADED, если cap ок, но sta/current ниже цели
|
|
if [[ "$status" == "OK" ]]; then
|
|
awk -v sta_s="${sta_speed_num:-0}" -v sta_w="${sta_width_num:-0}" \
|
|
-v cur_s="${cur_speed_num:-0}" -v cur_w="${cur_width_num:-0}" \
|
|
-v tgt_s="$TARGET_SPEED_GT" -v tgt_w="$TARGET_WIDTH" '
|
|
BEGIN {
|
|
if (sta_s+0.0 < tgt_s+0.0001 || sta_w+0 < tgt_w ||
|
|
cur_s+0.0 < tgt_s+0.0001 || cur_w+0 < tgt_w) exit 24;
|
|
else exit 0;
|
|
}' || status="DOWNGRADED"
|
|
fi
|
|
|
|
case "$status" in
|
|
OK) verdict="${C_OK}OK${C_CLR}" ;;
|
|
LIMITED) verdict="${C_WARN}LIMITED${C_CLR} (cap < target)"; problems=$((problems+1)) ;;
|
|
DOWNGRADED)verdict="${C_WARN}DOWNGRADED${C_CLR}"; problems=$((problems+1)) ;;
|
|
MISSING|*) verdict="${C_ERR}MISSING${C_CLR}"; problems=$((problems+1)) ;;
|
|
esac
|
|
|
|
# Компактный вывод cap/sta
|
|
short_cap="Speed $(extract_speed_gt "$lnkcap")GT/s, x$(extract_width "$lnkcap")"
|
|
short_sta="Speed $(extract_speed_gt "$lnksta")GT/s, x$(extract_width "$lnksta")"
|
|
|
|
fmt_pad "$ctrl" "$model" "$pci" "$cur_speed" "$cur_width" "$short_cap" "$short_sta" "$verdict"
|
|
done
|
|
|
|
if [[ $found_any -eq 0 ]]; then
|
|
echo "No NVMe controllers found under /sys/class/nvme"
|
|
exit 3
|
|
fi
|
|
|
|
hr
|
|
if [[ "$problems" -gt 0 ]]; then
|
|
echo -e "${C_WARN}Warnings/Issues: ${problems}${C_CLR}"
|
|
echo -e "${C_DIM}Legend:${C_CLR}
|
|
${C_OK}OK${C_CLR} — соответствует цели (>= ${TARGET_SPEED_GT} GT/s, x${TARGET_WIDTH})
|
|
${C_WARN}DOWNGRADED${C_CLR} — порт способен на большее (cap ок), но текущее/статус ниже цели
|
|
${C_WARN}LIMITED${C_CLR} — аппаратное ограничение (cap ниже цели)
|
|
MISSING — нет данных из sysfs/lspci"
|
|
exit 1
|
|
else
|
|
echo -e "${C_OK}All NVMe links meet the target (${TARGET_SPEED_GT} GT/s, x${TARGET_WIDTH}).${C_CLR}"
|
|
fi
|
|
``` |