Взаимодействие на низком уровне, плагины с++
Несмотря на то, что для подавляющего большинства случаев можно воспользоваться внешними плагинами, написанными на любом удобном вам языке программирования, иногда возникает потребность взаимодействия с системой на более низком уровне. В этом случае необходимо писать плагины на родном для продуктов ISPsystem языке — C++.
Основные причины:
- быстродействие — код встроенных плагинов уже загружен и не требуется накладных расходов на запуск скриптов по время каждого вызова функции или обработки события;
- необходимость модифицировать данные внутри одной транзакции;
- доступ к внутренним структурам данных, не доступным из внешних скриптов.
К основным проблемам и недостаткам использования С++ можно отнести:
- относительно высокий порог вхождения, знание С++, необходимость осваивать библиотеки ISPsystem и внутреннюю структуру;
- отсутствие бинарной совместимости для разных ОС и платформ. Код скомпилированный, например, на CentOS, требует повторной компиляции на Debian;
- возможные проблемы бинарной совместимости с основным продуктом, к которому пишется плагин. После обновления основного продукта плагин может не загрузится и его нужно компилировать заново после каждого обновления. С версии 5.53 также существует каталог src/<имя библитеки> и при загрузке этой библиотеки возникают ошибки, панель попытается её пересобрать при помощи команды make. Если ошибка осталась, повторная попытка пересборки может быть предпринята не раньше чем через час после предыдущей.
Предполагается что читатель знаком с основами языка С++, синтаксисом Makefile и процессами компиляции программ, а также базовыми навыками работы в командной строке.
Подготовка окружения
В первую очередь установите продукт, под который вы собираетесь разрабатывать плагин и настройте окружение.
Затем установите пакет для разработчиков:
Debian, Ubuntu
CentOS
Если требуется взаимодействие на низком уровне с конкретным продуктом, то установите пакет разработчика для соответствующего продукта. Например:
Debian, Ubuntu
CentOS
Далее необходимо установите компилятор и все необходимые библиотеки:
Для CentOS это будет соответственно:
Описание задачи
Cоздание плагина на конкретном примере:
Необходимо написать плагин для DNSmanager, который при удалении доменов пользователями будет добавлять их на реселлера (пользователя хостера) с определенными параметрам. А если кто-то создаёт этот временно прикреплённый к хостеру домен, нужно беспрепятственно позволить создать его пользователю, не говоря о том, что он уже кем-то занят. Если первую часть этой задачи можно просто решить с помощью внешнего плагина, то вторая требует вмешательство в работу программы в рамках одной транзакции, а это возможно только используя низкоуровневые плагины.
Подготовка файлов
Для начала создайте отдельную директорию, где будут располагаться файлы плагина и осуществляться его компиляция. В этом примере назван и директорию seodns:
Перейдите в созданную директорию:
и создайте там Makefile со следующим содержимым:
Полное описание правил формирования Makefile описано в статье Сборка собственных компонентов.
Далее создайте минимальный компилируемый файл исходного кода:
Cоздайте XML-файл c описанием плагина.
Cоздайте директорию, где будут хранится наши XML-файлы:
и в ней файл dnsmgr_mod_seodns.xml со следующим содержимым:
Подробное описание структуры файла смотрите в соответствующей статье.
В этом описании ничего не объявляется, а лишь указывается, что нужно загрузить библиотеку с именем seodns.
Собрерите и установите свой модуль. Эта команда соберёт и установит модуль, а заодно перезапустит продукт указанный в Makefile в переменной MGR.
После чего в логе DNSmanager dnsmgr.log во время его старта можно будет увидеть примерно такую строчку:
Все подготовительные шаги проведены — готов полностью работоспособный плагин, который пока ничего не умеет делать, кроме как инициализироваться и писать в лог информацию.
Разработка функционала плагина
Самая простая задача — перехватить событие удаления домена.
Для этого напишите класс обработчика события:
И добавьте его инициализацию в процедуру инициализации модуля:
Теперь при удалении домена в логе будет вызов этого события:
Но для этого предварительно в конфигурации логирования debug.conf нужно включить максимальный уровень отладки для модуля, добавив строчку:
Событие заглушка создана, теперь нужно наполнить её функционалом.
Поскольку при удалении домена нужно знать его владельца, а точнее владельца (реселлера) владельца домена, то метод AfterExecute не очень подходит, так как домен уже будет удалён и никакую информацию о нём получить уже нельзя.
Используйте метод BeforeExecute, для того что бы определить пользователя, на которого нужно пересоздать домен, и сохраните его в параметр в сессии:
Здесь используйте поиск по таблицам. Для этого нужно подключить заголовочные файлы core для работы с базами данных, а так же описание структуры данных DNSmanager 'dnsmgr/db.h'.
Информация о структуре внутренних баз данных не публикуется в открытом доступе, но названия таблиц и полей должны быть интуитивно понятны. Также вся структура базы описана в заголовочных файлах.
Кроме этого в процедуре инициализации модуля инициализирована переменная db, предварительно описанная глобально:
id пользователя (точнее реселлера), под его именем которого нужно прикрепить домен, известен. После того как отработает основной функционал по удалению домена, необходимо пеерехватить управление и создать домен другому пользователю. Для этого нужно использовать штатную функцию создания домена, но вызвав её через InternalCall.
Попробуйте установить плагин (make install) и затем удалить домен в панели.
Создайте тестовый набор данных:
- создайте реселлера c именем rs;
- зайдите под реселлера rs;
- создайте пользователя user1;
- зайдите под пользователем user1;
- создайте несколько доменов;
- удалите произвольный домен — у пользователя он исчез;
- проверьте, что плагин отработал успешно и выполнил поставленную задачу. Вернитесь на уровень реселера и посмотрите удаленный пользователем домен, принадлежащий самому реселлеру.
Недостатки реализованного функционала:
- IP-адрес, на который будут крепиться домены захардкожен, а если реселлер не один, то возможно нужны будут разные;
- нужно помечать прикреплённые домены (для автоматического освобождения). Вэтом случае можно определять, что их владелец реселлер, но у него могут быть и свои домены, так что такая проверка не подходит.
Настройка IP-адреса для прикрепления в каких-либо настройках у реселлера. Его можно добавить на форму редактирования реселлера, но более логично сделать его в Настройки DNS на уровне самого реселлера. Тем более, что они индивидуальны для каждого реселлера, и там же настраиваются остальные параметры создания доменных зон.
Добавьте поле на форму, путём добавления уже имеющегося XML следующего содержания:
Теперь нужно сохранять где-то новый параметр. Самое логичное место — та же таблица в базе данных, которая содержит и остальные параметры создания доменных зон. Для того, чтобы добавить в описание таблицы свое поле, создайте файл (путь относительно рабочего каталога с исходным кодом).
со следующим содержимым:
Более подробно о том, как добавлять описание пользовательских полей в существующие таблицы, написано в статье добавление дополнительных полей в таблицы.
Далее нужно написать обработчик события, который организует передачу данных между формой и базой данных:
Не забудьте инициализировать его в процедуре инициализации модуля:
Вторая проблема с запоминанием признака прикрепления решается аналогичным образом. Создайте дополнительное поле в таблице описания доменов .
Создайте файл:
со следующим содержимым:
И после создания прикреплённого домена выставите признак прикрепления.
Итого, после внесения дополнений событие прикрепления домена выглядит следующим образом:
Все потенциально опасные действия, которые могут сгенерировать исключения, обернуты в try catch для того, чтоб пользователь в любом случае мог удалить свой домен. Даже если что-то пойдет не так во время прикрепелния. При желании можно добавить каких-либо уведомления администратору в блоке catch.
Осталось одно действие — автоматически освобождать прикреплённые домены, если кто-то хочет их создать. Для этого напишите обработчик события создания домена:
Не забудьте инициализировать его. В конечном варианте функция инициализации модуля, будет выглядеть подобным образом:
Полный код, со всеми вспомогательными файлами можно скачать с github:
Позже было внесено несколько доработок:
- обработать событие удаления пользователя и перехват его доменов;
- сделать проверку, что удаляемый домен делегирован на сервера имён реселлера (проверяется, что домен в пространстве имён реселлера);
- сделать периодическую очистку перехваченных доменов, если они позже были делегированы на другие сервера имён.
Создание пакета для распространения
После того, как вы закончили разработку плагина, если вы предполагаете его использование не на одном сервере, то разумнее всего будет оформить его в виде пакета.
Для этого необходимо создать несколько файлов сценариев для пакетов.
RPM
Если нужно собрать RPM пакет, то нужно создать файл pkgs/rpm/specs/ИМЯ_ПАКЕТА.spec.in по правилам создания spec файлов с некоторыми особенностями: поля Source указывать не нужно и секции %prep быть не должно.
В секции %files для RPM пакета нужно указывать все файлы, которые получаются в результате сборки.
Также, вместо версии нужно использовать макрос %%VERSION%%, а вместо "ревизии" макрос %%REL%%%
Пример spec.in файла для данного плагина:
Для установки зависимостей сборки, нужно выполнить:
Для сборки пакета:
Пакет будет собран в директории .build/packages.
DEB
Если нужно собрать DEB пакет, создайте директорию pkgs/debian по правилам создания deb пакета с отличием: в файле control указываются только зависимости сборки, но не указывается описание самого пакета. Описание же самого пакета делается в файле control.ИМЯ_ПАКЕТА.
Также, используются макрос _VERSION_ в который входит версия и "ревизия".
Примеры файлов в директории pkgs/debian, необходимых для сборки DEB пакета.
changelog
compat
control
control.seodns-checker
rules
seodns-checker.install
source/format
seodns-checker.postinst
seodns-checker.postrm
Сборка
Для установки зависимостей сборки, нужно выполнить:
Для сборки пакета, выполните:
Пакет будет собран в директории .build/packages.