Як закріпити ядро, яке наступним завантажить VPS
В Ubuntu cloud image GRUB_DEFAULT може нічого не змінювати. Перевірте реальні записи меню та безпечно закріпіть ядро для наступного запуску VPS.
Що визначає, яке ядро завантажить ваш VPS
Ядро, яке ваш VPS завантажить наступного разу, визначається одним згенерованим файлом — /boot/grub/grub.cfg. Редагувати цей файл не потрібно. Змініть його вхідні дані та згенеруйте файл повторно. В Ubuntu cloud image один із таких вхідних файлів надходить від постачальника образу. Він може зробити вибір пункту меню неактуальним. Тому 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 означає, що ваш сервер використовує ядро хоста. Тому у вас немає власного bootloader і нічого закріплювати. У такому разі uname -r повідомляє версію, якої взагалі немає у /boot/vmlinuz-*, оскільки запущене ядро належить хосту, і жодне налаштування на вашому диску не може його змінити.
ls -1 /boot/vmlinuz-* містить фактичний список ядер, між якими можна вибирати. Якщо в ньому лише один рядок, попереднє ядро вже видалено, і жодне налаштування bootloader не поверне його. Зазвичай це відбувається під час autoremove. Перед очищенням старих ядер на Ubuntu на важливому для вас сервері варто зрозуміти, як працює цей процес: очистити старі ядра на 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Підключення файлів виконується звичайною оболонкою, тому перемагає останнє присвоєння. Образи Ubuntu для хмарних середовищ містять файли в цьому каталозі та встановлюють такі параметри, як timeout і командний рядок ядра, уже після обробки вашого файла. Ваше 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 вказує генератору знаходити кореневу файлову систему за 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 потрібен образу вашого провайдера для пошуку кореневої файлової системи. Його видалення переводить систему на шлях пошуку замість цього механізму. Перед фактичним запуском 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 читає цю змінну, очищає її та зберігає очищене значення до завантаження будь-чого. Тому ядро, яке спричиняє паніку, не буде повторно вибране під час наступного завантаження. Ви отримуєте одну спробу, після чого машина самостійно повертається до звичайного варіанта за замовчуванням.
Спочатку переконайтеся, що згенерована конфігурація взагалі читає цю змінну:
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 listgrub-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 — це інший параметр, який легко сплутати з цим. Він зберігає щойно завантажений запис як новий типовий, тому типовий запис змінюється після останнього успішного завантаження. На сервері це означає, що автоматичне перезавантаження може непомітно змінити зафіксований вибір. Не вмикайте цей параметр, якщо саме цього не потрібно.
Фіксація за ідентифікатором усе одно має один недолік. Якщо видалити ядро, на яке він посилається, ідентифікатор більше не визначатиметься, і система повернеться до першого запису. Тому також утримуйте відповідний пакет або виключіть це ядро з автоматичного видалення.
Виведення меню на консоль провайдера
Для інтерактивного вибору меню має відображатися на екрані, але cloud-образи його приховують. Додайте ці рядки до файлу, який обробляється останнім, а потім виконайте sudo update-grub.
GRUB_TIMEOUT=10
GRUB_TIMEOUT_STYLE=menu
GRUB_RECORDFAIL_TIMEOUT=10Комбінація GRUB_TIMEOUT_STYLE=hidden і GRUB_TIMEOUT=0 взагалі нічого не відображає, тому той, хто спостерігає за консоллю, одразу бачить повідомлення ядра й робить висновок, що bootloader було пропущено. GRUB_RECORDFAIL_TIMEOUT — це окремий timeout, який використовується після невдалого завантаження. Cloud-образи також встановлюють його в 0. Саме тому сервер, який щойно не зміг завантажитися, не зупиняється й не чекає на ваші дії.
Якщо провайдер надає serial console, а не графічну консоль, і ви все одно нічого не бачите, GRUB виводить дані на terminal, якого ви не бачите. Додайте обидва рядки разом: перший вибирає outputs, а другий налаштовує port:
GRUB_TERMINAL="console serial"
GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1"Відтепер до кожного завантаження додаватиметься десять секунд. Після завершення поверніть timeout до 0.
Безпечніші варіанти замість редагування bootloader
Зміна параметрів bootloader на машині, до якої ви підключаєтеся лише через SSH, є найризикованішим варіантом на цій сторінці. Є простіші рішення, і зазвичай вони усувають справжню проблему.
Утримуйте пакети ядра. Якщо мета — «не встановлювати новіше ядро», вкажіть це 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. apt upgrade пропускає пакет, що перебуває на утриманні, і повідомляє про це через The following packages have been kept back:. Також його пропускає unattended upgrades в Ubuntu. Ціна цього рішення реальна: утримуване ядро не отримує security fixes, тому сприймайте це як паузу з визначеною датою завершення та зніміть утримання за допомогою sudo apt-mark unhold. Якщо ви уникаєте оновлень ядра через простої під час перезавантаження, а не через проблеми з конкретним ядром, скористайтеся live kernel patching на VPS.
Створіть snapshot перед оновленням. Snapshot відновлюється за кілька хвилин. Для цього не потрібно вводити команди в консолі, і немає ризику частково застосувати зміни bootloader. Створіть snapshot, виконайте оновлення, перезавантажте сервер і перевірте його роботу. Якщо нове ядро працює некоректно, виконайте відкат. Шлях завантаження буде точно таким самим, як до оновлення.
Для сервера, який уже не працює, використовуйте консоль або rescue image. Якщо сервер не завантажується, конфігурацію bootloader не виправляють у завантаженій системі. Для відновлення потрібна окрема процедура: що робити, якщо VPS не завантажується після оновлення ядра.
Що саме ламається і яке повідомлення ви побачите
Ваше редагування /boot/grub/grub.cfg зникло. Було встановлено або видалено пакет ядра, запущено його maintainer script 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). Запис, який ви закріпили, вказує на 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-середовища згенерований /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 перед завантаженням, тому вибір застосовується лише до однієї спроби, а ядро, яке завершується через 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 і скопіюйте рядок у лапках, що йде після кожного рядка запису.
Чи безпечніше утримувати пакет ядра, ніж змінювати bootloader?
Для типової мети — так. sudo apt-mark hold linux-image-virtual linux-headers-virtual не дає новішому ядру встановитися взагалі. Тому шлях завантаження не змінюється, і ризик помилки через консоль, до якої у вас може не бути доступу, відсутній. Спочатку перевірте назви встановлених flavour на власному сервері за допомогою apt list --installed, а потім підтвердьте утримання за допомогою apt-mark showhold. Компроміс полягає в тому, що утримуване ядро не отримує виправлень безпеки. Тому визначте, коли запустите sudo apt-mark unhold, ще до встановлення утримання.