Як закріпити kernel для наступного запуску VPS
GRUB_DEFAULT не працює в Ubuntu cloud image. Перевірте реальні записи меню GRUB і закріпіть kernel для наступного запуску без ризику втратити SSH-доступ.
Що визначає, з якого kernel завантажиться ваш VPS
Наступний kernel, з якого завантажиться ваш VPS, визначається одним згенерованим файлом — /boot/grub/grub.cfg. Цей файл не редагують вручну. Редагуйте його вхідні файли та повторно генеруйте результат. В Ubuntu cloud image один із цих вхідних файлів надходить від постачальника образу. Він може зробити вибір у меню неактуальним. Саме тому GRUB_DEFAULT=1 з наступним update-grub нічого не змінюють на орендованому сервері, хоча ті самі два кроки працюють під час встановлення на ноутбуці.
Працюйте в такому порядку. Переконайтеся, що kernel можна вибирати самостійно. Прочитайте всі вхідні файли, зокрема додані постачальником. Перегляньте згенерований результат і порахуйте записи, які він фактично містить. Лише після цього вибирайте спосіб закріплення версії kernel. Якщо помилитися на машині, до якої ви підключаєтеся лише через SSH, знадобиться rescue console. Тому найбезпечніші варіанти наведено наприкінці цієї сторінки, і часто саме вони є правильними.
Спочатку перевірте, чи можете ви вибирати kernel для завантаження
uname -r
systemd-detect-virt
ls -1 /boot/vmlinuz-*systemd-detect-virt, kvm, qemu або xen означає, що ви використовуєте власний kernel, і все наведене нижче застосовне. lxc або openvz означає, що сервер використовує kernel хоста. Отже, у вас немає власного bootloader і немає чого налаштовувати для вибору kernel. У такому разі uname -r повідомляє версію, якої взагалі немає в /boot/vmlinuz-*, оскільки запущений kernel належить хосту, і жодне налаштування на вашому диску не може його змінити.
ls -1 /boot/vmlinuz-* містить фактичний список kernel, між якими можна вибирати. Якщо в ньому лише один рядок, попередній kernel уже видалено, і жодне налаштування bootloader не поверне його. Зазвичай це відбувається під час autoremove. Це варто врахувати, перш ніж видаляти старі kernel в Ubuntu на важливому сервері.
Файл, який ви редагуєте, не є файлом, який читає GRUB
/etc/default/grub містить звичайні присвоєння змінних shell. Це вхідні дані. /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Підключення виконується звичайною shell-командою, тому перемагає останнє присвоєння. Ubuntu cloud images постачають файли в цьому каталозі. У них після читання вашого файла встановлюються, зокрема, timeout і command line ядра. Ваше GRUB_TIMEOUT=10 у /etc/default/grub через мить перевизначається vendor-файлом, який встановлює це значення в 0. Наведена вище команда grep виводить точні присвоєння у вашому image, тому орієнтуйтеся на них, а не на це пояснення.
Практичне правило таке: зберігайте власні налаштування у файлі, який сортується останнім, наприклад /etc/default/grub.d/99-local.cfg, замість редагування /etc/default/grub. Тоді жоден файл, що постачається разом з image, не зможе застосувати налаштування після нього.
Чому 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 вказує генератору знаходити root-файлову систему за UUID розділу. Це значення безпосередньо записується в командний рядок ядра як 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 використовується образом вашого провайдера для пошуку root-файлової системи. Його видалення переводить машину на шлях пошуку замість цього режиму. Перш ніж виконувати update-grub фактично, створіть snapshot.
Якщо ваша єдина мета — пережити проблему з одним ядром, зупиніться на цьому та скористайтеся безпечнішими варіантами нижче. Перебудовувати меню завантаження на віддаленому сервері, щоб обійти наслідки одного оновлення, ризикованіше, ніж сама проблема того варта.
Чому номери записів не слід фіксувати
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 читає цю змінну, очищає її та зберігає очищене значення до завантаження будь-чого. Тому ядро, яке спричинило panic, не буде повторно вибране під час наступного завантаження. Ви отримуєте одну спробу, після чого система самостійно повертається до звичайного значення за замовчуванням.
Спочатку переконайтеся, що згенерована конфігурація взагалі читає цю змінну:
sudo grep -n -B2 -A5 'next_entry' /boot/grub/grub.cfgПотрібен рядок load_env і блок, який встановлює default зі значення next_entry. Якщо grep нічого не виведе, ваш образ не читає grubenv під час завантаження. Тоді grub-reboot буде прийнято в shell, але bootloader його проігнорує. Це той самий примусовий шлях прямого завантаження з попереднього розділу, який проявляється ще в одному місці.
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 — це інше налаштування, яке легко сплутати з цим. Воно зберігає щойно завантажений запис як новий запис за замовчуванням, тому запис за замовчуванням змінюється після кожного успішного завантаження. На сервері це означає, що автоматичне перезавантаження може непомітно змінити зафіксований вами вибір. Залиште це налаштування вимкненим, якщо саме цього ви не прагнете.
Фіксація за ідентифікатором все одно має одне обмеження. Якщо видалити ядро, на яке він посилається, ідентифікатор більше не визначатиметься, і система повернеться до першого запису. Тому також утримуйте відповідний пакет або виключіть це ядро з autoremove.
Налаштування меню в консолі провайдера
Для інтерактивного вибору меню має відображатися на екрані, але cloud images приховують його. Додайте ці параметри у файл, який обробляється останнім, а потім виконайте sudo update-grub.
GRUB_TIMEOUT=10
GRUB_TIMEOUT_STYLE=menu
GRUB_RECORDFAIL_TIMEOUT=10GRUB_TIMEOUT_STYLE=hidden разом із GRUB_TIMEOUT=0 повністю приховують меню. Тому користувач, який стежить за консоллю, одразу бачить повідомлення ядра й робить висновок, що bootloader було пропущено. GRUB_RECORDFAIL_TIMEOUT — це окремий тайм-аут, який використовується після невдалого завантаження. cloud images також встановлюють його в 0. Саме тому сервер, який не зміг завантажитися, усе одно не зупиняється й не чекає на ваш вибір.
Якщо провайдер надає serial console замість графічної і ви все одно нічого не бачите, GRUB виводить дані в термінал, якого ви не бачите. Додайте обидва рядки разом: перший вибирає вихідні пристрої, а другий налаштовує порт:
GRUB_TERMINAL="console serial"
GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1"Відтепер до кожного завантаження додаватиметься десять секунд. Після завершення роботи поверніть значення тайм-ауту до 0.
Безпечніші варіанти, ніж редагування bootloader
Зміна параметрів bootloader на машині, доступній лише через SSH, — найризикованіший варіант на цій сторінці. Є простіші рішення, і зазвичай вони усувають справжню проблему.
Зафіксуйте пакети kernel. Якщо потрібно «не встановлювати новіший kernel», повідомте про це package manager, а не bootloader.
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Використовуйте назви, які вивела перша команда, оскільки cloud images часто встановлюють варіант virtual або kvm, а не generic. Якщо новіший kernel з’явився після оновлення image і ви підозрюєте, що сама release непомітно змінилася, це не так, оскільки point release — це оновлення, уже додані до нового install media, і вона не надає вже пропатченому server нічого, чого йому не пропонували кілька тижнів тому. apt upgrade пропускає пакет, який має статус hold, і повідомляє про це через The following packages have been kept back:. unattended upgrades в Ubuntu також пропускає такий пакет. Наслідки реальні: kernel зі статусом hold перестає отримувати security fixes. Тому сприймайте це як паузу з визначеною датою завершення та зніміть hold командою sudo apt-mark unhold. Якщо ви уникаєте оновлень kernel через downtime під час перезавантаження, а не через проблему з конкретним kernel, скористайтеся live kernel patching на VPS.
Створіть snapshot перед оновленням. Snapshot відновлюється за кілька хвилин. Для цього не потрібно вводити команди в console, і немає ризику частково застосувати зміни bootloader. Створіть snapshot, виконайте оновлення, перезавантажте server і перевірте його. Якщо новий kernel працює неправильно, виконайте rollback. Boot path залишиться точно таким, яким був.
Для вже непрацюючої машини використовуйте console або rescue image. Якщо server уже не завантажується, виправляти bootloader config у ньому не можна. Цей recovery path потребує окремої процедури: що робити, якщо VPS не завантажується після оновлення kernel.
Що ламається і яке повідомлення ви побачите
Вашу зміну в /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.
Закріплене ядро аварійно завершує роботу з повідомленням VFS: Unable to mount root fs on unknown-block(0,0). Запис, який ви закріпили, посилається на kernel або 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, а в cloud image саме розбіжність між ними спричиняє плутанину. Спочатку прочитайте згенеровану конфігурацію. Усі рішення на цій сторінці ґрунтуються на її фактичному вмісті.
FAQ
Чому GRUB_DEFAULT=1 не змінює ядро, з якого завантажується мій VPS?
В Ubuntu cloud image згенерований /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 перед завантаженням, тому вибір застосовується лише до однієї спроби. Якщо ядро аварійно завершує роботу, його не буде повторно вибрано. Перевірте, що значення записано, за допомогою 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 і скопіюйте рядок у лапках, що йде після кожного рядка запису.
Чи безпечніше зафіксувати пакет ядра, ніж змінювати bootloader?
Для звичайного завдання — так. sudo apt-mark hold linux-image-virtual linux-headers-virtual повністю забороняє встановлення новішого ядра. Тому шлях завантаження не змінюється, і ризик помилки через консоль, до якої у вас може не бути доступу, відсутній. Спочатку перевірте назви встановлених flavour на власному сервері за допомогою apt list --installed, а потім перевірте встановлення hold за допомогою apt-mark showhold. Недолік полягає в тому, що зафіксоване ядро не отримує виправлень безпеки. Тому перед встановленням hold визначте, коли виконаєте sudo apt-mark unhold.