Внимание! Длиннопост!
Сегодняшний эпос готовился очень долго и даже пару раз перекраивался, менял дату публикации и объём. Сначала я планировал делать серию постов, но строить перекрёстные ссылки оказалось дольше и сложнее, чем склеить данную простыню из кода и текста. В общем, терпения Вам.
Кстати, если вдруг потребуется вернуться к какой-то определённой части поста, прикрутил простенькую навигацию по всему этому безобразию:
Часть I. Страсти по кикстарту.
Итак, Вашему вниманию (очень) долгий рассказ про мой опыт организации PXE-сервера для быстрого развёртывания операционной системы РЕД ОС 7.3 МУРОМ. Кроме запуска самого PXE-сервера, я поведаю как разместил в загрузчике кое-какие нужные инструменты, создал файл kickstart, а также организовал общий принцип настройки пользовательского пространства.
А ещё продемонстрирую собственную выключалку dhcp-сервера написанную на связке php и js. Немного коснусь создания маленького api-сервиса и его использования с помощью простой программы на python. И расскажу про создание и применение скриптов настройки системы и установки необходимых программ.
Черновики для этого триллера собирались в течении всего 2024 года. В целом, весь представленный здесь материал можно было бы счесть устаревшим ещё на старте подготовки публикаций, так как к тому моменту уже давно состоялся релиз РЕД ОС 8. Однако, в силу ряда причин я проводил все свои эксперименты исключительно с версией РЕД ОС 7.3 МУРОМ.
К прочтению в обязательном порядке: здесь очень подробная инструкция для создания PXE-сервера на основе сервера РЕД ОС, а здесь читаем про kickstart.
Помните — где-то тут я рассказывал про iVentoy и его магию? Можете даже не начинать его использовать для установки РЕД ОС. По-крайней мере, на версии 7.3 волшебство iVentoy отдыхает. Конечно, если разместить iso в папке образов iVentoy система попытается загрузиться. Терминал будет вести свой рассказ, но в какой-то момент сетевое соединение пропадёт и всё. Нет сети — нет pxe. Нет pxe — нет установки.
Поэтому я рекомендую создать отдельный PXE-сервер для установки системы и обращаться для этого исключительно к официальной документации дистрибутива (тык). И даже если все эти прописные истины (так или иначе) Вам уже знакомы…
В общем, как сказал один известный персонаж:
There is a difference between knowing the path and walking the path.
— Morpheus
Часть I. Страсти по кикстарту.
Прежде чем я приступил к созданию виртуального сервера для сетевой загрузки, я развернул установочный образ на флеш-носителе и загрузился с него. В моём случае для создания загрузочного носителя использовался Rufus. Целевой системой для загрузки была выбрана UEFI. Флеш-носитель записывался в режиме DD. Больше про установку как таковую ничего говорить не буду. Ознакомиться с доками можно здесь.
Как только Вы разметите диск, выберите часовой пояс, настроите ntp, зададите пароль для root, выберите имя системы и, наконец, определитесь с набором софта и окружения — можно начать установку. Все манипуляции, что вы проделали перед установкой системы — установщик бережно соберёт и запишет сюда:
/root/anaconda-ks.cfg
Рекомендую сразу после установки системы забрать этот файл и использовать в качестве шаблона для написания полноценного конфига развёртывания системы. Установочный флеш-накопитель больше не понадобиться. Следующая установка будет проходить уже с сервера.
Рассказывать про установку и первичную настройку сервера РЕД ОС 7.3 МУРОМ я не стану. Процесс установки не должен вызвать каких-либо проблем. Со своей стороны упомяну, что я проводил установку северной ОС без графического окружения на Hyper-V. Также опущу процесс создания самого PXE-сервера — в начале поста уже привёл ссылки на официальную документацию по его созданию.
Как только Вы полностью сконфигурировали сервер и запустили dhcp, tftp и httpd, а в качестве конфигов загрузки и меню использовали стандартные шаблоны из документации — начинается самое интересное. Подстройка всей этой красоты под свои нужды.
Передо мной стояла задача упростить установку операционной системы и максимально автоматизировать подготовку рабочих станций с учётом требований пользователей. Кроме того, нужно было и предусмотреть наличие в системе всех дополнительных программных пакетов и необходимых зависимостей для обеспечения работы в корпоративной среде.
Конечно, в идеале нужно провести тихую установку и ОС, и всего необходимого софта, а также обеспечить дальнейшую автоматизацию с помощью скриптов. Но это я слишком забегаю вперёд. Давайте обо всём по порядку.
Для начала предлагаю обратиться к моему варианту файла ../tftpboot/pxelinux.cfg/default:
default menu.c32 PROMPT 0 TIMEOUT 150 MENU TITLE PXE Menu LABEL REDOS 7.3 MENU LABEL REDOS 7.3 KERNEL images/REDOS/images/pxeboot/vmlinuz APPEND initrd=images/REDOS/images/pxeboot/initrd.img ramdisk_size=128000 ip=dhcp inst.repo=http://192.168.1.100/images/REDOS inst.ks=http://192.168.1.100/images/CONF/anaconda-ks.cfg LABEL GPARTED MENU LABEL GPARTED KERNEL images/GPARTED/live/vmlinuz APPEND initrd=images/GPARTED/live/initrd.img username=user boot=live union=overlay config components noswap noeject noprompt vga=788 fetch=http://192.168.1.100/images/GPARTED/live/filesystem.squashfs LABEL MEMTEST MENU LABEL MEMTEST KERNEL images/MEMTEST/memtest64.bin APPEND LABEL CLONEZILLA MENU LABEL CLONEZILLA KERNEL images/CLONEZILLA/live/vmlinuz APPEND initrd=images/CLONEZILLA/live/initrd.img boot=live username=user union=overlay config components quiet noswap edd=on nomodeset nodmraid locales= keyboard-layouts= ocs_live_run="ocs-live-general" ocs_live_extra_param="" ocs_live_batch=no net.ifnames=0 nosplash noprompt fetch=http://192.168.1.100/images/CLONEZILLA/live/filesystem.squashfs
Этот файл отвечает за меню и загрузку в режиме legacy. Обратите внимание, что здесь указаны пути к общей иерархии загрузчиков: целевой РЕД ОС, а также добавленных мною GParted (здесь инструкция по размещению образа на pxe), утилиты для диагностики ОЗУ MemTest и драгоценной CloneZilla.
В случае с MemTest предлагаю скачать вот этот пакет. Кстати, обращаю Ваше внимание, что настройки по загрузке MemTest самые простые.
Теперь очередь uefi — ../tftpboot/uefi/grub.cfg:
function load_video {
insmod efi_gop
insmod efi_uga
insmod video_bochs
insmod video_cirrus
insmod all_video
}
load_video
set gfxpayload=keep
insmod gzio
menuentry 'REDOS 7.3' {
linux images/REDOS/images/pxeboot/vmlinuz ip=dhcp kernel vmlinuz inst.ks=http://192.168.1.100/images/CONF/anaconda-ks.cfg inst.repo=http://192.168.1.100/images/REDOS
initrd images/REDOS/images/pxeboot/initrd.img
}
menuentry 'GPARTED' {
linux images/GPARTED/live/vmlinuz username=user boot=live union=overlay config components noswap noeject noprompt vga=788 fetch=http://192.168.1.100/images/GPARTED/live/filesystem.squashfs
initrd images/GPARTED/live/initrd.img
}
menuentry 'MEMTEST' {
linux images/MEMTEST/memtest64.efi
}
menuentry 'CLONEZILLA' {
linux images/CLONEZILLA/live/vmlinuz boot=live username=user union=overlay config components quiet noswap edd=on nomodeset nodmraid locales= keyboard-layouts= ocs_live_run="ocs-live-general" ocs_live_extra_param="" ocs_live_batch=no net.ifnames=0 nosplash noprompt fetch=http://192.168.1.100/images/CLONEZILLA/live/filesystem.squashfs
initrd images/CLONEZILLA/live/initrd.img
}
По сути, это та же менюшка, но уже для загрузки в режиме uefi. Принцип наполнения самих файлов абсолютно идентичен. Отличия только в синтаксисе.
Для наглядности — иерархию конфигураций и образов я построил так:

К любой из данных директорий и её содержимому можно обратиться по протоколу http и выдернуть любой файл с сервера, например, с помощью wget.
Cамое время взглянуть на anaconda-ks.cfg, отвечающий за автоматизацию установки системы:
# Generated by Anaconda 33.25.4
# Generated by pykickstart v3.30
#version=F33
# Графическая среда установщика
graphical
# Установка пакетов графического рабочего окружения
%packages
@^desktop-environment
%end
# Раскладка клавиатуры
keyboard --xlayouts='ru','us' --switch='grp:alt_shift_toggle'
# Язык системы
lang ru_RU.UTF-8
# Установка имени хоста
network --hostname=redos
# Сетевая установка
url --url="http://192.168.1.100/images/REDOS"
# Отключение первоначальной настройки
firstboot --disable
# Принятие лицензионного соглашения
eula --agreed
# Использование только sda
ignoredisk --only-use=sda
# Удаляем все существующие разделы
clearpart --all --initlabel
# Авторазметка диска
autopart
# NTP сервера
timesource --ntp-server=ntp1.vniiftri.ru
timesource --ntp-server=ntp2.vniiftri.ru
timesource --ntp-server=ntp3.vniiftri.ru
timesource --ntp-server=ntp4.vniiftri.ru
timesource --ntp-server=192.168.1.1
timesource --ntp-server=192.168.1.100
# Часовой пояс
timezone Europe/Astrakhan --utc
# Установка пароля root
rootpw --iscrypted ЗДЕСЬХЭШПАРОЛЯ
# Создание пользователя admin
#user --groups=wheel --name=admin --password=ЗДЕСЬТОЖЕХЭШПАРОЛЯ --iscrypted --gecos="admin"
%anaconda
pwpolicy root --minlen=6 --minquality=1 --notstrict --nochanges --notempty
pwpolicy user --minlen=6 --minquality=1 --notstrict --nochanges --emptyok
pwpolicy luks --minlen=6 --minquality=1 --notstrict --nochanges --notempty
%end
# Послеустановочные скрипты
%post
# Включение SSH
systemctl enable sshd
systemctl start sshd
# Отключение IPv6
echo "net.ipv6.conf.all.disable_ipv6 = 1" >> /etc/sysctl.conf
echo "net.ipv6.conf.default.disable_ipv6 = 1" >> /etc/sysctl.conf
# Настройка DNS-серверов
echo "nameserver 192.168.1.1" >> /etc/resolv.conf
echo "nameserver 192.168.1.100" >> /etc/resolv.conf
echo "nameserver 8.8.8.8" >> /etc/resolv.conf
echo "nameserver 8.8.4.4" >> /etc/resolv.conf
# Применяем настройки
sysctl -p
# Очистка кэша репозиториев
dnf clean all
# Обновление всех пакетов
dnf update -y
# Создание кэша репозиториев
dnf makecache
# Включение автоматических обновлений
dnf install -y dnf-automatic
systemctl enable dnf-automatic.timer
# Установка программы обновления скриптов
updatepath="/scripts/update.py"
updateurl="http://192.168.1.100/update.py"
mkdir -p /scripts
wget -q "$updateurl" -O "$updatepath"
if [ -f "$updatepath" ]; then
chmod +x "$updatepath"
"$updatepath"
fi
# Настройка root SSH
CONFIG_FILE="/etc/ssh/sshd_config.d/01-permitrootlogin.conf"
if [ -f "$CONFIG_FILE" ]; then
rm "$CONFIG_FILE"
fi
# Отключение окна приветсвия
URL="http://192.168.1.100/images/CONF/createnorunflag.py"
DESTINATION="/usr/local/bin/createnorunflag.py"
PROFILE="/etc/profile"
if wget -O "$DESTINATION" "$URL"; then
chmod +x "$DESTINATION"
if ! grep -q '/usr/bin/python3 /usr/local/bin/createnorunflag.py' "$PROFILE"; then
echo "/usr/bin/python3 /usr/local/bin/createnorunflag.py" >> "$PROFILE"
fi
fi
# Блокировка флэш-накопителей
RULES_FILE="/etc/udev/rules.d/99-usb.rules"
echo 'ENV{ID_USB_DRIVER}=="usb-storage",ENV{UDISKS_IGNORE}="1"' > "$RULES_FILE"
udevadm control --reload-rules
# Настройка панели MATE
panelpath="/usr/share/mate-panel/layouts"
panelurl="http://192.168.1.100/images/CONF/ventana.layout"
wget -q $panelurl -O $panelpath/ventana.layout
# Перевод SELinux в молчаливый режим и его отключение
sed -i "s/SELINUX=enforcing/SELINUX=disabled/" /etc/selinux/config
setenforce 0
# Уменьшение таймаута загрузки grub
if [ -f /etc/default/grub ]; then
sed -i 's/^GRUB_TIMEOUT=.*/GRUB_TIMEOUT=0/' /etc/default/grub
if ! grep -q '^GRUB_HIDDEN_TIMEOUT_QUIET=true' /etc/default/grub; then
echo "GRUB_HIDDEN_TIMEOUT_QUIET=true" >> /etc/default/grub
fi
if ! grep -q '^GRUB_HIDDEN_TIMEOUT=0' /etc/default/grub; then
echo "GRUB_HIDDEN_TIMEOUT=0" >> /etc/default/grub
fi
sed -i 's/^\(GRUB_THEME=.*\)/#\1/' /etc/default/grub
grub2-mkconfig -o /boot/grub2/grub.cfg
fi
%end
Пойду по порядку чтения конфигурационного файла. Обратите внимание на директиву graphical. Эту директиву породила родная анакондушка и она отвечает не только за графическое окружение установщика, как подумалось мне. Она отвечает за наличие графического окружения рабочего стола во всей операционной системе (возможно данное поведение встречается только в РЕД ОС).
Если Вы удалите замените директиву graphical на text, то после установки Вы получите операционку с консольным типом управления. Конечно можно накатить окружение и отдельно, но мы собрались здесь, чтобы упрощать себе работу…
Пропустим блок %packages, минуем настройки раскладки клавиатуры и языка системы. Это всё можно оставить без изменений, а теперь вносим изменения в директиву network:
network --hostname=redos
Здесь можно задать настройки сети, но в моём случае я ограничился только именем хоста. Следом встречаем директиву url, которая жаждет получить прямой путь к папке с установщиком. Передаем ей ссылку на папку с дистрибутивом:
url --url="http://192.168.1.100/images/REDOS"
Для того чтобы сразу после установки системы мы попадали на экран авторизации, необходимо отключить функцию первоначальной настройки и автоматизировать согласие с EULA:
# Отключение первоначальной настройки firstboot --disable # Принятие лицензионного соглашения eula --agreed
Двигаемся к разметке диска хоста. Мне требовалось настроить установщик таким образом, чтобы он полностью очищал системный диск и проводил авторазметку:
# Использование только sda ignoredisk --only-use=sda # Удаляем все существующие разделы clearpart --all --initlabel # Авторазметка диска autopart
Таким образом, диск будет полностью очищен от всех томов и текущих файловых систем.
Далее идут настройки времени системы. Это самая простая настройка. Я думаю, останавливаться на ней особо смысла нет. Всё, что нужно — задать корректные сервера точного времени и указать свой часовой пояс.
В блоке настройки пользователей я ничего не трогал, но на всякий случай добавил шаблон для создания пользователя. К сожалению, я не проводил тестирование методов создания шифрованных паролей без использования установщика. Хеши паролей я получил уже готовыми, когда проводил первичную установку операционной системы с флеш-накопителя.
Для автоматизации получения зашифрованного пароля можно попробовать воспользоваться этим скриптом:
#!/usr/bin/python3
import sys
import crypt
if len(sys.argv) != 2:
print("Использование: cryptpass.py <your_password>")
sys.exit(1)
password = sys.argv[1]
salt = crypt.mksalt(crypt.METHOD_SHA512)
hashed_password = crypt.crypt(password, salt)
print(hashed_password)
Модуль crypt устарел и будет удалён из Python версии 3.13. Будьте внимательны! Говорят, что весь функционал можно заменить пакетом passlib. Не проверял.
Общий лейтмотив для написания скрипта я почерпнул здесь. Во вселенной файловой системы моего PXE-сервера этот скрипт именуется cryptpass.py и живёт здесь:
/scripts/cryptpass.py
Разумеется, потребуется дать ему права исполняемого файла:
chmod +x /scripts/cryptpass.py
Работать со скриптом можно напрямую из терминала просто передав ему пароль, который необходимо защифровать:
/scripts/cryptpass.py ЭтоОченьСложныйПароль
Выполнив эту команду, в терминал вернётся хешированный пароль. Теоретически — должно сработать.
Вернёмся к конфигурации кикстарта. У нас остался блок %post. Сюда можно запихнуть любые постустановочные команды на родном и близком bash. Здесь Вас ограничивает только фантазия. К примеру, можно установить необходимые пакеты, настроить записи о dns-серверах, внести изменения в конфигурационные файлы операционной системы, отключить ipv6, поколдовать с системными демонами и многое-многое другое.
У меня %post выглядит так:
# Послеустановочные скрипты
%post
# Включение SSH
systemctl enable sshd
systemctl start sshd
# Отключение IPv6
echo "net.ipv6.conf.all.disable_ipv6 = 1" >> /etc/sysctl.conf
echo "net.ipv6.conf.default.disable_ipv6 = 1" >> /etc/sysctl.conf
# Настройка DNS-серверов
echo "nameserver 192.168.1.1" >> /etc/resolv.conf
echo "nameserver 192.168.1.100" >> /etc/resolv.conf
echo "nameserver 8.8.8.8" >> /etc/resolv.conf
echo "nameserver 8.8.4.4" >> /etc/resolv.conf
# Применяем настройки
sysctl -p
# Очистка кэша репозиториев
dnf clean all
# Обновление всех пакетов
dnf update -y
# Создание кэша репозиториев
dnf makecache
# Включение автоматических обновлений
dnf install -y dnf-automatic
systemctl enable dnf-automatic.timer
# Установка программы обновления скриптов
updatepath="/scripts/update.py"
updateurl="http://192.168.1.100/update.py"
mkdir -p /scripts
wget -q "$updateurl" -O "$updatepath"
if [ -f "$updatepath" ]; then
chmod +x "$updatepath"
"$updatepath"
fi
# Настройка root SSH
CONFIG_FILE="/etc/ssh/sshd_config.d/01-permitrootlogin.conf"
if [ -f "$CONFIG_FILE" ]; then
rm "$CONFIG_FILE"
fi
# Отключение окна приветствия
URL="http://192.168.1.100/images/CONF/createnorunflag.py"
DESTINATION="/usr/local/bin/createnorunflag.py"
PROFILE="/etc/profile"
if wget -O "$DESTINATION" "$URL"; then
chmod +x "$DESTINATION"
if ! grep -q '/usr/bin/python3 /usr/local/bin/createnorunflag.py' "$PROFILE"; then
echo "/usr/bin/python3 /usr/local/bin/createnorunflag.py" >> "$PROFILE"
fi
fi
# Блокировка флэш-накопителей
RULES_FILE="/etc/udev/rules.d/99-usb.rules"
echo 'ENV{ID_USB_DRIVER}=="usb-storage",ENV{UDISKS_IGNORE}="1"' > "$RULES_FILE"
udevadm control --reload-rules
# Настройка панели MATE
panelpath="/usr/share/mate-panel/layouts"
panelurl="http://192.168.1.100/images/CONF/ventana.layout"
wget -q $panelurl -O $panelpath/ventana.layout
# Перевод SELinux в молчаливый режим и его отключение
sed -i "s/SELINUX=enforcing/SELINUX=disabled/" /etc/selinux/config
setenforce 0
# Уменьшение таймаута загрузки grub
if [ -f /etc/default/grub ]; then
sed -i 's/^GRUB_TIMEOUT=.*/GRUB_TIMEOUT=0/' /etc/default/grub
if ! grep -q '^GRUB_HIDDEN_TIMEOUT_QUIET=true' /etc/default/grub; then
echo "GRUB_HIDDEN_TIMEOUT_QUIET=true" >> /etc/default/grub
fi
if ! grep -q '^GRUB_HIDDEN_TIMEOUT=0' /etc/default/grub; then
echo "GRUB_HIDDEN_TIMEOUT=0" >> /etc/default/grub
fi
sed -i 's/^\(GRUB_THEME=.*\)/#\1/' /etc/default/grub
grub2-mkconfig -o /boot/grub2/grub.cfg
fi
%end
Логически я разбил его на субблоки и снабдил каждый из них соответствующим комментарием. В шапке блока я включаю SSH, отключаю IPv6, настраиваю DNS-сервера, включаю автоматические обновления системы.
Далее начинаются развлечения со скриптами и редактированием общих настроек установленной системы. Прежде всего я размещаю в структуре системы программу для обновления скриптов, конфигурационных файлов и дистрибутивов. Об этой программе поговорим чуть позже.
Следующий этап это разрешение работы пользователя root в ssh-сессии. В дальнейшем это можно отключить, но на данном этапе я счёл данный ход вполне оправданным.
Двигаясь ниже по постустановке, я отключаю окно приветствия операционной системы и блокирую монтирование флэш-накопителей (тык). Предвосхищая вопросы — да, при такой блокировке накопителей, токены ЭЦП продолжают работать.
Думаю, что об отключении окна приветствия нужно рассказать более подробно. Вот этот франгмент:
# Отключение окна приветствия
URL="http://192.168.1.100/images/CONF/createnorunflag.py"
DESTINATION="/usr/local/bin/createnorunflag.py"
PROFILE="/etc/profile"
if wget -O "$DESTINATION" "$URL"; then
chmod +x "$DESTINATION"
if ! grep -q '/usr/bin/python3 /usr/local/bin/createnorunflag.py' "$PROFILE"; then
echo "/usr/bin/python3 /usr/local/bin/createnorunflag.py" >> "$PROFILE"
fi
fi
Данный «кирпичик» скачивает с сервера скрипт createnorunflag.py, размещает среди системных команд и добавляет директиву для его выполнения в файл /etc/profile.
Так я обеспечил, чтобы каждый раз при входе пользователя выполнялся код указанного py-скрипта. Содержимое файла createnorunflag.py:
import os
flag_path = os.path.expanduser("~/.redoswelcome/norun.flag")
flag_dir = os.path.dirname(flag_path)
if not os.path.exists(flag_dir):
os.makedirs(flag_dir)
if not os.path.exists(flag_path):
with open(flag_path, 'w') as f:
f.write('This flag file was created on first run.\n')
Этот скрипт создаёт «файл-флажок«, наличие которого проверяет утилита redoswelcome перед тем, как запуститься. Иными словами, если файл ~/.redoswelcome/norun.flag нашёлся — значит окна настроек и конфигурирования системы Вы не увидите.
В файле кикстарта я предусмотрел также и отключение SELinux, но это уже вопрос Вашего подхода к обеспечению безопасности системы и контроля доступа. Возможно, что Вам этот субблок и не понадобится.
Что касается настройки панели задач графического окружения MATE, то здесь я тоже приложил руку. Например, удалил с панели виджет (апплет?) управления рабочими пространствами, чтобы пользователи не заплутали, а также разместил ярлыки для запуска файлового менеджера Caja и браузера Chromium. Остальные апплеты расставил в привычном порядке.
Содержимое файла ventana.layout, который отвечает за конфигурацию панели задач:
[Toplevel bottom] expand=true orientation=bottom size=32 [Object show-desktop] object-type=applet applet-iid=WnckletFactory::ShowDesktopApplet toplevel-id=bottom position=0 panel-right-stick=true locked=true [Object menu-bar] object-type=applet applet-iid=BriskMenuFactory::BriskMenu toplevel-id=bottom position=1 locked=true [Object clock] object-type=applet applet-iid=ClockAppletFactory::ClockApplet toplevel-id=bottom position=2 panel-right-stick=true locked=true [Object menu-separator] object-type=separator toplevel-id=bottom position=3 locked=true [Object file-browser] object-type=launcher launcher-location=/usr/share/applications/caja-browser.desktop toplevel-id=bottom position=4 locked=true [Object web-browser] object-type=launcher launcher-location=/usr/share/applications/chromium-browser.desktop toplevel-id=bottom position=5 locked=true [Object window-list] object-type=applet applet-iid=WnckletFactory::WindowListApplet toplevel-id=bottom position=6 locked=true [Object notification-area] object-type=applet applet-iid=NotificationAreaAppletFactory::NotificationArea toplevel-id=bottom position=7 panel-right-stick=true locked=true [Object st-separator] object-type=separator toplevel-id=bottom position=8 panel-right-stick=true locked=true
Именно этот файл будет размещён в системе с помощью вот этого фрагмента в кикстарте:
# Настройка панели MATE panelpath="/usr/share/mate-panel/layouts" panelurl="http://192.168.1.100/images/CONF/ventana.layout" wget -q $panelurl -O $panelpath/ventana.layout
Закрывая тему постустановочного кордебалета, скажу, что я совсем скрыл меню загрузчика системы GRUB:
# Уменьшение таймаута загрузки grub
if [ -f /etc/default/grub ]; then
sed -i 's/^GRUB_TIMEOUT=.*/GRUB_TIMEOUT=0/' /etc/default/grub
if ! grep -q '^GRUB_HIDDEN_TIMEOUT_QUIET=true' /etc/default/grub; then
echo "GRUB_HIDDEN_TIMEOUT_QUIET=true" >> /etc/default/grub
fi
if ! grep -q '^GRUB_HIDDEN_TIMEOUT=0' /etc/default/grub; then
echo "GRUB_HIDDEN_TIMEOUT=0" >> /etc/default/grub
fi
sed -i 's/^\(GRUB_THEME=.*\)/#\1/' /etc/default/grub
grub2-mkconfig -o /boot/grub2/grub.cfg
fi
Данная мера была направлена на ускорение и упрощение загрузки операционной системы.
На этом с файлом anaconda-ks.cfg всё. Теперь можно спокойно загружаться с сервера, выбрав сетевой адаптер основным загрузочным устройством и любоваться полностью автоматизированным процессом установки операционной системы. Как только система будет установлена перед Вами предстанет экран авторизации.
Очень важный момент: убедитесь, что в меню загрузки PXE-сервера были заданы корректные пути, как к установочным файлам, так и к файлу anaconda-ks.cfg. Иначе чуда не произойдёт.
Думали, что это конец истории? Извините, но нет. Предлагаю двигаться дальше…
Часть II. Тумблер для DHCP.
Изначально в инфраструктуре планировалось использовать два независимых PXE-сервера: один с нашим старым знакомым, увесистой пачкой iso-образов и коллекцией расшаренных папок с софтом, а второй должен отвечать исключительно за развёртывание РЕД ОС и администрирование рабочих станций с ней.
И вот, чтобы на этапе загрузки компьютера с сетевого адаптера не допустить получения ip-адреса «не с того PXE«, я решил написать простейший тумблер для управления dhcp прямо с индексной веб-странички. Веб-сервер так и так уже установлен в системе, а для полного комплекта не хватает только php.
Напоминаю, что в РЕД ОС служба веб-сервера Apache называется httpd (в отличии от ubuntu, где она же называется apache2).
Забегая вперёд — покажу внешний вид творения:

На страничке всего одна кнопка, которая отвечает за запуск и остановку демона dhcp, а также навигационные ссылки для прыжков по ресурсам сервера. Нечто размытое второй строкой — это данные об имени сервера и его ip-адрес. Вся страничка обновляется «на лету» в динамическом режиме.
Эта красота логически состоит из 7 файлов, считая favicon (можно использовать любой png-файл), js-скрипт и файл css. Начну с файла index.php:
<?php
$title = "Управление DHCP-сервером";
$serverip = $_SERVER['SERVER_ADDR'];
$webminport = 10000;
?>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="favicon.png" type="image/png">
<link type="text/css" rel="stylesheet" href="style.css">
<title><?php echo $title; ?></title>
<script src="script.js" defer></script>
</head>
<body>
<h1><?php echo $title; ?></h1>
<div id="serverInfo" class="status"></div>
<div id="status" class="status"></div>
<div id="message" class="status"></div>
<button id="toggleButton" title="Переключить состояние DHCP-сервера">Переключить состояние</button>
<div id="server">
<a href="../images/" target="_blank" title="Открыть страницу сервера">http</a>
|
<a href="../images/CONF/anaconda-ks.cfg" target="_blank" title="Файл кикстарта" download>kickstart</a>
|
<a href="../api.php" target="_blank" title="API для вывода перечня скриптов">api</a>
|
<a href="../update.py" target="_blank" title="Обновление скриптов конфигурирования" download>update</a>
|
<a href="https://<?php echo $serverip; ?>:<?php echo $webminport; ?>" target="_blank" title="Webmin">webmin</a>
</div>
<div id="author">Автор: <a href="https://ngdream.ru" target="_blank" title="СерЁжкин код">ngdream.ru</a></div>
</body>
</html>
Оформление страницы в структуре style.css выглядит так:
body
{
font-family:Arial,sans-serif;
background-color:#f0f0f0;
color:#333;
text-align:center;
padding:50px;
}
.status
{
margin:20px 0;
font-size:24px;
}
button
{
background-color:#4CAF50;
border:none;
color:white;
padding:15px 32px;
text-align:center;
text-decoration:none;
display:inline-block;
font-size:16px;
margin:4px 2px;
cursor:pointer;
border-radius:10px;
transition:background-color 0.3s ease;
}
button:hover
{
background-color:#CD5C5C;
}
#author
{
margin-top:auto;
padding:20px 0;
font-size:10pt;
color: #999;
}
#author a
{
color:#999;
text-decoration:none;
}
#author a:hover
{
color:#CD5C5C;
}
#server
{
margin-top:auto;
padding:20px 0;
font-size:10pt;
color: #333;
}
#server a
{
color:#333;
text-decoration:none;
}
#server a:hover
{
color:#CD5C5C;
}
#serverInfo
{
margin-bottom:30px;
font-size:11pt;
color:#333;
}
И, наконец, основной элемент — script.js, который отвечает за автоматизацию работы всего этого хулиганства:
// Переменные
let isDhcpActive;
let serverName = '';
let serverIP = '';
// Функция для обновления статуса службы DHCP-сервера
function updateStatus()
{
fetch('get_dhcp_status.php')
.then(response => response.text())
.then(status => {
// Сравнение полученного статуса с 'active' для определения состояния
isDhcpActive = (status.trim() === 'active');
// Установка текста статуса на странице в зависимости от состояния
const statusText = isDhcpActive ?
'Статус DHCP-сервера: <strong style="color:#4CAF50;">Active</strong>' :
'Статус DHCP-сервера: <strong style="color:#CD5C5C;">Inactive</strong>';
document.getElementById('status').innerHTML = statusText;
// Обновление текста и стиля кнопки в зависимости от статуса DHCP
const toggleButton = document.getElementById('toggleButton');
if (isDhcpActive)
{
// Текст кнопки
toggleButton.innerText = 'Остановить сервер';
// Цвет кнопки для остановки
toggleButton.style.backgroundColor = '#CD5C5C';
// Цвет при наведении
toggleButton.onmouseover = function()
{ toggleButton.style.backgroundColor = '#4f0014'; };
// Возвращение к исходному
toggleButton.onmouseout = function()
{ toggleButton.style.backgroundColor = '#CD5C5C'; };
}
else
{
// Текст кнопки
toggleButton.innerText = 'Запустить сервер';
// Цвет кнопки для запуска
toggleButton.style.backgroundColor = '#4CAF50';
// Цвет при наведении
toggleButton.onmouseover = function()
{ toggleButton.style.backgroundColor = '#004524'; };
// Возвращение к исходному
toggleButton.onmouseout = function()
{ toggleButton.style.backgroundColor = '#4CAF50'; };
}
});
}
// Функция для получения информации о сервере
function updateServerInfo()
{
// Отправка запроса для получения информации о сервере
fetch('get_server_info.php')
// Обработка ответа как JSON
.then(response => response.json())
.then(data => {
// Сохранение имени сервера
serverName = data.name;
// Сохранение IP-адреса сервера
serverIP = data.ip;
// Обновление заголовка страницы и отображение информации о сервере
document.title = `Управление DHCP-сервером на ${serverName} (${serverIP})`;
document.getElementById('serverInfo').innerHTML = `${serverName} (${serverIP})`;
});
}
// Функция для переключения состояния DHCP-сервиса
function toggleDHCP()
{
// Отправка POST-запроса для переключения состояния DHCP
fetch('toggle_dhcp.php',
{
method: 'POST',
// Передача параметра для переключения
body: new URLSearchParams('toggle=true')
})
// Обработка ответа как текст
.then(response => response.text())
.then(message => {
// Отображение сообщения
document.getElementById('message').innerHTML = message;
// Обновление статуса после переключения
updateStatus();
});
}
// Очистка действий при загрузке страницы
window.onload = function()
{
// Информации о сервере
updateServerInfo();
// Статус DHCP-сервера
updateStatus();
document.getElementById('toggleButton').addEventListener('click', function(event)
{
// Предотвращение стандартного поведения кнопки
event.preventDefault();
// Вызов функции для переключения службы DHCP-сервера
toggleDHCP();
});
};
За получение текущего статуса демона dhcp отвечает скрипт get_dhcp_status.php с самым кратким содержанием:
<?php
echo trim(shell_exec("systemctl is-active dhcpd"));
?>
За информацию о сервере ответственен скрипт get_server_info.php:
<?php $serverName = gethostname(); $serverIP = $_SERVER['SERVER_ADDR']; echo json_encode(['name' => $serverName, 'ip' => $serverIP]); ?>
Непосредственным управлением dhcpd занимается — toggle_dhcp.php:
<?php
$dhcp_service_status = shell_exec("systemctl is-active dhcpd");
if ($_SERVER['REQUEST_METHOD'] === 'POST')
{
if (isset($_POST['toggle'])) {
if (trim($dhcp_service_status) === 'active')
{
shell_exec("sudo systemctl stop dhcpd");
#echo "DHCP-сервис отключен.";
}
else
{
shell_exec("sudo systemctl start dhcpd");
#echo "DHCP-сервис включен.";
}
}
}
?>
В скрипте toggle_dhcp.php мною закомментированы отладочные команды echo. Пусть полежат здесь на всякий случай.
Базовый функционал управления демоном есть, но в таком виде он не более чем набор скриптов, так как чтобы управлять службами в Linux нужно иметь права для выполнения команд от имени суперпользователя. В том числе с использованием команды sudo.
Чтобы магия заработала нужно узнать от чьего имени запускается веб-сервер разрешить этому пользователю управлять демоном dhcpd.
Узнать имя пользователя от которого запущен веб-сервер можно так:
ps aux | grep httpd
В моём случае веб-сервер запущен от имени пользователя apache. Теперь можно отредактировать файл sudoers так, чтобы пользователь мог выполнять только определённый набор команд.
Открываем файл в nano:
sudo nano /etc/sudoers
Добавляем туда строку:
apache ALL=(ALL) NOPASSWD: /bin/systemctl stop dhcpd, /bin/systemctl start dhcpd
Сохраняемся и покидаем редактор.
Теперь пользователь apache сможет управлять состоянием dhcpd и веб-мордашка будет выполнять предполагаемый функционал. В перечень дополнительных ссылок можно добавить что-то своё, поколдовав в div id=“server”.
Часть III. API в перспективе.
Когда я только начинал создавать загрузочный сервер и формировать рабочие станции, я не мог даже примерно представить количество скриптов, которые мне потребуются. Изначально планировался лишь один скрипт — settings.sh, но в последствии метод моноскриптинга себя не зарекомендовал. Пришлось создавать целый каскад скриптов и конфигураций.
Следующая проблема возникла в процессе тестирования и отладки скриптов. Я не продумал метод обновления файлов, в которые приходилось вносить изменения, а каждый раз вызывать wget в мои планы не входило.
Прежде чем начинать разработку программы обновления, я решил создать скрипт api.php, который бы мог считывать на сервере указанные мною папки с исходными файлами и формировать простой JSON:
<?php
header('Content-Type: application/json');
// Определяем IP-адрес сервера
$serverip = $_SERVER['SERVER_ADDR'];
// Пути к папкам
// Для расширения JSON - указать путь к новой папке
$directories = [
'SCRIPTS' => '/var/lib/tftpboot/images/SCRIPTS',
'SOFT' => '/var/lib/tftpboot/images/SOFT',
'1C' => '/var/lib/tftpboot/images/1C',
'DESKTOP' => '/var/lib/tftpboot/images/DESKTOP'
];
$baseUrl = 'http://' . $serverip . '/images/';
// Инициализируем массив для хранения списка файлов
$fileList = [];
// Проходим по всем директориям
foreach ($directories as $label => $directory) {
// Проверяем, существует ли директория
if (!is_dir($directory)) {
echo json_encode(['error' => "Директория $label не найдена"]);
exit;
}
// Получаем список файлов
$files = array_diff(scandir($directory), array('..', '.'));
// Массив для файла этой директории
$fileList[$label] = [];
foreach ($files as $file) {
// Полный путь к файлу
$filePath = $directory . '/' . $file;
// Проверяем, является ли файлом (а не директорией)
if (is_file($filePath)) {
// Добавляем информацию о файле в массив
$fileList[$label][] = [
'name' => $file,
'size' => filesize($filePath),
'modified' => date("Y-m-d H:i:s", filemtime($filePath)),
'checksum' => hash_file('sha256', $filePath),
'url' => $baseUrl . $label . '/' . urlencode($file)
];
}
}
}
// Возвращаем список файлов в формате JSON
echo json_encode($fileList);
?>
В дальнейшем в этот скрипт можно с лёгкостью добавить необходимые папки или убрать те, что уже утратили свою актуальность. Для этого достаточно подстроить под свои нужды массив в переменной $directories. Чуть выше я приводил пример иерархии моего PXE-сервера. Можете ознакомиться с перечнем директорий, которые попали в карусель api, а какие нет.
Обратившись по http к этому скрипту можно получить файл с вот такой структурой и, примерно, вот такого вида:
{
"SCRIPTS": [
{
"name": "settings.sh",
"size": 3034,
"modified": "2024-11-14 10:24:44",
"checksum": "1355a7389c7d03c2283ad953c3e7b0e2f189762025e67875bc4f97609b097327",
"url": "http://192.168.1.100/images/SCRIPTS/settings.sh"
}
],
"SOFT": [
{
"name": "librtpkcs11ecp.rpm",
"size": 2388828,
"modified": "2024-11-12 09:40:51",
"checksum": "18967652980a16d0e8633ab74f95601bcfa2f7b9152c814690689f03883b552a",
"url": "http://192.168.1.100/images/SOFT/librtpkcs11ecp.rpm"
}
],
"1C": [
{
"name": "1cestart.cfg",
"size": 59,
"modified": "2024-11-13 11:16:54",
"checksum": "48c4c627cfda40cabdf948c8a7b2860b9c822677f325ee762a6d122274a076ac",
"url": "http://192.168.1.100/images/1C/1cestart.cfg"
},
{
"name": "base.v8i",
"size": 240,
"modified": "2024-11-13 10:20:57",
"checksum": "4b4ef9991af995e904d7ab4374d99364bb2ce2670b4454210e4bb4f1eb26033c",
"url": "http://192.168.1.100/images/1C/hospital_pr1.v8i"
}
],
"DESKTOP": [
{
"name": "chromium.desktop",
"size": 13065,
"modified": "2024-11-14 10:25:49",
"checksum": "374a2df53ef3060ed9ce8c344cb6c3cd40b692c5a7556cc2f47634d79622b663",
"url": "http://192.168.1.100/images/DESKTOP/chromium.desktop"
},
{
"name": "share.desktop",
"size": 128,
"modified": "2024-11-14 11:13:44",
"checksum": "3035de924d83798929cf019a718636ea8905bc1554ce06b210309e5e9d4c9955",
"url": "http://192.168.1.100/images/DESKTOP/share.desktop"
}
]
}
Наполнение файла зависит исключительно от содержимого папок, объявленных в файле api.php. Думаю, по форматам файлов понятно для каких целей служат те или иные файлы.
Логически я поделил данные в JSON на те, с которыми в дальнейшем будет работать программа обновления и те, которые были бы полезны для сбора статистических данных. Обновление файлов можно построить на имени, url-адресе и расчете контрольной суммы, а вот размер и дата модификации файлов — пригодятся для формирования статлиста.
Код моей программы update.py для обновления скриптов и конфигов:
#!/usr/bin/python3
import os
import glob
import requests
import hashlib
import stat
import shutil
import subprocess
# URL API для получения JSON
api_url = "http://192.168.1.100/api.php"
# Локальные директории для сохранения файлов
local_directory_scripts = "/scripts"
local_directory_soft = "/distrib"
local_directory_1c = "/1c_config"
local_directory_desktop = "/desktop"
# Функция удаления файлов и директорий, не упомянутых в массиве
def remove_unlisted_files(directory: str, existing_files: set) -> None:
all_items = glob.glob(os.path.join(directory, '*'))
for item_path in all_items:
item_name = os.path.basename(item_path)
if item_name not in existing_files:
try:
if item_name == "update.py":
print(f'Файл {item_name} нельзя удалять.')
continue
if os.path.isdir(item_path):
shutil.rmtree(item_path)
print(f'Удалена директория: {item_path}')
else:
os.remove(item_path)
print(f'Удален файл: {item_path}')
except Exception as e:
print(f'Ошибка при удалении {item_path}: {e}')
else:
print(f'Элемент сохранен: {item_path}')
# Функция для вычисления контрольной суммы файла
def calculate_checksum(file_path):
sha256 = hashlib.sha256()
try:
with open(file_path, 'rb') as f:
while chunk := f.read(8192):
sha256.update(chunk)
except Exception as e:
print(f'Ошибка при чтении файла {file_path}: {e}')
return None
return sha256.hexdigest()
# Создание локальных директорий, если не существуют
os.makedirs(local_directory_scripts, exist_ok=True)
os.makedirs(local_directory_soft, exist_ok=True)
os.makedirs(local_directory_1c, exist_ok=True)
os.makedirs(local_directory_desktop, exist_ok=True)
# Получение данных из API
try:
response = requests.get(api_url)
response.raise_for_status()
except requests.RequestException as e:
print(f"Не удалось получить данные с API: {e}")
exit()
files_data = response.json()
# Множества для хранения имен файлов, которые будут сохранены
existing_scripts = set()
existing_soft = set()
existing_1c = set()
existing_desktop = set()
# Функция для обработки файлов
def process_files(file_data, local_directory, existing_set, set_permissions=False):
for file_info in file_data:
file_name = file_info['name']
file_url = file_info['url']
file_checksum = file_info['checksum']
local_file_path = os.path.join(local_directory, file_name)
existing_set.add(file_name)
if os.path.exists(local_file_path):
local_checksum = calculate_checksum(local_file_path)
if local_checksum is None or local_checksum != file_checksum:
print(f"Файл '{file_name}' существует и контрольная сумма не совпадает. Загружаем заново.")
else:
print(f"Файл '{file_name}' уже существует и контрольная сумма совпадает. Пропускаем загрузку.")
continue
else:
print(f"Файл '{file_name}' не существует. Загружаем.")
print(f"Скачивание '{file_name}'...")
try:
file_response = requests.get(file_url)
file_response.raise_for_status()
with open(local_file_path, 'wb') as f:
f.write(file_response.content)
print(f"Файл '{file_name}' успешно скачан.")
if set_permissions:
os.chmod(local_file_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR |
stat.S_IRGRP | stat.S_IXGRP |
stat.S_IROTH | stat.S_IXOTH)
print(f"Права на выполнение установлены для '{file_name}'.")
except requests.RequestException as e:
print(f"Ошибка при скачивании файла '{file_name}': {e}")
except Exception as e:
print(f"Ошибка при сохранении файла '{file_name}': {e}")
# Обработка всех файлов из блока SCRIPTS
process_files(files_data['SCRIPTS'], local_directory_scripts, existing_scripts, set_permissions=True)
# Обработка всех файлов из блока SOFT
process_files(files_data['SOFT'], local_directory_soft, existing_soft, set_permissions=False)
# Обработка всех файлов из блока 1C
process_files(files_data['1C'], local_directory_1c, existing_1c, set_permissions=False)
# Обработка всех файлов из блока DESKTOP
process_files(files_data['DESKTOP'], local_directory_desktop, existing_desktop, set_permissions=False)
# Удаление файлов, которые не перечислены в JSON
remove_unlisted_files(local_directory_scripts, existing_scripts)
remove_unlisted_files(local_directory_soft, existing_soft)
remove_unlisted_files(local_directory_1c, existing_1c)
remove_unlisted_files(local_directory_desktop, existing_desktop)
Проводить разбор кода я не буду. Все блоки и функции снабжены комментариями.
Конечно, код программы очень далёк от всех основных принципов масштабирования. Но такой метод распространения файлов очень помог мне избежать рутинной синхронизации и ускорил процесс отладки и передачи скриптов на (пока ещё) тестовые машины прямо в процессе установки системы.