В продолжении статьи: https://habr.com/ru/articles/962290
Пришла мне мысль что для такой задачи должны быть готовые зрелые решения. В частности в smartmontools и оказалось что всё уже давно придумано. Но не до конца ...

Решил поделится опытом. Причесал моё словоблудие нейронкой.


? Оглавление

  1. ⚙️ Основная конфигурация smartd

  2. ? Параметры DEVICESCAN

  3. ? Скрипт уведомлений в Telegram

  4. ? Fallback-скрипт проверка пропущенных тестов

  5. ? Weekly Summary еженедельная сводка

  6. ? Архитектура системы

  7. ✅ Чеклист проверки


⚙️ Основная конфигурация smartd

Файл: /etc/smartmontools/smartd.conf

DEVICESCAN -a -o on -S on \
  -s (S/../../7/10|L/../../01/12) \
  -m root \
  -M exec /path/to/script/smartd-tg-alert.sh \
  -n standby,10,q

? Параметры DEVICESCAN

Параметр

Описание

DEVICESCAN

Автоматическое обнаружение всех дисков: SATA, NVMe, USB (с поддержкой SMART)

-a

Мониторить все SMART-атрибуты, ошибки, health-статус

-o on

Включить автоматический offline-сбор данных (если поддерживается)

-S on

Включить автосохранение атрибутов

-s (S/../../7/10\|L/../../01/12)

Расписание тестов (см. ниже)

-m root

Получатель email-уведомлений (резервный канал)

-M exec /path/to/script.sh

Скрипт для кастомных уведомлений

-n standby,10,q

Не будить диск в спящем режиме (до 10 пропусков, q = quiet)

? Формат расписания -s

Синтаксис: T/MM/DD/d/HH

Поле

Значение

Пример

T

Тип теста: S = Short, L = Long

S, L

MM

Месяц (01–12 или .. = любой)

..

DD

День месяца (01–31 или .. = любой)

01, ..

d

День недели (1–7, где 7 = воскресенье)

7, .

HH

Час запуска (00–23)

10, 12

Моя конфигурация:

(S/../../7/10|L/../../01/12)

Тест

Расписание

? Короткий

Каждое воскресенье в 10:00

? Длинный

1-го числа каждого месяца в 12:00

? Важно: Если в момент запуска диск в режиме standby и активна опция -n, тест будет пропущен. Для критичных систем можно убрать эту опцию или использовать -n standby,10,m (отправить письмо при пропуске).


? Скрипт уведомлений в Telegram

Файл: /path/to/script/smartd-tg-alert.sh

#!/bin/bash

BOT_TOKEN="bot-token"
CHAT_ID="chat-id"

HOST=$(hostname)
MESSAGE="$SMARTD_MESSAGE"

curl -s -X POST "https://api.telegram.org/bot$BOT_TOKEN/sendMessage" \
    -d "chat_id=$CHAT_ID" \
    -d "text=? *$HOST*\n\n$MESSAGE" \
    -d "parse_mode=Markdown" >/dev/null 2>&1

# Всегда возвращаем 0, чтобы smartd не считал ошибку
exit 0

? Настройка прав:

sudo chmod +x /path/to/script/smartd-tg-alert.sh
sudo chown root:root /path/to/script/smartd-tg-alert.sh

⚠️ Важно: Бот должен быть запущен, а чат с ним — начат пользователем. Бот не может писать первым.


? Fallback-скрипт: проверка пропущенных тестов

Зачем: Если ПК был выключен в момент планового теста — этот скрипт запустит его при первой возможности.

Файл: /path/to/script/smart-fallback.sh

#!/bin/bash

# === CONFIG ===
SHORT_MAX_DAYS=8     # если short тест старше 8 дней → запускаем
LONG_MAX_DAYS=35     # если long тест старше 35 дней → запускаем

LOG_TAG="smart-fallback"

log() {
    logger -t "$LOG_TAG" "$1"
}

get_power_on_hours() {
    # Возвращает Power-On Hours (целое) или пусто.
    # Поддержка ATA и NVMe.
    local disk="$1"
    local a out

    out=$(smartctl -a "$disk" 2>/dev/null) || return 0

    # NVMe: "Power On Hours:  1234"
    a=$(echo "$out" | awk -F: '/^[[:space:]]*Power On Hours[[:space:]]*:/ {gsub(/[^0-9]/,"",$2); print $2; exit}')
    if [[ -n "$a" ]]; then
        printf '%s' "$a"
        return 0
    fi

    # ATA: таблица атрибутов, строка "Power_On_Hours"
    a=$(echo "$out" | awk '$2=="Power_On_Hours" {print $10; exit}')
    if [[ "$a" =~ ^[0-9]+$ ]]; then
        printf '%s' "$a"
        return 0
    fi
}

get_last_selftest_poh() {
    # Возвращает Lifetime (hours) из самого свежего self-test указанного типа.
    # type: "Short" или "Extended"
    local output="$1"
    local type="$2"

    # В selftest-логе smartctl колонка обычно называется "LifeTime(hours)".
    # Для строк вида:
    # "# 1  Short offline  Completed ...  00%   12345  -"
    # или:
    # "# 1  Extended offline Completed ... 00%  12345  -"
    # NVMe формат другой, например:
    # "0   Short   Completed ...  1353  -"
    #
    # Берем первое (самое свежее) совпадение и вынимаем число из колонки lifetime.
    echo "$output" | awk -v t="$type" '
        $0 ~ t && $0 !~ /^[[:space:]]*Num[[:space:]]/ {
            for (i=1; i<=NF; i++) {
                if ($i ~ /^[0-9]+$/) n = $i
            }
            if (n ~ /^[0-9]+$/) { print n; exit }
        }
    '
}

check_and_run_test() {
    local disk="$1"

    # получаем лог тестов
    local output
    output=$(smartctl -l selftest "$disk" 2>/dev/null)

    local poh
    poh=$(get_power_on_hours "$disk")

    # --- SHORT TEST ---
    local last_short_poh short_age_days
    last_short_poh=$(get_last_selftest_poh "$output" "Short")

    short_age_days=""
    if [[ -n "$poh" && -n "$last_short_poh" && "$poh" =~ ^[0-9]+$ && "$last_short_poh" =~ ^[0-9]+$ && "$poh" -ge "$last_short_poh" ]]; then
        short_age_days=$(( (poh - last_short_poh) / 24 ))
    fi

    if [[ -z "$last_short_poh" || -z "$poh" || -z "$short_age_days" || "$short_age_days" -gt "$SHORT_MAX_DAYS" ]]; then
        log "Running SHORT test on $disk (age_days: ${short_age_days:-unknown})"
        smartctl -t short "$disk" >/dev/null 2>&1
    fi

    # --- LONG TEST ---
    local last_long_poh long_age_days
    last_long_poh=$(get_last_selftest_poh "$output" "Extended")

    long_age_days=""
    if [[ -n "$poh" && -n "$last_long_poh" && "$poh" =~ ^[0-9]+$ && "$last_long_poh" =~ ^[0-9]+$ && "$poh" -ge "$last_long_poh" ]]; then
        long_age_days=$(( (poh - last_long_poh) / 24 ))
    fi

    if [[ -z "$last_long_poh" || -z "$poh" || -z "$long_age_days" || "$long_age_days" -gt "$LONG_MAX_DAYS" ]]; then
        log "Running LONG test on $disk (age_days: ${long_age_days:-unknown})"
        smartctl -t long "$disk" >/dev/null 2>&1
    fi
}

# === MAIN ===

smartctl --scan-open 2>/dev/null | awk '{print $1}' | while IFS= read -r disk; do
    [[ -z "$disk" ]] && continue
    if smartctl -c "$disk" &>/dev/null; then
        check_and_run_test "$disk"
    fi
done

? systemd: сервис + таймер

/etc/systemd/system/smart-fallback.service

[Unit]
Description=SMART fallback test runner

[Service]
Type=oneshot
ExecStart=/path/to/script/smart-fallback.sh

/etc/systemd/system/smart-fallback.timer

[Unit]
Description=Run SMART fallback daily

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target

▶️ Активация:

sudo chmod +x /path/to/script/smart-fallback.sh
sudo systemctl daemon-reload
sudo systemctl enable --now smart-fallback.timer

? Weekly Summary: еженедельная сводка

Зачем: Не просто алерты «что-то сломалось», а регулярный обзор состояния всех дисков.

Файл: /path/to/script/smart-summary.sh

#!/bin/bash

BOT_TOKEN="bot-token"
CHAT_ID="chat-id"

HOST=$(hostname)
DATE=$(date '+%Y-%m-%d')

LOG_TAG="smart-summary"
log() {
    logger -t "$LOG_TAG" -p user.info "$1"
}

# Используем <b>...</b> вместо *...* для жирного
MESSAGE=$(printf '? <b>SMART Summary (%s)</b>\nДата: %s' "$HOST" "$DATE")$'\n\n'

for disk in $(smartctl --scan-open | awk '{print $1}'); do
    if smartctl -c "$disk" &>/dev/null; then
        # Получаем health (работает и для ATA, и для NVMe)
        health=$(smartctl -H "$disk" 2>/dev/null | grep -E "SMART overall-health|SMART Health Status" | sed 's/.*: //')

        # Температура
        temp=$(smartctl -A "$disk" 2>/dev/null | grep -i temperature | head -n1 | awk -F': ' '{print $2}' | awk '{print $1, $2}')

        MESSAGE+=$(printf '? <b>%s</b> - %s' "$disk" "$health")$'\n\n'

        if [[ -n "$temp" && "$temp" != *" "* ]]; then
            # Важно: $(...) срезает завершающие переводы строк, поэтому добавляем их снаружи.
            MESSAGE+=$(printf '%s' "$temp")$'\n'
        fi

        # Ошибки: Reallocated, Pending, Uncorrectable
        errors=$(smartctl -A "$disk" 2>/dev/null | grep -E "(Reallocated_Sector|Current_Pending|Offline_Uncorrectable|Media_Error)" | awk '$10 > 0 {print "  • " $2 ": " $10}')
        if [[ -n "$errors" ]]; then
            MESSAGE+=$(printf '❗ Ошибки:\n%s' "$errors")$'\n'
        fi

        # Износ SSD
        wear=$(smartctl -A "$disk" 2>/dev/null | grep -E "(Wear_Leveling|Percent_Lifetime|Media_Wearout)" | head -n1 | awk -F': ' '{print $2}' | awk '{print $1}')
        if [[ -n "$wear" ]]; then
            MESSAGE+=$(printf '? Износ: %s' "$wear")$'\n'
        fi

        MESSAGE+=$'\n'
    fi
done

# Отправляем с parse_mode=HTML и получаем код ответа
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
    "https://api.telegram.org/bot$BOT_TOKEN/sendMessage" \
    --data-urlencode "chat_id=$CHAT_ID" \
    --data-urlencode "text=$MESSAGE" \
    --data-urlencode "parse_mode=HTML")

# Опционально: логирование ошибок
if [[ "$HTTP_CODE" != "200" ]]; then
    log "[$(date)] SMART summary failed: HTTP $HTTP_CODE"
fi

exit 0

? systemd: сервис + таймер

/etc/systemd/system/smart-summary.service

[Unit]
Description=SMART weekly summary

[Service]
Type=oneshot
ExecStart=/path/to/script/smart-summary.sh

/etc/systemd/system/smart-summary.timer

[Unit]
Description=Run SMART summary weekly

[Timer]
OnCalendar=Mon 12:00
Persistent=true

[Install]
WantedBy=timers.target

▶️ Активация:

sudo chmod +x /path/to/script/smart-summary.sh
sudo systemctl daemon-reload
sudo systemctl enable --now smart-summary.timer

? Архитектура системы

┌─────────────────────────────────────┐
│           smartd (демон)            │
│  • Мониторит диски в реальном времени│
│  • Запускает тесты по расписанию    │
│  • Вызывает скрипт при событиях     │
└────────────┬────────────────────────┘
             │ событие (алерт)
             ▼
┌─────────────────────────────────────┐
│   ? smartd-tg-alert.sh             │
│   • Мгновенное уведомление в Telegram│
└─────────────────────────────────────┘

┌─────────────────────────────────────┐
│   ? smart-fallback.timer (daily)   │
│   • Проверяет, не пропущены ли тесты│
│   • Запускает их при необходимости  │
└─────────────────────────────────────┘

┌─────────────────────────────────────┐
│   ? smart-summary.timer (weekly)   │
│   • Формирует сводку по всем дискам │
│   • Отправляет отчёт в Telegram     │
└─────────────────────────────────────┘

? Принципы работы:

Компонент

Тип

Частота

Цель

smartd

Демон

Постоянно

Реактивный мониторинг

smartd-tg-alert.sh

Скрипт-хук

По событию

Мгновенные алерты

smart-fallback.sh

Скрипт + timer

Ежедневно

Надёжность (пропущенные тесты)

smart-summary.sh

Скрипт + timer

Еженедельно

Проактивный обзор


✅ Чеклист проверки

? Конфигурация

  • sudo smartd -d -c /etc/smartmontools/smartd.conf — нет ошибок парсинга

  • sudo smartctl --scan — все ожидаемые диски отображаются

  • sudo systemctl enable --now smartd — служба активна

? Уведомления

  • Скрипт smartd-tg-alert.sh имеет права +x и владельца root

  • Бот активен, чат начат, токен верный

  • Проверка отправки в телеграм напрямую: SMARTD_MESSAGE=$'TEST from smartd\nsecond line' /path/to/script/smartd-tg-alert.sh

  • Проверка отправки через smartd: добавить в /etc/smartmontools/smartd.conf временно строку -M test \ И запустить sudo smartd -c /etc/smartmontools/smartd.conf -q onecheck

? Fallback

  • systemctl list-timers | grep smart-fallback — таймер активен, следующее срабатывание указано

  • sudo systemctl start smart-fallback.service — выполняется без ошибок

  • В journalctl -t smart-fallback появляются записи при запуске

? Summary

  • systemctl list-timers | grep smart-summary — таймер настроен на воскресенье 12:00

  • sudo systemctl start smart-summary.service — сводка приходит в Telegram

  • В сообщении отображаются: здоровье, температура, ошибки, износ (для SSD)

? Полезные команды

# Статус службы smartd
sudo systemctl status smartd

# Логи в реальном времени
journalctl -u smartd -f

# Проверка расписания тестов для диска
sudo smartctl -l selftest /dev/sdb

# Ручной запуск теста
sudo smartctl -t short /dev/sdb
sudo smartctl -t long /dev/sdb

# Просмотр результатов тестов
sudo smartctl -l selftest /dev/sdb

# Проверка таймеров
systemctl list-timers --all | grep smart

Комментарии (0)