- PVSM.RU - https://www.pvsm.ru -
Мы двигаемся к финалу нашей саги об интеграции Raspberry Pi 4 в выделенные серверы. В первом тексте [1] я рассказал об отличиях процесса загрузки «малинок» от «классических» серверов. Во втором [2] — собрал образ, способный после загрузки файлов по TFTP-протоколу запускаться и работать из оперативной памяти. При этом показал, как его кастомизировать, добавляя нужные пакеты и файлы.
Теперь нужно воспроизвести поведение, которое мы показали на примере iPXE-скрипта.
Напомню основную часть скрипта, оставив самое важное.
isset 224 || goto noparameter
chain --autofree ${224}
В данном случае скрипт проверяет, задана ли опция 224 (определяется в ответе от DHCP-сервера). Если да, скрипт идет дальше и выполняется chain [3], который загружает по URI (значение задается как раз опцией) следующий образ и запускает его.
Опция кастомная, поэтому объясню, для чего она служит и как формируется. Значение 224 выбрано как первое свободное для частного использования в стандарте DHCP [4].
Использование опции удобнее всего пояснить на схеме:
Чтобы проделать это на практике, обратимся к Kea DHCP-серверу и его системе hook-модулей.
Когда нужно установить DHCP-сервер, Linux-дистрибутивы по умолчанию предлагают ISC DHCP [7] (ISC — Internet Systems Consortium [8]). За почти 20 лет существования он продолжает поддерживаться консорциумом. Это мощный и гибкий продукт, который позволяет не только гибко управлять опциями в ответе, но даже задавать собственные [9]. А через механизм dhcp-eval [10] можно определять собственную логику. Например, через execute [11] можно вызывать внешние скрипты с передать им аргументы.
Для схемы выше использование execute не подходит, так как эта команда не умеет возвращать данные обратно в сервер после выполнения.
Из-за этого и других подобных архитектурных ограничений основной фокус развития переключился на Kea. На странице его описания [12] обозначены отличия от прежнего проекта. Для первого знакомства на практике пригодится эта статья [13], с поправкой, что текущая актуальная версия 2.0. Тем, кто задумался о переходе, для облегчения переноса конфигов рекомендуем воспользоваться ассистентом KeaMA [14].
В каком-то смысле возможности Kea также ограничены. В документации по конфигурированию [15] мы не найдем ничего, что позволило бы сделать внешний вызов. Но она позволяет расширить функциональность через систему hook-модулей [16].
По сути, hook — это динамическая библиотека, подгружаемая в процессе обработки запросов. При этом может использоваться несколько модулей, в таком случае их порядок обработки определяется порядком в конфигурационном файле. Модуль может обращаться ко всему API, доступному ядру Kea, и возможности на этом уровне не ограничены.
Правда, для нас это вызов. Ядро Kea написано на C++, и нужно разбираться в его архитектуре на этом уровне.
При написании собственного хука нам придется постоянно обращаться к руководству по их созданию [17]. В его примерах также используется С++. Уже есть возможность использовать Python через kea_python [18], но здесь мы будем следовать руководству. Ориентироваться будем на уже готовый пример [19].
Рассмотрим файлы из директории src, где располагается основной код.
├── Makefile
├── pkt4_receive.cc
├── pkt4_send.cc
├── remopts_callouts.cc
├── remopts_callouts.h
├── remopts_common.cc
├── remopts_common.h
├── remopts_log.cc
├── remopts_log.h
├── remopts_messages.mes
└── version.cc
#include <hooks/hooks.h>
extern "C" {
int version() { return (KEA_HOOKS_VERSION); }
}
Наш хук — это библиотека, подгружаемая ядром Kea. Перед загрузкой нужно убедиться, что файл собран под ту версию Kea, которая его вызывает. Код каждой версии Kea содержит собственную версию для хуков в символе KEA_HOOKS_VERSION [21]. Как видно, функция version() использует ее, что гарантирует совпадение версий.
Загрузка и выгрузка модуля ядром происходит через функции load() и unload().
При вызове load() аргументом передается объект handle [23] класса LibraryHandle [24] от ядра. Он служит для регистрации собственных вызовов и получения параметров хука, заданных в конфигурационном файле Kea. В нашем примере мы используем только получение [25] параметров.
int load(LibraryHandle &handle) {
try {
ConstElementPtr param_url = handle.getParameter("url");
ConstElementPtr param_user_class = handle.getParameter("user_class");
ConstElementPtr param_machine_guid = handle.getParameter("machine_guid");
Дальше по коду эти параметры проходят простую проверку на соответствие типу. Так как смысл нашего хука в том, чтобы обращаться к внешнему серверу, то параметр url является обязательным и его отсутствие вызывает исключение.
В конфигурационном файле /etc/kea/kea-dhcp4.conf это будет соответствовать отрывку:
"hooks-libraries": [
{
"library": "/usr/lib/x86_64-linux-gnu/kea/hooks/libremote_opts.so",
"parameters": {
"url": "http://nginx/data",
"user_class": "test",
"machine_id": "RPi4"
}
}
],
На основе этих параметров создаются переменные, которые используются в дальнейшем при обработке DHCP-пакетов.
std::string conf_url;
std::string conf_user_class;
std::string conf_machine_guid;
При чтении файла выше remopts_callouts.cc можно заметить использование функций для вывода сообщений в лог — например, сообщение о том, что наш хук был успешно загружен.
LOG_INFO(remopts_logger, REMOPTS_LOAD);
Для работы функций нужно заранее создать общий логгер remopts_logger, используемый в остальных файлах.
isc::log::Logger remopts_logger("kea-hook-remote-opts");
Для удобства обращения к логгеру стоит использовать макросы (вроде LOG_INFO), заданные в файле macros.h [27] из кода ядра Kea.
Для отправки сообщений в лог, помимо логгера, требуются еще сами сообщения, созданные определенным образом. Чтобы упростить работу с ними, нам предлагается файл особого формата, который при компиляции будет преобразован в код.
$NAMESPACE isc::log
% REMOPTS_MSG remopts message: %1
A common message logger
Формат файла хорошо описан в документации [29]. Как можно заметить, сперва мы задаем пространство имен, в котором будут располагаться сообщения.
Строки, начинающиеся с символа «%», задают сами сообщения. Сперва идет идентификатор сообщения (здесь REMOPTS_MSG), который будет передаваться логгеру. Далее — текст, который попадает в вывод лога. При необходимости сообщению могут передаваться позиционные аргументы.
Для преобразования данного файла в код используется утилита kea-msg-compiler:
kea-msg-compiler remopts_messages.mes
После компиляции файла сообщений мы получаем готовый код С++, представленный в файле remopts_messages.cc, и связанный с ним заголовочный файл remopts_messages.h
На примере сообщения REMOPTS_MSG выше будет сгенерирован код:
namespace isc {
namespace log {
extern const isc::log::MessageID REMOPTS_MSG = "REMOPTS_MSG";
}
}
namespace {
const char* values[] = {
"REMOTEOPTS_MSG", "remopts message: %1",
NULL
};
const isc::log::MessageInitializer initializer(values);
}
В этом файле определены вспомогательные функции, используемые при обработке пакетов. Помимо функций, связанных с преобразованием HEX-строк, здесь также определена функция make_curl_request(), через которую происходит вызов к внешнему серверу.
Обработка входящих пакетов начинается с этого файла. В нем мы определяем функцию pkt4_receive() [32], вызываемую ядром Kea на приходящий DHCPv4-пакет. Аргументом передается объект handle типа CalloutHandle [33], который содержит контекст вызова на входящий пакет.
int pkt4_receive(CalloutHandle &handle) {
try {
Pkt4Ptr query4_ptr;
handle.getArgument("query4", query4_ptr);
Здесь контекст входящего пакета становится доступным через Pkt4Ptr. В нашем примере он используется чтобы определить, пришел ли запрос от «нашего» клиента или нет. Протокол DHCP широковещательный, и обрабатывать все входящие пакеты получается накладно. Нас интересуют только запросы, связанные с PXE загрузкой. «Свои» пакеты мы определяем по опциям 77 (user-class) [34] и 97 (uuid/guid) [35].
OptionPtr user_class_ptr;
OptionPtr uuid_guid_ptr;
user_class_ptr = query4_ptr->getOption(77);
uuid_guid_ptr = query4_ptr->getOption(97);
Далее, при наличии нужной опции, мы задаем новый контекст объекту handle. Он является общим для базовых функций (как увидим далее). Так мы можем через создание контекста передать значение guid_id, чтобы использовать его при формировании DHCP-ответа.
if (uuid_guid_ptr) {
string guid_id;
string guid_str = gethexOptionPtr(uuid_guid_ptr);
handle.setContext("guid_id", guid_str);
Аналогично задается контекст user_class_id и для опции user_class_ptr.
По аналогии с файлом pkt4_receive.cc здесь определена функция pkt4_send() [37], которая отвечает за формирование исходящего DHCPv4-пакета. Передается тот же объект handle, на основе которого в этот раз мы получаем контекст пакета responce4_ptr, формируемого для отправки.
int pkt4_send(CalloutHandle &handle) {
Pkt4Ptr response4_ptr;
handle.getArgument("response4", response4_ptr);
Далее мы обращаемся к объекту handle, чтобы получить из него ранее созданный контекст guid_id. При этом дополнительно проверяем, что его значение совпадает с conf_machine_guid. Это параметр, который мы получали ранее из файла remopts_callouts.cc и который соответствует параметру из конфигурационного файла Kea.
string guid_id;
bool guid_match = false;
try {
handle.getContext("guid_id", guid_id);
if (boost::algorithm::contains(guid_id, conf_machine_guid))
guid_match = true;
} catch (const std::exception &ex) {
LOG_INFO(remopts_logger, REMOPTS_MSG).arg("guid_id is missing");
}
Аналогичная схема используется для контекста user_class_id.
Далее мы получаем опцию 82 с ее субопциями. Напомню, что на ее основе мы однозначно идентифицируем клиента в PXE-сети. При этом проверяем как наличие самой опции 82, так и ранее созданные userclass_match и guid_match.
OptionPtr option82;
option82 = response4_ptr->getOption(82);
string final_url;
if (option82 and (userclass_match or guid_match)) {
OptionPtr option82sub1_ptr = option82->getOption(1);
OptionPtr option82sub2_ptr = option82->getOption(2);
OptionPtr option82sub9_ptr = option82->getOption(9);
Субопции нужны для формирования адреса final_url, который затем будет использован для вызова через libcurl к внешнему Web-серверу. Формируется адрес на основе conf_url (параметр url в конфигурационном файле Kea), к которому добавляются значения субопций, преобразованных в HEX-строку. Последнее необходимо для передачи внешнему серверу через GET HTTP-запрос.
if (option82sub1_ptr) {
final_url = final_url + "?sub82_1=" + gethexOptionPtr(option82sub1_ptr);
}
if (option82sub2_ptr) {
final_url = final_url + "&sub82_2=" + gethexOptionPtr(option82sub2_ptr);
}
if (option82sub9_ptr) {
final_url = final_url + "&sub82_9=" + gethexOptionPtr(option82sub9_ptr);
}
final_url = conf_url + final_url;
После всех подготовительных этапов происходит вызов к внешнему серверу. Ответ ожидается в формате JSON, который необходимо дополнительно распарсить. Для этого мы создаем объект root типа pt:ptree (property_tree [38] из библиотеки boost), в который копируется полученный от сервера JSON-ответ.
pt::ptree root;
try {
pt::read_json(ss, root);
} catch (const exception &ex) {
LOG_ERROR(remopts_logger, REMOPTS_MSG)
.arg("curl ERR invalid json response: "" + sstrim(ss) + """);
}
При обработке мы ожидаем ответ в следующем виде [39]. Здесь ключу соответствует номер опции, которую нужно включить в DHCP-ответ. Значение принимается пока только в виде строки.
{
"224": "somestring",
"66": "0.0.0.0"
}
Для подстановки опций мы проходимся в цикле по объекту root и получаем указанные по ключу опции:
for (auto &node : root) {
string first_ = node.first; // json key
string second_ = node.second.data(); // json value
OptionPtr option = response4_ptr->getOption(stoi(first_));
После определяем, задана ли уже эта опция или нет. В первом случае достаточно подменить данные на основе значения элемента (здесь second). Во втором случае необходимо предварительно создать опцию, после чего добавить ее в ответный DHCP-пакет.
if (option) {
option->setData(second_.cbegin(), second_.cend());
} else {
OptionBuffer buffer;
buffer.assign(second_.cbegin(), second_.cend());
option.reset(new Option(Option::V4, stoi(first_), buffer));
response4_ptr->addOption(option);
response4_ptr->pack();
}
На этом месте все действия выполнены и происходит выгрузка нашего хука через функцию unload(), описанную ранее. При появлении следующего DHCP-запроса хук загружается, и все повторяется заново.
Перед сборкой необходимо убедиться, что удовлетворены все зависимости (на примере Ubuntu 20.04).
apt install g++ make kea-common kea-dev
При использовании репозитария [40] от ISC последние два пакета можно заменить на isc-kea-common и isc-kea-dev.
apt install g++ make isc-kea-common isc-kea-dev
Так как код нашего хука опирается на библиотеки libcurl и boost, их также необходимо установить.
apt install libboost-dev libboost-system-dev libcurl4-openssl-dev
После достаточно перейти в директорию src и запустить команду make. После завершения в директории на уровень выше получим готовую библиотеку ../kea-hook-remote-opts.so
cd src && make
Для демонстрации и упрощения сборки/тестирования подготовлен файл docker-compose.yml [41]. В нем мы создаем отдельную сеть Kea, чтобы связать контейнеры kea-hook и dhtest и пускать через нее DHCP-трафик. Сами контейнеры при этом запускаются в привилегированном режиме. Это важно, так как сервер kea-dhcp4 и утилита dhtest требуют прямого доступа к сетевым интерфейсам для своей работы.
Запуск производится стандартно:
docker-compose up
После чего будет собрано два образа (kea-hook и dhtest), запустится nginx, выполняющий роль внешнего Web-сервера, и kea-hook, который запускает сервер kea-dhcp4 с модулем kea-hook-remote-opts.so.
Контейнер dhtest завершится после запуска, так как не является сервисом. Dhtest [42] — это утилита, предназначенная для тестирования серверов DHCP. Она позволяет формировать запрос к серверу с различными значениями и наблюдать содержимое ответа.
В файле test/Dockerfile.dhtest [43] приведены аргументы, которые формируют опции 12 и 82, используемые для тестов. В командной строке это будет соответствовать вызову:
dhtest --interface eth1 --verbose --timeout 30 -c "12,str,test" -c "82,hex,0103666f6f0203626172" --unicast
При использовании контейнера dhtest достаточно запустить его заново и изучить вывод:
docker container start --attach dhtest
Статья получилась не про сами «малинки». Ни опция 224, ни умение создавать хук-модули для Kea DHCP не требуются для загрузки Raspberry Pi 4 по сети. Более того, описанный здесь хук-модуль использовался в инфраструктуре Selectel еще до того, как появился вопрос интеграции «малинок» (хоть и не раз переделывался).
Но при использовании «пишек» проявилась особенность, решить которую без данного хука оказалось невозможно. Речь про переключение режимов загрузки по сети и с SD-карты. Поэтому обойти эту тему никак нельзя. Не говоря уже про то, что через него удалось максимально унифицировать поведение «малинок» и «стандартных» серверов.
В следующей, финальной, статье мы соберем все знания цикла текстов про интеграцию «малинок» и покажем, что происходит после загрузки Raspberry Pi 4 в Buildroot-образ. Заодно посмотрим, как решилась проблема с переключением режимов загрузки.
Автор:
burlunder
Источник [44]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/raspberry-pi/369502
Ссылки в тексте:
[1] первом тексте: https://habr.com/ru/company/selectel/blog/580398/
[2] втором: https://habr.com/ru/company/selectel/blog/582576/
[3] chain: https://www.ipxe.org/cmd/chain
[4] стандарте DHCP: https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml
[5] опции 82: http://xgu.ru/wiki/%D0%9E%D0%BF%D1%86%D0%B8%D1%8F_82_DHCP
[6] MAC-адресу: https://habr.com/ru/post/483670/
[7] ISC DHCP: https://www.isc.org/dhcp/
[8] Internet Systems Consortium: https://www.isc.org/
[9] задавать собственные: https://stackoverflow.com/questions/47871468/declare-dhcp-custom-option-and-configure-client-to-send-it
[10] dhcp-eval: https://kb.isc.org/v1/docs/isc-dhcp-44-manual-pages-dhcp-eval
[11] execute: https://kb.isc.org/v1/docs/isc-dhcp-44-manual-pages-dhcp-eval#REFERENCE:%20ACTION%20EXPRESSIONS
[12] описания: https://www.isc.org/kea/
[13] статья: https://habr.com/ru/post/458180/
[14] KeaMA: https://gitlab.isc.org/isc-projects/dhcp/-/wikis/kea-migration-assistant
[15] документации по конфигурированию: https://kea.readthedocs.io/en/kea-2.0.0/arm/config.html#
[16] систему hook-модулей: https://kea.readthedocs.io/en/kea-2.0.0/arm/hooks.html
[17] руководству по их созданию: https://reports.kea.isc.org/dev_guide/df/d46/hooksdgDevelopersGuide.html
[18] kea_python: https://github.com/davejohncole/kea_python
[19] пример: https://github.com/burlunder/kea-hook-remote_options/
[20] version.сс: https://github.com/burlunder/kea-hook-remote_options/blob/main/src/version.cc
[21] KEA_HOOKS_VERSION: https://reports.kea.isc.org/dev_guide/df/d46/hooksdgDevelopersGuide.html#hooksdgVersionFunction
[22] remopts_callouts.cc: https://github.com/burlunder/kea-hook-remote_options/blob/main/src/remopts_callouts.cc
[23] handle: https://github.com/burlunder/kea-hook-remote_options/blob/main/src/remopts_callouts.cc#:~:text=int%20load(LibraryHandle%20%26handle)%20%7B
[24] LibraryHandle: https://reports.kea.isc.org/dev_guide/d2/d6c/classisc_1_1hooks_1_1LibraryHandle.html
[25] получение: https://github.com/burlunder/kea-hook-remote_options/blob/main/src/remopts_callouts.cc#:~:text=load(LibraryHandle%20%26handle)%20%7B-,try%20%7B,-ConstElementPtr%20param_url%20%3D%20handle
[26] remopts_log.cc: https://github.com/burlunder/kea-hook-remote_options/blob/main/src/remopts_log.cc
[27] macros.h: https://reports.kea.isc.org/dev_guide/de/d3c/macros_8h.html
[28] remopts_messages.mes: https://github.com/burlunder/kea-hook-remote_options/blob/main/src/remopts_messages.mes
[29] документации: https://reports.kea.isc.org/dev_guide/d8/d33/logKeaLogging.html#logDeveloperUse
[30] remopts_common.cc: https://github.com/burlunder/kea-hook-remote_options/blob/main/src/remopts_common.cc
[31] pkt4_receive.cc: https://github.com/burlunder/kea-hook-remote_options/blob/main/src/pkt4_receive.cc
[32] pkt4_receive(): https://reports.kea.isc.org/dev_guide/de/df3/dhcpv4Hooks.html#dhcpv4HooksPkt4Receive
[33] CalloutHandle: https://reports.kea.isc.org/dev_guide/d7/d9b/classisc_1_1hooks_1_1CalloutHandle.html
[34] 77 (user-class): https://www.rfc-editor.org/rfc/rfc3004.html
[35] 97 (uuid/guid): https://www.rfc-editor.org/rfc/rfc4578.html#section-2.3
[36] pkt4_send.cc: https://github.com/burlunder/kea-hook-remote_options/blob/main/src/pkt4_send.cc
[37] pkt4_send(): https://reports.kea.isc.org/dev_guide/de/df3/dhcpv4Hooks.html#dhcpv4HooksPkt4Send
[38] property_tree: https://www.boost.org/doc/libs/1_77_0/doc/html/property_tree.html
[39] виде: https://github.com/burlunder/kea-hook-remote_options/blob/main/test/data.json
[40] репозитария: https://cloudsmith.io/~isc/repos/kea-2-0/setup/#formats-deb
[41] docker-compose.yml: https://github.com/burlunder/kea-hook-remote_options/blob/main/docker-compose.yml
[42] Dhtest: https://github.com/saravana815/dhtest
[43] test/Dockerfile.dhtest: https://github.com/burlunder/kea-hook-remote_options/blob/main/test/Dockerfile.dhtest
[44] Источник: https://habr.com/ru/post/587194/?utm_source=habrahabr&utm_medium=rss&utm_campaign=587194
Нажмите здесь для печати.