Всем привет! С момента публикации моей первой статьи про установку linux через kickstart прошло почти полгода, и за это время были пересмотрены некоторые принципы, выявлены «косяки», появилось более глубокое понимание некоторых моментов установки. Все эти «tips & tricks» я и решил собрать в новой статье. В самом конце покажу, каким образом можно выводить информацию по ходу выполнения послеустановочных скриптов на графический экран инсталлятора. Не переключайтесь :)
Как обычно, будем экспериментировать на отечественной операционной системе REDOS-7.3.1, которая является некой смесью RedHat/CentOS 7.3 и Fedora 33 (а может, и посвежее). Также я не вношу изменений в установочный носитель — в каком виде его скачали с интернета, в таком и будем использовать. Всё самое свежее будем брать из внутреннего репозитория. Всё здесь написанное может быть реализовано иначе и/или вообще быть неприменимо к иной инфраструктуре. Возможно, некоторые вещи покажутся тривиальными — не ругайтесь, многие видят unix впервые, и им это важно.
▍ Версия формата кикстарта
Очень важный нюанс — это версия формата кикстарта. Если рассмотреть кикстарт с установленной системы, то в начале файла имеется комментарий:
#version=F33
Вроде просто комментарий, но хочу всё же заострить на нём внимание. Как правило, поиск по ключевым словам в Google относительно формирования конфигурационного файла kickstart приводит на сайт RedHat-а, где мы интуитивно находим документацию по redhat 7.3 и начинаем писать kickstart, используя её в качестве основы. Я сам так делал, и это в корне неправильно. Ориентироваться надо именно на версию формата кикстарта.
Полный перечень директив смотрим здесь Welcome to Pykickstart’s documentation! Выбираем, что нам ближе: для REDOS это Fedora версии 33 — далее ознакамливаемся как с самими командами, так и с их параметрами.
По тексту встречаются вот такие строчки:
New in version RedHatEnterpriseLinux6.
Deprecated since version Fedora29.
Removed in version Fedora34.
Думаю, тут комментировать нечего — и так всё понятно. Самое важное то, что это первоисточник, а различные кусочки, которые пишут люди добрые на Stack Overflow и других форумах, могут не срабатывать на вашей системе именно из-за версии формата кикстарта (вернее анаконды, которая работает с определённой версией kickstart).
▍ Пользовательские скрипты
Анаконда даёт нам возможность выполнять пользовательские скрипты в 3 различных местах (жирным выделены называния секций):
- %pre — выполняется фактически до установки. Сеть, если ставите по сети, поднята. Диск не размечен. Экраны с запросом параметров клавиатуры/языка/источника установки/времени и т. д. нам ещё не предъявлены и даже X-сервер не запущен;
- %pre-install — диск размечен и смонтирован в
/mnt/sysroot/ (/mnt/sysimage/)
, копирование файлов ещё не началось. Все необходимые параметры системы уже собраны и настроены; - %post — новая система установлена в
/mnt/sysroot/
. Эти скрипты особенные — их можно запускать как в chroot, так и вне chroot'а.
В документации вскользь упомянуто, что может быть несколько секций %post-скриптов, чтобы была возможность запускать их как в chroot-окружении, так и вне его. Выполняются %post-скрипты по порядку появления в kickstart. Про несколько %pre и %pre-install секций точно не скажу, т. к. не проверял, хотя в документации есть строка — «Each %pre-install section is required to be closed with a corresponding %end».
В процессе установки при выполнении данных секций (скриптов) в каталоге /tmp/ появятся файлы вида ks-XXXXXX и ks-XXXXX.log — сам скрипт и лог (stdout, stderr) его выполнения. К сожалению, лог запишется после завершения скрипта, и в онлайне его не посмотреть. После завершения установки все журналы анаконды, включая наши скрипты и их логи, будут скопированы в установленную систему и доступны в каталоге
/var/log/anaconda/
. Это информация, полезная для выяснения причин, по которым установка пошла не по плану.▍ Определяем источник установки
В моей инфраструктуре установка производится с локального носителя. Этим локальным носителем может быть мультизагрузочный диск, флешка или DVD-диск. Соответственно, эти три варианта и необходимо проработать в кикстарт-файле, иначе установка споткнётся при выборе источника и продолжить её в большинстве случаев не удастся.
В общем случае всё выполняется в 3 приёма: определяем, откуда идёт инсталляция, формируем соответствующую конфигурацию в файл /tmp/install-media, подключаем файл /tmp/install-media в наш кикстарт.
- Установка с multiboot drive — определить довольно легко — в каталог /run/install/repo будет смонтирован наш iso-файл через /dev/loop0. В кикстарт включаем директиву harddrive с необходимыми параметрами. Кусок кода привожу:
REPODEV=$(df --output=source /run/install/repo| grep -v Filesystem) if [ "x${REPODEV}" == "x/dev/loop0" ] ; then SRCDEV=$(df --output=source /run/initramfs/isoscan | grep -v Filesystem) ISO=$(find /run/initramfs/isoscan -name redos-MUROM-7.3.1-* | cut -b 23-) echo "harddrive --dir=//${ISO} --partition=$(basename ${SRCDEV})" > /tmp/source-media fi
- Установка с DVD — также простой вариант. В каталог
/run/install/repo
уже монтируется/dev/sr0
(мы предполагаем, что на простой офисной машине всего один DVD-привод). Код:
SRCDEV=$(df --output=source /run/install/repo | grep -v Filesystem) [ "x${SRCDEV}" == "x/dev/sr0" ] && echo "cdrom" > /tmp/source-media
- Установка с флешки — очень интересный вариант. Если произвести установку вручную, то анаконда сформирует финальный kickstart-файл, в котором будет директива как для DVD-носителя —
cdrom
. Однако если загрузиться с таким кикстартом, то установка не пройдёт :( Рабочая строчка:
SRCDEV=$(df --output=source /run/install/repo | grep -v Filesystem) echo "harddrive --dir=// --partition=$(basename ${SRCDEV})" > /tmp/source-media
Ну а последний этап — включение в кикстарт файла /tmp/source-media — для всех вариантов одинаковый. Перед строчками с разбивкой диска просто добавить команду:
%include /tmp/source-media
▍ Установка софта
Штатное место — секция %packages. Прописываем необходимые пакеты, удаляем ненужные, используем группировку, wildcards — всё хорошо задокументировано. Однако надо понимать, что тут можно вписать только пакеты, которые находятся на носителе. Например, Firefox не поставите, т. к. в инсталляционном образе REDOS его просто нет. Также пакеты ставятся в своей первоначальной версии, и их потом потребуется проапдейтить.
Ситуацию можно исправить, если в кикстарт добавить строчку с дополнительным репозиторием — директива
repo
. Только я так не делаю — у меня нет доверия внутренним корпоративным каналам. Если что-то ломается на этом этапе, то мы получаем недоустановленную систему, которую не всегда можно исправить. Поэтому здесь только то, что на пластинке, а ставить дополнительный софт и апдейт системы будем из %post-скрипта, т. к. если он упал, то будет несложно посмотреть, в каком месте, и догнать вручную.▍ SELinux
Странная ситуация творится с модулем security у анаконды. Вроде по логам модуль отрабатывает, однако в процессах его не видно и директива
selinux
в кикстарте просто игнорируется. Мне не сложно отключить selinux в секции %post
:sed -i '/^SELINUX=/s/enforcing/disabled/g' /etc/sysconfig/selinux /etc/selinux/config
Однако модуль security обрабатывает также директивы
authselect
и realm
. Видимо, это «особенность» REDOS, и что будет, когда они мне внезапно понадобятся — не знаю.▍ Настройка сети — где можно, где нельзя
Т. к. я получаю kickstart по сети, то сеть у меня уже настроена. Где-то в документации на kickstart я читал, что поддержка сети ограничена, т. к. не настроены dns, но это немного неправда. Если в параметрах ядра указали директиву(ы)
nameserver
, то имена будут резолвиться на всех этапах. В противном случае общаемся с NetworkManager:DEV=$(nmcli -t -f DEVICE con)
nmcli c modify $DEV ipv4.dns-search corporation.ru
nmcli c modify $DEV ipv4.dnf <NS1>,<NS2>
nmcli general hostname linux.corporation.ru
nmcli d reapply $DEV
Следует обратить внимание, что это конфигурация на этапе установки системы. Настройки, которые в итоге будут у системы, указывают в директиве network. И опять я не рекомендую ей пользоваться, т. к. как только вы её задействуете, то начиная с секции %pre-install и далее у вас сменится ip-адрес (именно этот косяк в моей предыдущей статье). Если вы хотите устанавливать систему с одним адресом, а после инсталляции использовать другой, сделайте их смену в секции %post.
▍ Как закидывать файлы в систему
Иногда хочется, чтобы в системе появился файл, который не принадлежит ни одному пакету. Можно их стаскивать по сети через wget/curl, а можно генерировать из kickstart-а.
Всё просто: допустим, нам нужно сложить мааааленький бинарник в систему. Перегоняем этот бинарник в base64 и вставляем в необходимую скриптовую секцию с последующим декодированием в необходимое место:
base64 -d << EOF > /etc/sysctl.conf
IyBzeXNjdGwgc2V0dGluZ3MgYXJlIGRlZmluZWQgdGhyb3VnaCBmaWxlcyBpbgojIC91c3IvbGli
L3N5c2N0bC5kLywgL3J1bi9zeXNjdGwuZC8sIGFuZCAvZXRjL3N5c2N0bC5kLy4KIwojIFZlbmRv
cnMgc2V0dGluZ3MgbGl2ZSBpbiAvdXNyL2xpYi9zeXNjdGwuZC8uCiMgVG8gb3ZlcnJpZGUgYSB3
aG9sZSBmaWxlLCBjcmVhdGUgYSBuZXcgZmlsZSB3aXRoIHRoZSBzYW1lIGluCiMgL2V0Yy9zeXNj
dGwuZC8gYW5kIHB1dCBuZXcgc2V0dGluZ3MgdGhlcmUuIFRvIG92ZXJyaWRlCiMgb25seSBzcGVj
aWZpYyBzZXR0aW5ncywgYWRkIGEgZmlsZSB3aXRoIGEgbGV4aWNhbGx5IGxhdGVyCiMgbmFtZSBp
biAvZXRjL3N5c2N0bC5kLyBhbmQgcHV0IG5ldyBzZXR0aW5ncyB0aGVyZS4KIwojIEZvciBtb3Jl
IGluZm9ybWF0aW9uLCBzZWUgc3lzY3RsLmNvbmYoNSkgYW5kIHN5c2N0bC5kKDUpLgo=
EOF
chmod 644 /etc/sysctl.conf
В данном примере мы закидываем в систему бинарник /etc/sysctl.conf (конечно, это совсем не бинарник, но для демонстрации пойдёт).
С текстовыми файликами также всё просто:
cat << EOF > /etc/sysconfig/kernel
# UPDATEDEFAULT specifies if new-kernel-pkg should make
# new kernels the default
UPDATEDEFAULT=yes
# DEFAULTKERNEL specifies the default kernel package type
DEFAULTKERNEL=kernel
EOF
chmod 644 /etc/sysconfig/kernel
Конечно, можно и текстовые файлы завернуть в base64, но тогда их будет сложнее оперативно поправить в самом kickstart.
Файлы большого размера включать в kickstart не стоит — тут опять возвращаемся к варианту вытаскивания их из сети посредством curl.
Крохотные файлы (не больше 3 строк) проще и быстрее наполнить через echo:
echo "test test test" > /etc/issue.net
echo "test2 test2 test2" >> /etc/issue.net
Здесь, кстати, по тексту можно использовать переменные, а в случаях выше — нет.
▍ Особенности chroot
Секция %post штатно выполняется в chroot-окружении, однако это можно изменить ключом
--nochroot
. Плюсы и минусы есть у обоих вариантов:- chroot — можно использовать полный набор программного обеспечения из установленной системы. Все манипуляции с файлами системы осуществляются по их нормальным путям. Минусы — недоступен X-сервер, недоступны файлы системы установки;
- nochroot — полный доступ ко всем файлам установленной системы включительно. Доступен Xserver. Минусы — работа с нестандартными путями, не все утилиты это умеют, весьма урезанный набор доступных утилит.
При необходимости можно сделать несколько %post-секций с различными вариантами chroot — смотрите, как вам удобнее. Мне удобнее всё делать исключительно в chroot, а проблемные места я обхожу, и далее опишу как, на примере установки антивируса Касперского и донастройки сети.
▍ Установка антивируса Касперского
Корпоративный стандарт, и никуда от этого не деться. Необходимо установить всего два пакета: klnagent64 и kesl-gui — всё остальное подтянет dnf (хотя нет, надо предварительно поставить perl — в зависимостях его нет, а он нужен). Попытка подцепить маленький внутренний репозиторий и поставить эти пакеты в секции %packages завершится неудачей, как и просто попытка поставить их через dnf в скрипте %post. Проблема в скрипте POSTIN rpm-пакета kesl. В этом скрипте опрашивается работающий systemd для определения местонахождения каталога с юнитами. Понятно, что он в итоге отличается от физического пути устанавливаемой системы. Соответственно, установка антивируса завершается ошибкой и конфигурация systemd для запуска антивируса при старте системы не создаётся. Ситуация не страшная, т. к. для исправления достаточно добавить несколько команд:
cp /opt/kaspersky/kesl/shared/kesl-supervisor.service /usr/lib/systemd/system/
ln -sf /usr/lib/systemd/system/kesl-supervisor.service /usr/lib/systemd/system/kesl.service
systemctl enable kesl-supervisor.service
Первой командой мы копируем файл с параметрами сервиса в конфигурационный каталог systemd, вторая создаёт алиас kesl для kesl-supervisor, ну а третья через systemctl создаёт симлинк на сервис, чтобы он стартовал при старте системы. В данном случае используем systemctl, который управляет конфигурацией самостоятельно, а не через systemd.
▍ Настройка сети
Чуть выше я советовал отложить настройку реального адреса и прочих сетевых параметров в %post-секцию. Это было связано с тем, что изменения через директиву кикстарта network будут приняты сразу и тогда не получится устанавливать машину с одним адресом (например, в тестовом vlan) для переноса в другую сеть — в процессе настройки мы просто потеряем связь с корпоративной сетью.
Если попробовать настройку через nmcli, то ничего не удастся:
DEV=$(nmcli -t -f DEVICE con)
nmcli c modify $DEV ipv4.address 192.168.0.5/24
nmcli c modify $DEV ipv4.gateway 192.168.0.1
nmcli c modify $DEV ipv4.dns-search corporation.ru
nmcli c modify $DEV ipv4.dnf 192.168.0.2,192.168.0.3
nmcli general hostname linux.corporation.ru
nmcli d reapply $DEV
Конечно, вроде бы всё отработает, но результата после перезагрузки мы не получим. Дело в том, что nmcli как раз работает только с NetworkManager, который посредством своего плагина nm-settings-ifcfg-rh правит файлики в каталоге /etc/sysconfig/network-scripts/ifcfg-*.
Обойдёмся без посредников. Если кто забыл, читаем
/usr/share/doc/initscripts-10.00.6/sysconfig.txt
и натравливаем на файл конфигурации sed:CONFIG_ETH="/etc/sysconfig/network-scripts/ifcfg-$(nmcli -t -f DEVICE con)"
sed -i 's/\(IPADDR\)=.*$/\1=192.168.0.5/' $CONFIG_ETH
sed -i 's/\(PREFIX\)=.*$/\1=24/' $CONFIG_ETH
sed -i 's/\(DNS1\)=.*$/\1=192.168.0.2/' $CONFIG_ETH
sed -i 's/\(DNS2\)=.*$/\1=192.168.0.3/' $CONFIG_ETH
sed -i 's/\(GATEWAY\)=.*$/\1=192.168.0.1/' $CONFIG_ETH
sed -i '/IPV6INIT/s/yes/no/' $CONFIG_ETH
▍ /etc/skel
Кто знаком с Эви Немет (заочно :) тот помнит, что useradd не только заводит пользователя в системе, но также создаёт ему домашний каталог и наполняет его содержимым каталога /etc/skel/. Сейчас этим занимается pam_mkhomedir при первом входе пользователя в систему. Как наполнить /etc/skel файликами, написано выше. Помимо стандартных rc-файлов шелла, туда дополнительно складываю ярлычки рабочего стола, файл с закладками для файлового менеджера, шаблоны конфигурации стандартного софта. Следует учесть, что пользователи, созданные в кикстарте директивой user, не будут иметь этих добавок — при необходимости им придётся напихать это вручную и поправить права.
▍ Desctop Environment
Ещё одна интересная задача — настройка gnome/mate и всего что с ними связано. Залогиниться под пользователем и отконфигурировать соответствующими графическими тулзами это просто, но неинтересно, утомительно и неправильно. Нам необходимо решение: а) глобальное — для всех имеющихся и потенциальных пользователей системы, б) настраиваимое неинтерактивно, с помощью скриптов.
Если немного погрузиться в историю развития дистрибутива RH7, то узнаем, что примерно в это время произошёл переход от GConf, где хранились настройки Gnome, к связке dconf и GSettings. Расписывать их внутренний мир не буду — долго, да и сам не знаю ничего :) Перейдём сразу к делу.
Для определения необходимых настроек нам понадобится установленная система. Логинимся в графике под обычным пользователем и запускаем терминал, а на нём dconf с параметрами watch /. Дополнительно через меню «Настройки» запустим апплет настройки комбинаций клавиш клавиатуры. Стандартная комбинация блокировки экрана и запуска скринсэйвера —
<ctrl>+<alt>+<L>
— дважды кликнем и заменим её на <win>+<L>
:Как видно на скриншоте, dconf отобразил, какие настройки изменились. Значит нам необходимо их аккуратно внести немного в ином виде в конфигурационный файл для всех пользователей системы (имя файла произвольное, расположение /etc/dconf/db/profile/):
[org/mate/settings-daemon/plugins/media-keys]
screensaver='<Mod4>l'
Затем скомпилировать изменения:
dconf update
После этого все пользователи будут блокировать экран нажатием
<win>+<L>
, если не настраивали для себя комбинацию клавиш отдельно.Аналогично ищем, какие ещё полезные настройки можно вставить в тот же файл.
Однако не всё так просто — утилиты конфигурирования кое-что от нас скрывают. Чтобы увидеть полный состав «реестра» gsettings (да, мы превращаемся в винду :( ), необходимо запустить ещё один терминал и вызовом gsettings list-recursively получить все имеющиеся параметры. Далее выбираем понравившийся и меняем его, параллельно наблюдая dconf watch / в соседнем окошке:
В примере я предварительно вернул обратно комбинацию блокировки экрана на
<ctrl>+<alt>+<L>
. Узнав, что конкретно сменил у себя dconf, мы вносим эти изменения, как описано выше. Какие параметры gsettings за что отвечают, пытаемся определить интуитивно, вычитать на форумах, в документации/исходниках соответствующих программ.▍ Как отображать ход выполнения %post
Отображение хода выполнения %post-скрипта в графическом инсталляторе — очень интересная задача. Анаконда такую возможность не предоставляет, а на форумах народ такое хочет частенько, но ответов нет, кроме как выводить на текстовую консоль. Также хочу ещё раз отметить, что журналы выполнения скриптовых секций не посмотреть в режиме реального времени — анаконда скинет их на диск только после завершения выполнения соответствующего скрипта.
В процессе установки нам доступна утилита zenity, т. е. по сути осталось достучаться до X-сервера. Если секция %post выполняется вне chroot, то это несложно — достаточно указать параметр display с правильным адресом. Адрес мы узнаём на консоли, посмотрев параметры Xorg:
[anaconda root@linux ~]# pgrep -a Xorg
1921 /usr/libexec/Xorg -br -logfile /tmp/X.log :1 vt6 -s 1440 -ac -nolisten tcp -dpi 96 -noreset
[anaconda root@linux ~]#
Помимо адреса дисплея
:1
, там можно увидеть параметр -ac
— это хорошо, т. к. не нужно заморачиваться с безопасностью, и параметр -nolisten tcp
— это плохо, т. к. сразу ставит крест на возможность доступа к X-серверу из chroot-окружения (локальный сокет X-сервера находится вне файловой системы).Я пробовал изменить параметры запуска Xorg в коде анаконды из %pre-секции, но безрезультатно, т. к. анаконда грузит свои модули до выполнения этой секции. Также хотел выставить локальный сокет в сеть посредством утилиты socat — в дистрибутиве он есть, но вот в инсталляционный образ не доложили.
В итоге нашлась-таки лазейка — файловая система /mnt/sysroot доступна с обеих сторон, и значит мы можем использовать named pipe для общения нашего скрипта с zenity.
Есть правда одна сложность — каким образом осуществить выполнение скрипта с zenity в фоновом режиме. Теоретически мы это можем и даже документация на кикстарт говорит — отстреливайте стандартные вводы/выводы/ошибки и уходите в фон, в противном случае анаконда будет ждать завершения скрипта. Только вот опять nohup в систему инсталлятора не положили.
Снова произведём небольшой трюк — сделаем %pre-install-секцию такого содержания:
%pre-install
cat << EOF > /tmp/boa.sh
#!/bin/bash
mknod /mnt/sysimage/boa p
zenity --display=:1 --no-cancel --progress --title="%post" --percentage=0 < /mnt/sysimage/boa
rm -f /mnt/sysimage/boa
EOF
chmod +x /tmp/boa.sh
cat << EOF > /etc/systemd/system/post-progress.service
[Service]
Type=simple
ExecStart=/tmp/boa.sh
EOF
systemctl daemon-reload
systemctl start post-progress
%end
Как видно, тут формируется скрипт, в котором создаём на файловой системе named pipe (mkfifo нет, поэтому через mknod) и запускаем zenity с чтением этого пайпа. Для запуска в фоне делается простейший сервис systemd.
Вторая часть производится в %post-секции. Теперь она должна выглядеть примерно так:
%post
{
<команды>
echo -e "1\n# Этап 1"
<команды>
echo -e "20\n# Этап 2"
<всякие полезные команды>
echo -e "40\n# Этап 3"
<ещё команды>
echo -e "60\n# Этап 4"
<команды>
echo -e "100\n# Выполнено!!!"
} | tee -a -i /boa
%end
Собственно, изменений не так уж и много — последовательность команд собираем в скобки для того, чтобы pipe не закрывался, т. е. заменить вызовы echo… на echo с перенаправлением в /boa не получится, ибо каждый вызов будет открывать и закрывать pipe и, как следствие, завершение zenity при первом же обращении.
Перенаправление в /boa сделано через tee, чтобы не потерять стандартный вывод наших команд, который anaconda запишет в лог. Ну а сами вызовы echo предназначены для zenity, подробности в манах. Можно их сделать как в одну строчку, так и по отдельности.
Результат радует:
Также хочу обратить внимание на одну тонкость, которая стоила мне пары дней разбирательств: при использовании
zenity --progress
в параметрах не указывайте --auto-close
. Теоретически окно с червячком закрывается при достижении 100%, практически оно закроется при любом числе больше 99 в начале строки — у меня вылетало при выводе 127.0.0.1 в новую строчку. Отлавливал долго и информации практически никакой: в логе скрипта внезапный обрыв совсем в другом месте (надо учитывать буферизацию вывода), в логе самой анаконды завершение скрипта с ошибкой 141. Судя по коду ошибки 141 = 128 + 13 наш скрипт завершился по сигналу 13 — SIGPIPE, что говорит о попытки записи без «читателя», т. е. zenity ушёл на покой. Ну а далее прогоны установки без индикации и анализ логов.Конечно, такой ситуации можно избежать, если фильтровать вывод перед подачей zenity, но решил сильно не усложнять.
Единственный фильтр на awk написал для обработки
dnf -y update
— там реально самая долгая операция. Если не отображать ход её выполнения, тогда нет смысла вообще заниматься индикацией этапов выполнения kickstart. По поводу фильтрации есть ещё одно замечание — желательно делать небуферизированный вывод, иначе записи будут просто пролетать большими порциями (64к) и будет висеть одна последняя строчка до вывода следующего буфера. В awk для сброса буфера используется вызов system("")
. Выглядит как хак, но работает :) Как это делать в других утилитах, ещё не разбирался.▍ Завершение
На этом закругляемся, но тема неисчерпаема. Ещё раз прошу накидать в комментарии свои интересные решения для пополнения моих kickstart-скриптов :)