Решил сделать зарисовку на тему корректного обновления системы на ригах (да и любых других подобных устройствах) из самой системы. Если быть точным, то просто позаписывать наработки за последний день на эту тему.
На старых ригах, в принципе, прокатывали апдейты прямо в живой системе. Приходилось лепить что-то в стиле
gunzip -c /tmp/rootfs.gz | dd of=/dev/sda2 oflag=sync bs=1M ; echo ok ; echo b > /proc/sysrq-trigger
и оно даже работало. Грубо, но работало.
В 9-м Debian’е, с его systemd и частыми обращениями на диск система начинала обижаться, когда этот самый диск внаглую начинали переписывать. Но с ростом числа ригов корректный апгрейд всего парка становится все актуальнее, так что решил проработать “правильный” вариант.
Правильный вариант заключается в перезаписи диска на стадии работы initrd. Раньше как-то избегал этой темы – частенько собирал ядра без необходимости в initrd, да и в целом во внутренностях не ковырялся. Решил исправить это упущение.
Итак… В initrd при сборке включается ряд скриптов. Часть из них может находиться в каких-то пакетах (тот же mdadm, к примеру) и находится в /usr/share/initramfs-tools/scripts/, для пользовательских есть каталог /etc/initramfs-tools/scripts/. В обоих случаях скрипты делятся на категории, определяющие момент выполнения. Об этом можно прочитать по ссылке.
Вкратце – обходятся каталоги *-top, *-premount и потом *-bottom – вначале init; потом в его теле boot, представленный в вариантах local или nfs. Иначе говоря, обход в моем случае идет так: init-top -> init-premount -> local-top -> local-premount -> local-bottom -> init-bottom. Вот где-то в районе local-top и можно размещать свои скрипты.
Особенность, с которой столкнулся, исследуя эту последовательность – ядро не подгружает автоматом нужные модули, как это делается в “большой” системе. Я написал скрипт, который сохраняет состояние системы на каждом этапе и потом сохраняет собранные данные на диск. И диск успешно монтировался только в bottom-секциях. Сохранив список модулей выяснил, что никакой поддержки ext4 на более ранних этапах еще нет – загрузкой занимаются какие-то еще скрипты. Подробнее не копал, но решил просто добавить в свой скрипт ручную загрузку модуля – после этого все заработало как надо.
Итоговый скрипт парсит /proc/cmdline на предмет наличия ключевого слова и если оно там есть – выполняет монтирование диска с данными и распаковку готовых образов на раздел с boot и rootfs, далее загрузка продолжается как обычно.
Чуть позже добавил еще один вариант – полную перезапись диска. Однако в этом случае перезаписываемые данные затерли бы и вычитываемый с диска образ, поэтому сделал чуть иначе – создается tmpfs по размеру сжатого образа (поэтому оперативной памяти должно хватать – возможно, стоит занулять пустое место на разделах диска, если файл получается сильно большим), образ копируется туда и исходный раздел отмонтируется – а далее с диском можно делать все, что угодно.
Осталось добавить в GRUB пункты меню с нужными триггерами. Вообще для кастомных записей есть файл /etc/grub.d/40_custom, но там придется продублировать штатный скрипт, создающий пункты меню, целиком. Поэтому я сделал чуть иначе – просто добавил в /etc/grub.d/10_linux после if’а с описанием recovery-строчки ряд своих (это почти в самом конце скрипта):
linux_entry "${OS}" "${version}" init-stopmining "disablemining=1 ${GRUB_CMDLINE_LINUX}"
linux_entry "${OS}" "${version}" init-NOKMS "disablemining=1 nomodeset=1 ${GRUB_CMDLINE_LINUX}"
linux_entry "${OS}" "${version}" init-sysupgrade "sysupgrade ${GRUB_CMDLINE_LINUX}"
linux_entry "${OS}" "${version}" init-imgupgrade "imgupgrade ${GRUB_CMDLINE_LINUX}"
Ключ init-* парсится сторонней функцией и то, что после тире, становится частью названия пункта меню.
Итого:
- Создаем /etc/initramfs-tools/scripts/local-top/sysupgrade.sh
- Правим /etc/grub.d/10_linux
- update-initramfs -k all -u
- update-grub
Маленькая особенность, чуть не забыл: скрипт выполняется в момент обновления образа initrd, стоит учитывать.
Каталоги с образами в системе размещаем в соответствии со своим скриптом. Далее grub-reboot 4 или grub-reboot 5, если использовать тот пример в 10_linux, что я привел выше.
Мой скрипт – напрашивается более аккуратный вариант и слияние обоих блоков в один, но уже решил не заморачиваться:
#!/bin/sh
grep -q sysupgrade /proc/cmdline && {
modprobe ext4
mkdir -p /run/sda3
mount /dev/sda3 /run/sda3
if [ -d '/run/sda3/sysupgrade' ]
then
for i in $(ls -1 /run/sda3/sysupgrade/sda[0-9]*)
do
type="$(basename "${i}" | sed 's/.*\.//g')"
if [ "${type}" == 'gz' ]
then
cmd='zcat'
elif [ "${type}" == 'bz2' ]
then
cmd='bzcat'
elif [ "${type}" == 'xz' ]
then
cmd='xzcat'
fi
${cmd} "${i}" > /dev/$(basename "${i}" | sed 's/\..*//g')
done
fi
sync
umount /dev/sda3
}
grep -q imgupgrade /proc/cmdline && {
modprobe ext4
mkdir -p /run/sda3 /run/sysimg
mount /dev/sda3 /run/sda3
if [ -d '/run/sda3/sysupgrade' ]
then
for i in $(ls -1 /run/sda3/sysupgrade/sda.*)
do
type="$(basename "${i}" | sed 's/.*\.//g')"
mount -t tmpfs tmpfs /run/sysimg -o size=$(du "${i}" | cut -f1)k
cp "${i}" /run/sysimg/
umount /dev/sda3
if [ "${type}" == 'gz' ]
then
cmd='zcat'
elif [ "${type}" == 'bz2' ]
then
cmd='bzcat'
elif [ "${type}" == 'xz' ]
then
cmd='xzcat'
fi
${cmd} /run/sysimg/$(basename "${i}") > /dev/$(basename "${i}" | sed 's/\..*//g')
sync
reboot
done
fi
}