Как зафиксировать версию ядра на VPS с Ubuntu
Параметр GRUB_DEFAULT не работает в облачных образах Ubuntu. Узнайте, как правильно прочитать список меню и зафиксировать загрузку нужного ядра без риска потери доступа по SSH.
Что определяет, какое ядро загружает ваш VPS
То, какое ядро будет загружено на вашем VPS при следующей перезагрузке, определяется одним сгенерированным файлом — /boot/grub/grub.cfg. Никогда не редактируйте этот файл напрямую. Изменяйте исходные файлы, на основе которых он создается, и перегенерируйте его. В облачных образах Ubuntu один из таких исходных файлов поставляется вендором, и он может сделать выбор в меню неактуальным. Именно поэтому GRUB_DEFAULT=1 в сочетании с update-grub не дает результата на арендованном сервере, хотя те же действия работают при установке на ноутбук.
Действуйте в следующем порядке. Убедитесь, что вы имеете право выбирать ядро. Прочитайте каждый исходный файл, включая те, что добавил вендор. Прочитайте сгенерированный выходной файл и подсчитайте количество записей, которые он действительно содержит. Только после этого выбирайте метод фиксации версии ядра. Ошибка в этом процессе на машине, доступной только по SSH, потребует использования rescue console, поэтому самые безопасные решения приведены в конце этой страницы, и зачастую они являются верными.
Сначала убедитесь, что ядро принадлежит вам, чтобы его зафиксировать
uname -r
systemd-detect-virt
ls -1 /boot/vmlinuz-*systemd-detect-virt вывод kvm, qemu или xen означает, что вы используете собственное ядро, и всё нижеописанное применимо. Вывод lxc или openvz означает, что ваш сервер использует ядро хоста, поэтому у вас нет собственного загрузчика и нечего фиксировать. В этом случае uname -r сообщает версию, которой вообще нет в /boot/vmlinuz-*, так как запущенное ядро принадлежит хосту, и никакие настройки на вашем диске не могут его изменить.
ls -1 /boot/vmlinuz-* — это фактический список ядер, между которыми вы можете выбирать. Если в нём осталась одна строка, значит, предыдущее ядро уже удалено, и никакие настройки загрузчика его не вернут. Обычно это происходит во время выполнения autoremove, что стоит изучить, прежде чем удалять старые ядра в Ubuntu на важном для вас сервере.
Файл, который вы редактируете, не является файлом, который читает GRUB
/etc/default/grub содержит простые присваивания переменных оболочки. Это входной файл. /boot/grub/grub.cfg — это выходной файл, который начинается с # DO NOT EDIT THIS FILE и пояснения причин. Всё, что вы запишете в выходной файл, будет удалено при следующей установке или удалении пакета ядра, так как скрипты пакетов перезаписывают его.
cat /usr/sbin/update-grubupdate-grub — это обертка. Она запускает grub-mkconfig -o /boot/grub/grub.cfg, который считывает переменные, выполняет каждый скрипт в /etc/grub.d/ и записывает результат. Две команды, одно направление: входные данные поступают на вход, а на выходе получается grub.cfg.
Что переопределяет ваши настройки: /etc/default/grub.d
grep -rn '^[^#]' /etc/default/grub /etc/default/grub.d/Второй путь — это то, что часто упускают из виду. grub-mkconfig считывает /etc/default/grub в первую очередь, а затем каждый файл *.cfg в /etc/default/grub.d/ в порядке glob. Изучите код, который это выполняет:
grep -n 'default/grub' /usr/sbin/grub-mkconfigПодключение файлов (sourcing) выполняется как обычный shell-скрипт, поэтому последнее присваивание имеет приоритет. В облачных образах Ubuntu в этой директории содержатся файлы, которые устанавливают параметры, такие как таймаут и командная строка ядра, уже после того, как ваш файл был прочитан. Ваше значение GRUB_TIMEOUT=10 в /etc/default/grub перезаписывается через мгновение файлом поставщика, который устанавливает его в 0. Команда grep выше выводит фактические присваивания в вашем образе, поэтому ориентируйтесь на них, а не на это утверждение.
Практическое правило: размещайте свои настройки в файле, который сортируется последним, например /etc/default/grub.d/99-local.cfg, вместо редактирования /etc/default/grub. Тогда ни один файл, поставляемый с образом, не сможет перезаписать ваши параметры.
Почему GRUB_FORCE_PARTUUID делает выбор в меню неактуальным
grep -rn GRUB_FORCE_PARTUUID /etc/default/grub /etc/default/grub.d/
grep -n GRUB_FORCE_PARTUUID /etc/grub.d/10_linux
sudo grep -n 'root=PARTUUID' /boot/grub/grub.cfgGRUB_FORCE_PARTUUID указывает генератору находить корневую файловую систему по PARTUUID раздела, записывая его прямо в командную строку ядра как root=PARTUUID=..., вместо поиска UUID файловой системы во время загрузки. Поставщик образа устанавливает этот параметр, так как он позволяет одному образу диска надежно загружаться на оборудовании, для которого он не был создан. Второй вызов grep показывает код, который обрабатывает эту переменную в /etc/grub.d/10_linux. Этот скрипт находится на вашем диске и является определяющим для того, как работает ваш образ.
Важно следующее: по этому пути генератор создает запись для прямой загрузки вместо полного списка установленных ядер. Посчитайте, что у вас получилось в итоге.
sudo grep -cE '^\s*(menuentry|submenu) ' /boot/grub/grub.cfg
sudo grep -nE '^\s*(menuentry|submenu) ' /boot/grub/grub.cfgЕсли количество записей равно 1, то второй записи для выбора не существует, поэтому GRUB_DEFAULT=1 ссылается на запись, которой нет. GRUB не может её разрешить, поэтому он загружает первую запись, которой является новое ядро, которого вы пытались избежать. grub-set-default также не помогает, так как проблема не в значении по умолчанию. Меню, из которого вы пытаетесь сделать выбор, так и не было создано.
Чтобы вернуть полное меню, переместите файл поставщика в другое место и просмотрите результат перед его применением. grub-mkconfig без -o выводит результат в стандартный поток вывода и ничего не меняет на диске.
sudo grub-mkconfig 2>/dev/null | grep -cE '^\s*(menuentry|submenu) '
sudo mkdir -p /root/grub-backup
sudo mv /etc/default/grub.d/<the file your grep named> /root/grub-backup/
sudo grub-mkconfig 2>/dev/null | grep -cE '^\s*(menuentry|submenu) 'Если количество записей увеличивается с 1 до нескольких, значит, записи появляются после отключения принудительного параметра. Пока ничего не записано. Верните файл на место, если второе количество записей выглядит неверно, так как принудительный PARTUUID — это способ, которым образ вашего провайдера находит корневую файловую систему, и его удаление переводит систему на путь поиска. Сделайте снимок состояния (snapshot) перед тем, как запускать update-grub по-настоящему.
Если ваша единственная цель — пережить одно неудачное обновление ядра, остановитесь здесь и используйте более безопасные варианты, описанные ниже. Пересоздание загрузочного меню на удаленном сервере ради одного обновления несет больше рисков, чем сама проблема.
Почему использование порядковых номеров записей — неверный подход
GRUB_DEFAULT принимает номер, заголовок или идентификатор. Номера отсчитывают записи верхнего уровня, начиная с 0. Вложенная запись использует > в качестве разделителя, поэтому GRUB_DEFAULT="1>2" означает запись с индексом 2 внутри подменю с индексом 1.
Индексы меняются. 10_linux выводит список ядер, начиная с самого нового, поэтому установка ядра сдвигает все старые записи на одну позицию вниз, а удаление ядра — вверх. Ваша тщательно настроенная 1>2 по-прежнему будет разрешаться после таких изменений. Теперь она указывает на другое ядро. Ошибок не возникает, предупреждений нет, и вы узнаете об этом только после перезагрузки.
Идентификаторы не меняются, так как каждый из них содержит версию ядра. Прочитайте свои:
sudo awk -F"'" '/menuentry_id_option/ {print $2, "==>", $4}' /boot/grub/grub.cfgПроигнорируйте первые несколько строк вывода, где определяется переменная в заголовке. После этого слева будет заголовок, который видит пользователь, а справа — идентификатор, который нужно передавать инструментам. Для записи внутри подменю объедините идентификатор подменю и идентификатор записи через > именно в таком порядке, как это делается в числовом формате.
Однократная загрузка предыдущего ядра с помощью grub-reboot
Выбор загрузки на один раз — оптимальное решение для удаленного сервера, так как настройка сбрасывается автоматически. Команда grub-reboot записывает next_entry в файл /boot/grub/grubenv. GRUB считывает эту переменную, очищает её и сохраняет пустое значение до начала загрузки. Благодаря этому, если ядро вызывает kernel panic, система не будет пытаться загрузить его снова при следующей перезагрузке. У вас есть одна попытка, после чего машина самостоятельно вернется к стандартным настройкам загрузки.
Сначала убедитесь, что ваш сгенерированный конфигурационный файл вообще считывает эту переменную:
sudo grep -n -B2 -A5 'next_entry' /boot/grub/grub.cfgВ выводе должна присутствовать строка load_env и блок, который устанавливает default из next_entry. Если команда grep ничего не вывела, значит, ваш образ не считывает grubenv при загрузке. В этом случае команда grub-reboot будет принята в командной строке, но проигнорирована загрузчиком. Это тот же путь принудительной прямой загрузки из предыдущего раздела, проявляющийся в другом месте.
sudo grub-reboot '<the identifier you copied>'
sudo grub-editenv listТеперь grub-editenv list должна вывести строку next_entry=, содержащую именно то значение, которое вы передали. Откройте консоль вашего провайдера в браузере, выполните перезагрузку и проверьте результат.
sudo rebootuname -rЕсли uname -r сообщает о старой версии ядра, значит, фиксация сработала. Если вы видите новую версию, это означает, что либо идентификатор не был разрешен, либо grubenv не считывается. В любом случае машина загрузилась, что и является главной целью использования однократного режима загрузки.
Закрепление выбора через GRUB_DEFAULT=saved
GRUB_DEFAULT=saved заставляет систему использовать значение по умолчанию из saved_entry в grubenv, а вы задаете это значение с помощью grub-set-default. Оно сохраняется после установки новых ядер, так как update-grub перезаписывает grub.cfg и никогда не затрагивает grubenv.
echo 'GRUB_DEFAULT=saved' | sudo tee /etc/default/grub.d/99-local.cfg
sudo update-grub
sudo grub-set-default '<the identifier you copied>'
sudo grub-editenv list
sudo grep -n 'set default' /boot/grub/grub.cfgПоследняя команда должна вывести set default="${saved_entry}". Если она выводит set default="0", значит, какой-то скрипт, выполняемый после вашего файла, сбросил GRUB_DEFAULT обратно на фиксированное значение. В этом случае снова выведите список /etc/default/grub.d/ и убедитесь, что 99-local.cfg действительно идет последним в списке.
GRUB_SAVEDEFAULT=true — это другой параметр, который легко спутать с текущим. Он сохраняет в качестве нового значения по умолчанию то, что вы загрузили последним, поэтому выбор по умолчанию всегда следует за последней успешной загрузкой. На сервере это означает, что автоматическая перезагрузка может незаметно изменить вашу «привязку» (pin). Оставьте этот параметр выключенным, если вам не нужно именно такое поведение.
Привязка по идентификатору все равно имеет один недостаток. Если удалить ядро, на которое он ссылается, идентификатор перестанет разрешаться, и система вернется к первому пункту меню. Поэтому зафиксируйте версию пакета (hold) или исключите это ядро из списка автоудаления (autoremove).
Вывод меню на консоль провайдера
Для интерактивного выбора меню должно отображаться на экране, но в облачных образах оно скрыто. Добавьте эти параметры в файл, который считывается последним, а затем выполните sudo update-grub.
GRUB_TIMEOUT=10
GRUB_TIMEOUT_STYLE=menu
GRUB_RECORDFAIL_TIMEOUT=10GRUB_TIMEOUT_STYLE=hidden вместе с GRUB_TIMEOUT=0 не выводят ничего, поэтому при просмотре консоли кажется, что сообщения ядра начинаются немедленно, а загрузчик был пропущен. GRUB_RECORDFAIL_TIMEOUT — это отдельный тайм-аут, используемый после неудачной загрузки; в облачных образах он также установлен в 0. Именно поэтому сервер, который не смог загрузиться, не останавливается и не ожидает ваших действий.
Если провайдер предоставляет последовательную консоль вместо графической, а вы всё равно ничего не видите, значит, GRUB пишет в терминал, который вам недоступен. Добавьте обе строки вместе: первая выбирает устройства вывода, а вторая настраивает порт:
GRUB_TERMINAL="console serial"
GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1"Теперь при каждой загрузке будет добавляться 10 секунд ожидания. Верните значение тайм-аута в 0, когда закончите работу.
Более безопасные альтернативы редактированию загрузчика
Изменение параметров загрузчика на машине, доступной только по SSH, — это самый рискованный вариант из всех описанных. Существуют более простые решения, которые обычно устраняют саму причину проблемы.
Зафиксируйте версии пакетов ядра. Если ваша цель — «не устанавливать более новое ядро», сообщите об этом пакетному менеджеру, а не загрузчику.
apt list --installed 2>/dev/null | grep -E '^linux-(image|headers|generic|virtual|kvm|aws|azure|gcp|oracle)'
sudo apt-mark hold linux-image-virtual linux-headers-virtual
apt-mark showholdИспользуйте те имена, которые вывела первая команда, так как в облачных образах часто устанавливается версия virtual или kvm, а не generic. Если новое ядро появилось после обновления образа и вы подозреваете, что изменился сам релиз, это не так, поскольку точечный релиз — это уже установленные вами обновления, включенные в новый установочный носитель, и он не предлагает серверу ничего, чего не было доступно неделями ранее. Зафиксированный пакет игнорируется командой apt upgrade, которая сообщает об этом с помощью The following packages have been kept back:, а также пропускается автоматическими обновлениями в Ubuntu. У этого решения есть цена: зафиксированное ядро перестает получать исправления безопасности, поэтому рассматривайте это как временную меру с установленным сроком и снимайте фиксацию командой sudo apt-mark unhold. Если вы избегаете обновлений ядра из-за того, что перезагрузки вызывают простой, а не из-за проблем с конкретной версией, live kernel patching на VPS решит эту задачу.
Создавайте снапшот перед обновлением. Снапшот позволяет восстановить систему за считанные минуты без ввода команд в консоли и риска некорректного изменения загрузчика. Сделайте снапшот, обновитесь, перезагрузитесь, проверьте работу. Если новое ядро ведет себя некорректно, выполните откат — путь загрузки останется в исходном состоянии.
Используйте консоль или rescue-образ для неработающей системы. Если сервер перестал загружаться, конфигурация загрузчика — не то место, где нужно искать исправление. Процедура восстановления в этом случае является отдельной задачей: что делать, если VPS не загружается после обновления ядра.
Что может пойти не так и какие сообщения вы увидите
Ваши правки в /boot/grub/grub.cfg исчезли. Был установлен или удален пакет ядра, его скрипт сопровождения выполнил update-grub, и файл был пересоздан на основе исходных данных. Заголовок # DO NOT EDIT THIS FILE указывает на два исходных расположения. Редактируйте именно их.
grub-editenv: error: environment block too small. Файл /boot/grub/grubenv отсутствует или поврежден. Воссоздайте его с помощью sudo grub-editenv /boot/grub/grubenv create, затем снова задайте свое значение и подтвердите его командой sudo grub-editenv list.
Закрепленное ядро вызывает kernel panic с VFS: Unable to mount root fs on unknown-block(0,0). Запись, которую вы закрепили, указывает на ядро или initrd, которых больше нет на диске. Обычно это происходит, если пакет был удален, а идентификатор остался в grubenv. Для восстановления загрузитесь через консоль, выбрав рабочую запись, а затем удалите устаревшее значение.
uname -r не изменился после перезагрузки, хотя вы ожидали изменений. Проверьте три вещи в указанном порядке: содержит ли grub-editenv list все еще ваше значение или оно было перезаписано; отображается ли заданный вами идентификатор в текущем grub.cfg; содержит ли grub.cfg строку set default, которая считывает установленную вами переменную. Один из этих пунктов всегда объясняет причину.
Меню появилось само по себе после сбоя. GRUB записывает неудачную загрузку в grubenv как recordfail=1, что принудительно вызывает меню при следующей загрузке для вмешательства администратора. Очистите его командой sudo grub-editenv /boot/grub/grubenv unset recordfail, как только система придет в норму.
Единственная фраза, которую стоит запомнить: файл, который вы редактируете, — это не тот файл, который читает GRUB, и в облачных образах именно этот разрыв является источником путаницы. Сначала читайте сгенерированную конфигурацию. Все решения на этой странице следуют из того, что написано в ней на самом деле.
FAQ
Почему параметр GRUB_DEFAULT=1 не меняет ядро, с которым загружается мой VPS?
Потому что в облачных образах Ubuntu сгенерированный /boot/grub/grub.cfg часто содержит только одну запись загрузки, поэтому индекс 1 ни на что не указывает, и GRUB возвращается к первой записи. Проверьте это с помощью sudo grep -cE '^\s*(menuentry|submenu) ' /boot/grub/grub.cfg. Результат 1 подтверждает это. Причина заключается в GRUB_FORCE_PARTUUID, установленном поставщиком образа в файле в /etc/default/grub.d/, который переводит генератор на путь прямой загрузки вместо формирования полного списка установленных ядер. Найдите этот файл с помощью grep -rn GRUB_FORCE_PARTUUID /etc/default/grub /etc/default/grub.d/.
Как загрузиться с предыдущим ядром только один раз?
Выполните sudo grub-reboot '<identifier>' с идентификатором, скопированным из вашего grub.cfg, а затем перезагрузитесь, предварительно открыв консоль провайдера. GRUB очищает next_entry перед загрузкой, поэтому выбор применяется ровно к одной попытке, и ядро, вызвавшее kernel panic, не будет загружено повторно. Проверьте, что значение было записано, с помощью sudo grub-editenv list. Прежде чем полагаться на этот метод, выполните sudo grep -n next_entry /boot/grub/grub.cfg, так как образ, конфигурация которого не загружает grubenv, проигнорирует команду без вывода ошибки.
Что лучше использовать для фиксации: номер записи или идентификатор?
Идентификатор. Номера записей — это позиции в списке, который 10_linux перестраивает, помещая новые версии в начало. Поэтому установка или удаление любого ядра сдвигает их, а устаревший 1>2 будет указывать на существующую, но неверную запись без каких-либо предупреждений. Идентификаторы содержат версию ядра, поэтому они либо соответствуют нужному ядру, либо не находят совпадений. Выведите их список с помощью sudo grep -n menuentry_id_option /boot/grub/grub.cfg и скопируйте строку в кавычках, которая следует за каждой записью.
Безопаснее ли удерживать пакет ядра, чем менять настройки загрузчика?
Для достижения стандартной цели — да. sudo apt-mark hold linux-image-virtual linux-headers-virtual предотвращает установку более новых ядер, поэтому путь загрузки не меняется, и риск совершить ошибку при работе через консоль, к которой может не быть доступа, отсутствует. Сначала проверьте названия установленных пакетов ядер на вашем сервере с помощью apt list --installed, а затем подтвердите статус удержания с помощью apt-mark showhold. Обратная сторона заключается в том, что удерживаемое ядро не получает обновлений безопасности, поэтому заранее определите, когда вы выполните sudo apt-mark unhold, прежде чем устанавливать удержание.