- PVSM.RU - https://www.pvsm.ru -

Маленькие «малинки» в крупном дата-центре (часть 3 — Kea DHCP)

Маленькие «малинки» в крупном дата-центре (часть 3 — Kea DHCP) - 1

Мы двигаемся к финалу нашей саги об интеграции Raspberry Pi 4 в выделенные серверы. В первом тексте [1] я рассказал об отличиях процесса загрузки «малинок» от «классических» серверов. Во втором [2] — собрал образ, способный после загрузки файлов по TFTP-протоколу запускаться и работать из оперативной памяти. При этом показал, как его кастомизировать, добавляя нужные пакеты и файлы.

Теперь нужно воспроизвести поведение, которое мы показали на примере iPXE-скрипта.

Опция 224


Напомню основную часть скрипта, оставив самое важное.

isset 224 || goto noparameter
chain --autofree ${224}

В данном случае скрипт проверяет, задана ли опция 224 (определяется в ответе от DHCP-сервера). Если да, скрипт идет дальше и выполняется chain [3], который загружает по URI (значение задается как раз опцией) следующий образ и запускает его.

Опция кастомная, поэтому объясню, для чего она служит и как формируется. Значение 224 выбрано как первое свободное для частного использования в стандарте DHCP [4].

Маленькие «малинки» в крупном дата-центре (часть 3 — Kea DHCP) - 2

Использование опции удобнее всего пояснить на схеме:

  1. DHCP-клиент делает запрос к DHCP-серверу. Здесь сервер определяет клиента по опции 82 [5], так как «малинки» находятся в дата-центре. В более простой схеме клиентов можно идентифицировать по MAC-адресу [6].
  2. DHCP-сервер заворачивает опцию 82 в URL, по которому происходит обращение к внешнему серверу по HTTP.
  3. Сторонний сервер из запроса DHCP-сервера определяет клиента и формирует уникальный для него ответ.
  4. Ответ от внешнего сервера запаковывается в виде опции 224 внутрь DHCP-ответа клиенту.

Чтобы проделать это на практике, обратимся к Kea DHCP-серверу и его системе hook-модулей.

Kea DHCP


Когда нужно установить 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++, и нужно разбираться в его архитектуре на этом уровне.

Собственный Kea hook


При написании собственного хука нам придется постоянно обращаться к руководству по их созданию [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

version.сс [20]

#include <hooks/hooks.h>
extern "C" {
int version() { return (KEA_HOOKS_VERSION); }
}

Наш хук — это библиотека, подгружаемая ядром Kea. Перед загрузкой нужно убедиться, что файл собран под ту версию Kea, которая его вызывает. Код каждой версии Kea содержит собственную версию для хуков в символе KEA_HOOKS_VERSION [21]. Как видно, функция version() использует ее, что гарантирует совпадение версий.

remopts_callouts.cc [22]


Загрузка и выгрузка модуля ядром происходит через функции 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_log.cc [26]


При чтении файла выше 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.

remopts_messages.mes [28]


Для отправки сообщений в лог, помимо логгера, требуются еще сами сообщения, созданные определенным образом. Чтобы упростить работу с ними, нам предлагается файл особого формата, который при компиляции будет преобразован в код.

$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);
}

remopts_common.cc [30]


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

pkt4_receive.cc [31]


Обработка входящих пакетов начинается с этого файла. В нем мы определяем функцию 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_send.cc [36]


По аналогии с файлом 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-запроса хук загружается, и все повторяется заново.

Сборка и тестирование


Make


Перед сборкой необходимо убедиться, что удовлетворены все зависимости (на примере 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


Для демонстрации и упрощения сборки/тестирования подготовлен файл 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