Это заключительная часть цикла статей, посвященного интеграции одноплатных компьютеров Raspberry Pi 4 в наши дата-центры. Мы рассмотрели, что происходит при загрузке «малинки» с момента включения до загрузки в операционную систему. Создали собственный мини-дистрибутив, который загружается по TFTP-протоколу и работает напрямую из памяти. Поговорили о хуках (hooks) Kea DHCP-сервера, разобрались, как они работают и что нужно для их создания.

Теперь соберем эти знания в цельную картину: зачем нам кастомная опция 224 и как управлять переключением режимов загрузки по сети и с локальной SD-карты.

Использование опции 224


Самое важное для нас свойство опции 224 — уникальность. В процессе автоустановки операционной системы каждый раз возникает необходимость передать настройки сети, чтобы после перезагрузки сервера клиент смог подключиться к нему удаленно.

Проблема в том, что непонятно, на каком этапе их передавать. Загрузившись в ранее собранный мини-дистрибутив, мы попадаем в систему, которая ничем не отличается от остальных, загруженных на других серверах. Поэтому просто запросить сценарий дальнейшей загрузки у удаленного сервера мы не можем, ему не хватит данных, чтобы однозначно идентифицировать клиента.

С другой стороны, мы однозначно можем определить клиента по опции 82:


Рассмотрим еще раз, как происходит формирование DHCP-ответа клиенту, которым у нас выступает «малинка», и какое место в этом занимает наша кастомная опция.

  1. Клиент начинает загрузку и отправляет запрос на получение сетевых настроек.
  2. Kea DHCP принимает этот запрос, определяет по опции 97 (GUID) «своего» клиента и передает данные опции 82 дальше.
  3. Hook собирает URL-адрес, используя для этого опцию 82 и ее подопции, каждую из которых перекодирует в HEX-строку. Затем выполняет запрос по полученному URL.
  4. Удаленный сервер определяет клиента по полученной опции 82 и формирует для него уникальный ответ в виде JSON.
  5. Hook определяет из JSON-ответа опции, которые нужно передать клиенту, и подставляет их в DHCP-ответ (прежде всего 224).

    Эти этапы знакомы нам по предыдущим частям цикла. Теперь нам нужен условный агент, который бы использовал полученную опцию и выполнял последующий сценарий автозагрузки. Для этого расширим нашу схему:

  6. Клиент получает в DHCP-ответе опцию 224 (кастомная опция) и использует ее для дальнейшей загрузки.
  7. После получения клиентом DHCP-ответа (напомню, клиентом у нас выступает мини-дистрибутив Buildroot) запускается агент, который принимает опцию 224.
  8. Агент использует опцию 224 к следующему запросу на сервер. Стоит отметить, что опция — это URL, который уже содержит в себе уникальный токен. Он и позволяет серверу однозначно идентифицировать клиента при втором запросе.
  9. Агент получает дальнейший сценарий и на его основе устанавливает операционную систему на сервер.

Для реализации агента обратимся к нашему кастомизированному образу Buildroot для загрузки по сети.

Агент Buildroot


Просмотрим еще раз структуру внешнего Buildroot-окружения, работающего напрямую из оперативной памяти:

├── configs
│   └── raspberrypi4_64_pxe_defconfig
├── external.desc
└── files
	└── root
    	    └── .bashrc

Теперь добавим в образ утилиту dhtest, которую мы использовали в предыдущей статье. Dhtest у нас будет использоваться скриптом для получения опции 224 из DHCP-ответа. Для этого создадим дополнительный пакет с тремя файлами, который добавит утилиту в собранный образ Buildroot:

package/dhtest/
├── Config.in
├── dhtest.hash
└── dhtest.mk

Config.in — файл описания пакета.

config BR2_PACKAGE_DHTEST
    bool "dhtest"
    help
	DHCP test utility.

	https://github.com/saravana815/dhtest

dhtest.hash — содержит хеш-сумму на исходники пакета, который будет скачан и распакован перед его сборкой.

dhtest.mk — Makefile, в котором описана сборка и установка пакета в итоговый образ.

Для того, чтобы основное buildroot-окружение знало о новом пакете, необходимо сообщить ему об этом. Для этого создадим в корне окружения два файла:

Config.in
source "$BR2_EXTERNAL_raspberrypi4_64_pxe_PATH/package/dhtest/Config.in"

external.mk
include$(sort $(wildcard$ (BR2_EXTERNAL_raspberrypi4_64_pxe_PATH)/package/*/*.mk))

Утилита dhtest обращается напрямую к сетевому интерфейсу, но служит исключительно для тестов. Поэтому после ее выполнения полученные сетевые настройки не сохраняются в системе. Чтобы второй вызов к внешнему серверу сработал, нужна предварительно настроенная сеть.

Наш мини-дистрибутив собран с системой инициализации systemd, воспользуемся ею и передадим настройку сети systemd-networkd. Создадим файл files/etc/systemd/network/20-wired.network со следующим содержимым:

[Match]
Name=eth0

[Network]
DHCP=yes

Теперь напишем скрипт, который будет использовать dhtest для получения опции 224. Разместим его в files/usr/local/bin/get_opt_224.sh:

#!/usr/bin/env bash

hex2ascii () {
    	I=0
    	while [ $I -lt ${#1} ]
    	do            	echo -en "\x"${1:$I:2}
            	let "I += 2"
    	done
}

jq_parse () {
    	jq -r 'map(select(."result-option-no"?)) | map(select(any(."result-option-no"; contains("224")))) | .[1]."result-data"' | tr -d ' '
}

hex_str=$(dhtest -c 97,hex,'52706934' -V -i eth0 -j 2>/dev/null | jq_parse)
hex2ascii "${hex_str}"

В нем мы предварительно определяем две вспомогательные функции. Первая служит для перекодирования HEX-строки в ASCII-представление. Вторая — фильтрует (на основе утилиты jq) значение опции 224 из переданного вывода. Далее мы вызываем утилиту dhtest с выводом в формат JSON, результат которой преобразуется из HEX в ASCII.

Теперь создадим скрипт, который будет заниматься автоустановкой операционной системы на SD-карту — в дальнейшем с нее будет загружаться «малинка». Устанавливать будем образ Ubuntu 20.04, подготовленный специально для установки на Raspberry Pi 4. Опишем скрипт в файле files/usr/local/bin/autoinstall.sh:

#!/usr/bin/env bash

IMG='https://cdimage.ubuntu.com/releases/20.04.3/release/ubuntu-20.04.3-preinstalled-server-arm64+raspi.img.xz'
curl "${IMG}" | xz -dc | dd of=/dev/mmcblk0

mount /dev/mmcblk0p2 /mnt

URL=$(get_opt_224.sh)
curl ${URL} > /mnt/etc/netplan/eth0.yaml|
echo 'network: {config:disabled}' > /mnt/etc/cloud/cloud.cfg.d/99-disable-network-config.cfg

sync && umount /mnt

В нем мы записываем на SD-карту готовый образ Ubuntu через утилиту dd. Затем монтируем корневую файловую систему из установленной системы.

Через ранее созданный скрипт get_opt_224.sh получаем URL, который используется для второго обращения к внешнему серверу. В ответ на запрос сервер возвращает конфигурационный файл netplan, который записывается в свежеустановленную операционную систему. Также отключаем настройку сети через cloud-init, чтобы избежать возможных конфликтов с netplan.

После подготовки скрипта автоустановки добавим его в автозапуск при старте нашего buildroot-образа. Для этого создадим файл files/etc/systemd/system/autoinstall.service:

[Unit]
Description = Run autoinstall script with option 224
After = network.target

[Service]
Type = oneshot
RemainAfterExit = yes
ExecStart = /usr/local/bin/autoinstall.sh

[Install]
WantedBy = multi-user.target

Теперь необходимо добавить в образ используемые нами утилиты curl и jq, в том числе добавленный ранее dhtest. Вернемся в папку нашего внешнего buildroot-образа и добавим их в профиль raspberrypi4_64_pxe_defconfig:

BR2_PACKAGE_LIBCURL=y
BR2_PACKAGE_LIBCURL_CURL=y
BR2_PACKAGE_COREUTILS=y
BR2_PACKAGE_JQ=y
BR2_PACKAGE_DHTEST=y

Сборка образа, как мы помним, осуществляется из основного buildroot-окружения через запуск:

cd ${BUILDROOT}

make BR2_EXTERNAL=${EXT_BUILDROOT} raspberrypi4_64_pxe_defconfig

make

После загрузки полученного образа по TFTP у нас в автоматическом режиме запустится установка Ubuntu на SD-карту. После ее завершения мы можем перезагрузить «малинку» и попасть по сети в свежеустановленную систему.

Переключение режимов загрузки


Нет, не получится. После перезагрузки Raspberry Pi 4 еще раз загрузит buildroot-образ по TFTP-протоколу, и автоустановка операционной системы пойдет по кругу. Нам нужно каким-то образом выключить режим загрузки по сети и включить загрузку напрямую с SD-карты.

Решение, которое напрашивается первым, — изменить через утилиту vcgencmd порядок загрузки и выставить первым запуск с SD-карты. Но у такого решения есть существенный недостаток: обратно поменять на загрузку по сети можно будет только при прямом подключении к плате. Нужно будет идти к стойке, подключить клавиатуру и монитор, собственную SD-карту с операционной системой (мы не может делать это из-под клиентской), загрузиться и только тогда снова запускать утилиту vcgencmd.

Более того, у клиента пропадает возможность самостоятельно переустановить систему или установить другой дистрибутив. Для этого ему придется постоянно обращаться к техподдержке. Хотелось бы, чтобы такими операциями можно было управлять полностью удаленно.

Для решения этой задачи нужно вспомнить, как осуществляется загрузка «малинки» по сети. Как только она получила сетевые настройки по DHCP, она начинает обращаться к TFTP-серверу за списком файлов, которые ей нужны для дальнейшей загрузки. Если она не может по каким-то причинам получить эти файлы, то «малинка» переходит к следующему варианту загрузки. Мы это указывали в файле bootconf.txt (файл конфигурации для rpi-eeprom-config)

BOOT_ORDER=0xf12 # <-- 2: netboot, 1: SD, f: in cycle
NET_BOOT_MAX_RETRIES=1

То есть нам нужен способ «включать» и «отключать» TFTP-сервер для Raspberry Pi 4 в зависимости от того, какой сценарий мы хотим получить. Если доступен TFTP, то начинается загрузка по сети, запускается buildroot-образ и идет автоустановка операционной системы на SD-карту. Если TFTP недоступен, то «малинка» пробует загрузиться по сети, завершает этот этап с ошибкой и переходит к следующему, в котором идет загрузка с SD-карты.

И у нас есть для этого инструмент — наш модуль для Kea DHCP-сервера, в котором мы запрашивали список опций для подмены от внешнего сервера. Вспомним, как выглядит ответ:

{
  "224": "somestring",
  "66": "0.0.0.0"
}

Первая опция (224) описана выше, она служит для передачи URL с уникальным токеном, который используется для второго запроса к внешнему серверу. Вторая (66) — адрес TFTP-сервера (TFTP server name). Учитывая, что ответ сервера уникален для каждого клиента, то мы можем гибко управлять вариантом загрузки для каждого сервера.

Здесь адрес 0.0.0.0 выступает адресом-заглушкой, который никуда не ведет. Когда сервер его получает, то сервер вынужден пропустить этап сетевой загрузки и перейти к загрузке с SD-карты. Если задан реальный адрес TFTP-сервера, то выполняется загрузка в buildroot-образ, выполняющий автоустановку.

Финал


В этом цикле статей я рассказал о том, как мы смогли интегрировать «малинки» в нашем дата-центре и через какие этапы проб и ошибок пришлось для этого пройти. Нужно было досконально изучить все этапы загрузки. Разобрать и сравнить ее с существующей для «обычных» серверов, чтобы понять точки пересечения, на которые мы можем повлиять. Совсем уж неожиданной стала необходимость доработки модуля Kea DHCP.

В самом начале, когда мы только изучали возможности одноплатников, рассматривались варианты частично пожертвовать автоматизацией. Но результат получился лучше ожидаемого. Да, со своими особенностями и ограничениями, но работа с «малинками» из клиентской панели управления мало чем отличается от «обычных» серверов. Это, определенно, можно считать успехом.

Если у вас остались вопросы по теме интеграции, задавайте в комментариях — обязательно отвечу.

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


  1. AlexGluck
    18.11.2021 00:05
    +1

    Может ли один клиент используя ваш механизм получить информацию другого клиента? Например образ клиента 2 на клиенте 1 манипулируя DHCP опцией 82?


    1. burlunder Автор
      18.11.2021 00:13
      +2

      Вся морока с опцией 224 как раз для этого. Опция 82 навешивается на нашем сетевом оборудовании. Даже если клиент как-то её подменит, до dhcp-сервера запрос придёт уже с нужным описанием. А дальше клиент идентифицируется через описание порта (которое контролируем мы), возвращает одноразовый токен (что исключает подбор) и только при запросе с ним возвращается уникальный сценарий автоустановки.