Всем привет! С момента публикации моей первой статьи про установку 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
    

Три варианта приведены, осталось собрать эти кусочки bash-скрипта в правильном порядке.

Ну а последний этап — включение в кикстарт файла /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-скриптов :)

Комментарии (2)


  1. 13werwolf13
    19.12.2022 13:14
    +1

    вот бы кто статейку об autoyast написал.. эх мечты мечты..


    1. alef13 Автор
      19.12.2022 22:18

      сусю щупал пару раз, но не моё это. Глянул сейчас autoyast, а там вся конфигурация в xml -- точно не моё!