Задача звучала на пять минут: проверить, нужна ли перезагрузка серверу клиента. Посмотреть /var/run/reboot-required, ответить «да» или «нет». Разбирались полдня, потому что почти каждое утверждение про needrestart, которое выглядит очевидным, при проверке оказалось неверным.

Дальше - что этот инструмент делает на самом деле. Всё проверено на живой машине: Ubuntu 22.04, работающее ядро 5.15.0-187, установленное 5.15.0-190, панель управления и полтора десятка чужих сервисов рядом с нашим сайтом.

Он не перезагружает систему и не умеет этого

Главное заблуждение: раз он говорит про ядро, значит может и ребутнуть. Не может. Проверил тремя способами, потому что на боевом сервере верить памяти не хочется.

Список опций: единственный режим действий это -r, и все три его значения касаются только сервисов - l (list), i (interactive), a (automatic). Опции «перезагрузить хост» там нет. Кстати, у needrestart нет -h: справка вылезает на любой неизвестный флаг.

Man-страница: слово reboot не встречается ни разу, ядро упомянуто один раз, и то как -k check for obsolete kernel, то есть только проверка.

Код: grep по /usr/share/perl5/NeedRestart нашёл два вхождения, и оба про контейнеры, а не про хост.

machined.pm:90   machinectl reboot <машина>
LXC.pm:115       lxc-stop --reboot --name <контейнер>

То есть роль needrestart по отношению к ядру чисто уведомительная. Он сам об этом честно пишет в своей плашке: «Restarting the system to load the new kernel will not be handled automatically».

Внутри две независимые проверки

Это второе, что стоит понять сразу: ядро и сервисы проверяются отдельно и никак друг на друга не влияют. Их можно запустить по одной.

$ sudo needrestart -k -b        # только ядро
NEEDRESTART-KCUR: 5.15.0-187-generic
NEEDRESTART-KEXP: 5.15.0-190-generic
NEEDRESTART-KSTA: 3

$ sudo needrestart -l -b        # только библиотеки
NEEDRESTART-SVC: exim4.service
NEEDRESTART-SVC: NetworkManager.service
NEEDRESTART-SVC: php8.1-fpm.service
...

KCUR - работающее ядро, KEXP - ожидаемое, KSTA - вердикт: 1 всё в порядке, 2 ABI совместим, 3 нужен ребут. Флаг -b (batch) обязателен, если команда идёт из скрипта или по ssh без TTY: без него программа уходит в интерактивный диалог и повисает в ожидании ввода.

Следствие, которое обычно и вызывает вопросы: если перезапустить демоны, исчезнут только строки NEEDRESTART-SVC. Блок про ядро останется, и плашка «Pending kernel upgrade» будет появляться при каждом запуске, пока новое ядро не загрузится по-настоящему.

Без sudo он молча уходит

Запуск от обычного пользователя выглядит как поломка:

$ needrestart
Scanning processes...
$

Ни списка, ни ядра, ни диалога. Причина простая: проверка библиотек читает чужие процессы, проверка ядра лезет в системное состояние, и без root оба возвращают пусто. Сканировать свои собственные процессы он, конечно, может, но перезапускать там нечего.

Интерактивный режим опаснее, чем кажется

Первый экран без флагов - это whiptail, текстовый диалог с рамкой из символов. Никакого графического режима, просто программа ждёт нажатия клавиши. Экран «Pending kernel upgrade» безобиден, это визуальная форма того же KSTA: 3.

А вот следующий экран - список сервисов с чекбоксами, и отмечены они по умолчанию. Подтверждение там сразу перезапустит на живой машине пул PHP-FPM, почтовик и NetworkManager. Ядро при этом всё равно не поменяется. Так что для просмотра надёжнее -b, а если уж зашли в диалог - Cancel или Esc.

Режим -p отвечает на всё одной строкой

Самый практичный вариант, который мы в итоге и оставили. Формат тут не собственный, а стандартный для Nagios-плагинов: СТАТУС - текст | perfdata.

$ sudo needrestart -p
CRIT - Kernel: 5.15.0-187-generic!=5.15.0-190-generic (!!), Services: 6 (!),
Containers: none, Sessions: none|Kernel=2;0;;0;2 Services=6;;0;0 ...
код возврата: 2

Пометки в скобках - это уровни: без пометки OK, (!) warning, (!!) critical. Общий статус берётся по худшему подпункту, а не по типу проверки. Хорошо видно на моей рабочей машине, где ядро не проверялось:

CRIT - Services: 5 (!), Containers: none, Sessions: 1 (!!)

Сервисы сами по себе дали бы WARN, но одна залипшая сессия на устаревших бинарниках вытянула ответ до CRIT. Коды возврата тоже готовые: 0 чисто, 1 нужен рестарт сервисов, 2 нужен ребут. Одной командой получаешь и вердикт для человека, и число для монитора.

Ручная альтернатива через uname -r плюс dpkg | sort -V выглядит проще, но она хрупкая: спотыкается на разных флейворах (-generic против -virtual), на HWE-ядрах и на случае, когда самое свежее установленное ядро не то, которое выберет GRUB. needrestart вычисляет ожидаемое ядро сам.

apt зовёт его сам, но только чтобы показать

Отдельная находка: needrestart уже встроен в apt через /etc/apt/apt.conf.d/99needrestart, то есть запускается после каждой операции с пакетами, включая ночные unattended-upgrades. В логе ночного прогона это выглядит так, будто он всё починил:

Services to be restarted:
 systemctl restart exim4.service
 systemctl restart php8.1-fpm.service

Service restarts being deferred:
 systemctl restart NetworkManager.service

Ключевое слово - «to be restarted», а не «Restarting». Он печатает команды, которые следовало бы выполнить, и выходит. Причина в конфиге: строка $nrconf{restart} в /etc/needrestart/needrestart.conf закомментирована, активного режима нет. Проверяется это не по логу, а по systemctl show -p ActiveEnterTimestamp: время старта юнитов не сдвинулось.

Отдельная категория deferred - это NetworkManager, который он сознательно не перезапускает. Разумно: на машине, до которой добираешься только по ssh, рестарт сетевого демона это классический способ потерять доступ.

reboot-required отвечает на другой вопрос

Привычный файл /var/run/reboot-required и его сосед .pkgs - это не индикатор, а журнал причин: какие пакеты в ходе установки попросили перезагрузку. Как показатель актуальности он ненадёжен по трём причинам. Туда пишет не только ядро (libc6, dbus, systemd, udev тоже добавляют себя). Он не очищается рестартом сервисов. И он только дописывается, поэтому пакет, попросивший ребут неделю назад, остаётся в списке.

Лежит он в /run, а это tmpfs, то есть память. Удалить руками технически можно, но бессмысленно: сам исчезнет при перезагрузке и вернётся при следующем обновлении ядра. needrestart этот файл не трогает вообще, он считает своё независимо.

Что закрывается без перезагрузки

Ровно половина списка - сервисы на удалённых библиотеках. У нас это оказались libcurl, libpng16 и libpq, которые php-fpm держал открытыми после обновления пакетов. Такое лечится обычным systemctl restart, но с оговорками: пул PHP отдаёт 502 в момент рестарта, поэтому лучше в тихий час, а сетевой демон стоит оставить перезагрузке.

Ядро не закрывается ничем, кроме ребута. Когда он наконец случился, вывод стал такой:

$ sudo needrestart -p
OK - Kernel: 7.0.0-30-generic, Microcode: CURRENT, Services: none,
Containers: none, Sessions: none|Kernel=0;0;;0;2 Microcode=0;0;;0;1 ...

Строка Microcode появляется не везде: она есть в needrestart 3.11 из свежей Ubuntu и отсутствует в 3.5 из 22.04. Полезно помнить, если парсите вывод скриптом.

Шпаргалка

sudo needrestart -p       # вердикт и код возврата, годится и человеку, и монитору
sudo needrestart -k -b    # только ядро, машинный формат
sudo needrestart -l -b    # только сервисы на старых библиотеках
sudo needrestart -r l     # список без диалога и без действий

И главное правило, ради которого всё это писалось: needrestart отвечает на вопрос «что устарело», а не «что сделать». Решение про перезагрузку и окно для неё остаётся человеку, у которого есть права и ответственность за эту машину.