Взаимодействие на низком уровне, плагины с++
Несмотря на то, что для подавляющего большинства случаев можно воспользоваться внешними плагинами, написанными на любом удобном вам языке программирования, иногда возникает потребность взаимодействия с системой на более низком уровне. В этом случае необходимо писать плагины на C++.
Основные причины:
- быстродействие — код встроенных плагинов уже загружен и не требуется накладных расходов на запуск скриптов по время каждого вызова функции или обработки события;
- необходимость модифицировать данные внутри одной транзакции;
- доступ к внутренним структурам данных, не доступным из внешних скриптов.
К основным проблемам и недостаткам использования С++ можно отнести:
- относительно высокий порог вхождения, знание С++, необходимость осваивать библиотеки ISPmanager и внутреннюю структуру;
- отсутствие бинарной совместимости для разных ОС и платформ. Код скомпилированный, например, на CentOS, требует повторной компиляции на Debian;
- возможные проблемы бинарной совместимости с основным продуктом, к которому пишется плагин. После обновления основного продукта плагин может не загрузится и его нужно компилировать заново после каждого обновления. С версии 5.53 также существует каталог src/<имя библитеки> и при загрузке этой библиотеки возникают ошибки, панель попытается её пересобрать при помощи команды make. Если ошибка осталась, повторная попытка пересборки может быть предпринята не раньше чем через час после предыдущей.
Предполагается что читатель знаком с основами языка С++, синтаксисом Makefile и процессами компиляции программ, а также базовыми навыками работы в командной строке.
Подготовка окружения
В первую очередь установите продукт, под который вы собираетесь разрабатывать плагин и настройте окружение.
Затем установите пакет для разработчиков (далее примеры команд для Debian):
apt-get install coremanager-dev
Если требуется взаимодействие на низком уровне с конкретным продуктом, то установите пакет разработчика для соответствующего продукта. Например:
apt-get install dnsmanager-dev
Далее необходимо установите компилятор и все необходимые библиотеки:
cd /usr/local/mgr5/src
make -f isp.mk debian-prepare
Для CentOS это будет соответственно:
make -f isp.mk centos-prepare
Описание задачи
Cоздание плагина на конкретном примере:
Необходимо написать плагин для DNSmanager, который при удалении доменов пользователями будет добавлять их на реселлера (пользователя хостера) с определенными параметрам. А если кто-то создаёт этот временно прикреплённый к хостеру домен, нужно беспрепятственно позволить создать его пользователю, не говоря о том, что он уже кем-то занят. Если первую часть этой задачи можно просто решить с помощью внешнего плагина, то вторая требует вмешательство в работу программы в рамках одной транзакции, а это возможно только используя низкоуровневые плагины.
Подготовка файлов
Для начала создайте отдельную директорию, где будут располагаться файлы плагина и осуществляться его компиляция. В этом примере назван и директорию seodns:
mkdir /usr/local/mgr5/src/seodns
Перейдите в созданную директорию:
cd /usr/local/mgr5/src/seodns
и создайте там Makefile со следующим содержимым:
MGR = dnsmgr
PLUGIN = seodns
VERSION = 0.1
LIB += seodns
seodns_SOURCES = seodns.cpp
BASE ?= /usr/local/mgr5
include $(BASE)/src/isp.mk
Полное описание правил формирования Makefile описано в статье Сборка собственных компонентов.
Далее создайте минимальный компилируемый файл исходного кода:
#include <api/module.h>
#include <mgr/mgrlog.h>
MODULE("seodns");
namespace {
using namespace isp_api;
MODULE_INIT(seodns, "") {
}
} // end of private namespace
Cоздайте XML-файл c описанием плагина.
Cоздайте директорию, где будут хранится наши XML-файлы:
mkdir xml
и в ней файл dnsmgr_mod_seodns.xml со следующим содержимым:
<?xml version="1.0" encoding="UTF-8"?>
<mgrdata>
<library name="seodns"/>
</mgrdata>
Подробное описание структуры файла смотрите в соответствующей статье.
В этом описании ничего не объявляется, а лишь указывается, что нужно загрузить библиотеку с именем seodns.
Собрерите и установите свой модуль. Эта команда соберёт и установит модуль, а заодно перезапустит продукт указанный в Makefile в переменной MGR.
make install
После чего в логе DNSmanager dnsmgr.log во время его старта можно будет увидеть примерно такую строчку:
May 21 09:51:32 [22041:1] core INFO Module 'seodns' loaded
Все подготовительные шаги проведены — готов полностью работоспособный плагин, который пока ничего не умеет делать, кроме как инициализироваться и писать в лог информацию.
Разработка функционала плагина
Самая простая задача — перехватить событие удаления домена.
Для этого напишите класс обработчика события:
class EventDomainDelete : public Event {
public:
EventDomainDelete(): Event("domain.delete.one", "seodns") { }
void AfterExecute(Session& ses) const {
STrace();
}
};
И добавьте его инициализацию в процедуру инициализации модуля:
MODULE_INIT(seodns, "") {
new EventDomainDelete();
}
Теперь при удалении домена в логе будет вызов этого события:
May 21 12:10:44 [31617:7] seodns TRACE virtual void {anonymous}::EventDomainDelete::AfterExecute(isp_api::Session&) const
Но для этого предварительно в конфигурации логирования debug.conf нужно включить максимальный уровень отладки для модуля, добавив строчку:
dnsmgr.seodns 9
Событие заглушка создана, теперь нужно наполнить её функционалом.
Поскольку при удалении домена нужно знать его владельца, а точнее владельца (реселлера) владельца домена, то метод AfterExecute не очень подходит, так как домен уже будет удалён и никакую информацию о нём получить уже нельзя.
Используйте метод BeforeExecute, для того что бы определить пользователя, на которого нужно пересоздать домен, и сохраните его в параметр в сессии:
void BeforeExecute(Session& ses) const {
auto domain_table = db->Get<DomainTable>();
auto user_table = db->Get<UserTable>();
if (domain_table->FindByName(ses.Param("elid"))
&& user_table->Find(domain_table->User)
&& !user_table->Parent.IsNull())
ses.SetParam("new_domain_owner", user_table->Parent);
else
ses.DelParam("new_domain_owner");
}
Здесь используйте поиск по таблицам. Для этого нужно подключить заголовочные файлы core для работы с базами данных, а так же описание структуры данных DNSmanager 'dnsmgr/db.h'.
#include <mgr/mgrdb_struct.h>
#include <api/stddb.h>
#include <dnsmgr/db.h>
Информация о структуре внутренних баз данных не публикуется в открытом доступе, но названия таблиц и полей должны быть интуитивно понятны. Также вся структура базы описана в заголовочных файлах.
Кроме этого в процедуре инициализации модуля инициализирована переменная db, предварительно описанная глобально:
mgr_db::JobCache *db;
db = GetDb();
id пользователя (точнее реселлера), под его именем которого нужно прикрепить домен, известен. После того как отработает основной функционал по удалению домена, необходимо пеерехватить управление и создать домен другому пользователю. Для этого нужно использовать штатную функцию создания домена, но вызвав её через InternalCall.
void AfterExecute(Session& ses) const {
string domain = ses.Param("elid");
string owner = ses.Param("new_domain_owner");
Debug("delete domain '%s' reseller=%s", domain.c_str(), owner.c_str());
if (!owner.empty()) {
try {
auto user_table = db->Get<UserTable>();
user_table->Assert(owner);
InternalCall("domain.edit", "su="+user_table->Name+"&sok=ok&name="+domain+"&dtype=master&ip=1.1.1.1");
} catch (...) { }
}
}
Попробуйте установить плагин (make install) и затем удалить домен в панели.
Создайте тестовый набор данных:
- создайте реселлера c именем rs;
- зайдите под реселлера rs;
- создайте пользователя user1;
- зайдите под пользователем user1;
- создайте несколько доменов;
- удалите произвольный домен — у пользователя он исчез;
- проверьте, что плагин отработал успешно и выполнил поставленную задачу. Вернитесь на уровень реселера и посмотрите удаленный пользователем домен, принадлежащий самому реселлеру.
Недостатки реализованного функционала:
- IP-адрес, на который будут крепиться домены захардкожен, а если реселлер не один, то возможно нужны будут разные;
- нужно помечать прикреплённые домены (для автоматического освобождения). Вэтом случае можно определять, что их владелец реселлер, но у него могут быть и свои домены, так что такая проверка не подходит.
Настройка IP-адреса для прикрепления в каких-либо настройках у реселлера. Его можно добавить на форму редактирования реселлера, но более логично сделать его в Настройки DNS на уровне самого реселлера. Тем более, что они индивидуальны для каждого реселлера, и там же настраиваются остальные параметры создания доменных зон.
Добавьте поле на форму, путём добавления уже имеющегося XML следующего содержания:
<metadata name="dnsparam">
<form>
<field name="seodnsip">
<input type="text" name="seodnsip" check="ip"/>
</field>
</form>
</metadata>
<lang name="en">
<messages name="dnsparam">
<msg name="seodnsip">SEO IP-address</msg>
<msg name="hint_seodnsip">IP-address for parking domain zones</msg>
</messages>
</lang>
Теперь нужно сохранять где-то новый параметр. Самое логичное место — та же таблица в базе данных, которая содержит и остальные параметры создания доменных зон. Для того, чтобы добавить в описание таблицы свое поле, создайте файл (путь относительно рабочего каталога с исходным кодом).
dist/etc/sql/dnsmgr.user.addon/seodnsip
со следующим содержимым:
type=string
size=40
Более подробно о том, как добавлять описание пользовательских полей в существующие таблицы, написано в статье добавление дополнительных полей в таблицы.
Далее нужно написать обработчик события, который организует передачу данных между формой и базой данных:
class EventDnsParam : public Event {
public:
EventDnsParam(): Event("dnsparam", "seodns") { }
void AfterExecute(Session& ses) const {
auto user_table = db->Get<UserTable>();
user_table->Assert(ses.auth.ext("uid"));
if (ses.Param("sok").empty()) {
ses.NewNode("seodnsip", user_table->FieldByName("seodnsip")->AsString());
} else {
user_table->FieldByName("seodnsip")->Set(ses.Param("seodnsip"));
user_table->Post();
}
}
};
Не забудьте инициализировать его в процедуре инициализации модуля:
new EventDnsParam();
Вторая проблема с запоминанием признака прикрепления решается аналогичным образом. Создайте дополнительное поле в таблице описания доменов .
Создайте файл:
dist/etc/sql/dnsmgr.domain.addon/seodnsparked
со следующим содержимым:
type=bool
И после создания прикреплённого домена выставите признак прикрепления.
Итого, после внесения дополнений событие прикрепления домена выглядит следующим образом:
void AfterExecute(Session& ses) const {
string domain = ses.Param("elid");
string owner = ses.Param("new_domain_owner");
Debug("delete domain '%s' reseller=%s", domain.c_str(), owner.c_str());
if (!owner.empty()) {
try {
auto user_table = db->Get<UserTable>();
user_table->Assert(owner);
InternalCall("domain.edit", "su="+user_table->Name+"&sok=ok&name="+domain+"&dtype=master&ip="+user_table->FieldByName("seodnsip")->AsString());
auto domain_table = db->Get<DomainTable>();
domain_table->AssertByName(domain);
domain_table->FieldByName("seodnsparked")->Set("on");
domain_table->Post();
} catch (...) { }
}
}
Все потенциально опасные действия, которые могут сгенерировать исключения, обернуты в try catch для того, чтоб пользователь в любом случае мог удалить свой домен. Даже если что-то пойдет не так во время прикрепелния. При желании можно добавить каких-либо уведомления администратору в блоке catch.
Осталось одно действие — автоматически освобождать прикреплённые домены, если кто-то хочет их создать. Для этого напишите обработчик события создания домена:
class EventDomainCreate : public Event {
public:
EventDomainCreate(): Event("domain.edit", "seodns") { }
void BeforeExecute(Session& ses) const {
if (!ses.Param("sok").empty() && ses.Param("elid").empty()) {
auto domain_table = db->Get<DomainTable>();
if (domain_table->FindByName(ses.Param("name")) && domain_table->FieldByName("seodnsparked")->AsString() == "on") {
InternalCall("domain.delete", "elid="+ses.Param("name"));
}
}
}
};
Не забудьте инициализировать его. В конечном варианте функция инициализации модуля, будет выглядеть подобным образом:
MODULE_INIT(seodns, "") {
db = GetDb();
new EventDnsParam();
new EventDomainCreate();
new EventDomainDelete();
}
Полный код, со всеми вспомогательными файлами можно скачать с github:
cd /usr/local/mgr5/src/
git clone https://github.com/ispsystem/seodns|https://github.com/ispsystem/seodns
Позже было внесено несколько доработок:
- обработать событие удаления пользователя и перехват его доменов;
- сделать проверку, что удаляемый домен делегирован на сервера имён реселлера (проверяется, что домен в пространстве имён реселлера);
- сделать периодическую очистку перехваченных доменов, если они позже были делегированы на другие сервера имён.
Создание пакета для распространения
НЕ ОКОНЧЕНО
После того как вы закончили разработку плагина, если вы предполагаете его использование не на одном сервере, то разумнее всего будет оформить его в виде пакета.
Для этого необходимо создать несколько файлов сценариев для пакетов.
RPM
Если нужно собрать RPM пакет, то нужно создать файл pkgs/rpm/specs/ИМЯ_ПАКЕТА.spec.in по правилам создания spec файлов с некоторыми особенностями: поля Source указывать не нужно и секции %prep быть не должно.
В секции %files для RPM пакета нужно указывать все файлы, которые получаются в результате сборки.
Также, вместо версии нужно использовать макрос %%VERSION%%, а вместо "ревизии" макрос %%REL%%%
Пример spec.in файла для данного плагина:
%define core_dir /usr/local/mgr5
Name: seodns-checker
Version: %%VERSION%%
Release: %%REL%%%{?dist}
Summary: seodns-checker package
Group: System Environment/Daemons
License: Commercial
URL: http://ispmanager.com/
BuildRequires: coremanager-devel
BuildRequires: dnsmanager-devel
Requires: coremanager
Requires: dnsmanager
%description
seodns-checker
%debug_package
%build
export LD_LIBRARY_PATH=".:./lib"
export CFLAGS="$RPM_OPT_FLAGS"
export CXXFLAGS="${CFLAGS}"
make %{?_smp_mflags} NOEXTERNAL=yes RELEASE=yes
%install
export LD_LIBRARY_PATH=".:./lib"
export CFLAGS="$RPM_OPT_FLAGS"
export LDFLAGS="-L%{core_dir}/lib"
export CXXFLAGS="${CFLAGS}"
rm -rf $RPM_BUILD_ROOT
INSTALLDIR=%{buildroot}%{core_dir}
mkdir -p $INSTALLDIR
make %{?_smp_mflags} dist DISTDIR=$INSTALLDIR NOEXTERNAL=yes RELEASE=yes
%check
%clean
rm -rf $RPM_BUILD_ROOT
%post
. %{core_dir}/lib/pkgsh/core_pkg_funcs.sh
ReloadMgr dnsmgr
%postun
if [ $1 -eq 0 ]; then
. %{core_dir}/lib/pkgsh/core_pkg_funcs.sh
ReloadMgr dnsmgr
fi
%files
%defattr(-, root, root, -)
%{core_dir}/etc/sql/dnsmgr.domain.addon/seodnsparked
%{core_dir}/etc/sql/dnsmgr.user.addon/seodnsip
%{core_dir}/etc/xml/dnsmgr_mod_seodns.xml
%{core_dir}/lib/seodns.so
%{core_dir}/libexec/seodns_checker.so
%{core_dir}/sbin/seodns_checker
Для установки зависимостей сборки, нужно выполнить:
make pkg-dep
Для сборки пакета:
make pkg
Пакет будет собран в директории .build/packages.
DEB
Если нужно собрать DEB пакет, создайте директорию pkgs/debian по правилам создания deb пакета с отличием: в файле control указываются только зависимости сборки, но не указывается описание самого пакета. Описание же самого пакета делается в файле control.ИМЯ_ПАКЕТА.
Также, используются макрос _VERSION_ в который входит версия и "ревизия".
Примеры файлов в директории pkgs/debian, необходимых для сборки DEB пакета.
changelog
seodns-checker (__VERSION__) unstable; urgency=low
* Release release (Closes: #0)
-- ISPmanager <sales@ispmanager.com> Fri, 04 Apr 2014 18:25:38 +0900
compat
8
control
Source: seodns-checker
Priority: extra
Maintainer: ISPmanager <sales@ispmanager.com>
Build-Depends: debhelper (>= 8.0.0),
coremanager-dev,
dnsmanager-dev
Standards-Version: 3.9.3
Section: libs
Homepage: http://ispmanager.com/
control.seodns-checker
Package: seodns-checker
Section: libs
Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends},
coremanager,
dnsmanager
Pre-Depends: coremanager
Description: seodns-checker
seodns-checker binary and libraries
Package: seodns-checker-dbg
Section: debug
Architecture: any
Depends: seodns-checker (= ${binary:Version}), ${misc:Depends}
Description: seodns-checker debug simbols
seodns-checker debug files
rules
#!/usr/bin/make -f
# -*- makefile -*-
# Sample debian/rules that uses debhelper.
# This file was originally written by Joey Hess and Craig Small.
# As a special exception, when this file is copied by dh-make into a
# dh-make output file, you may use that output file without restriction.
# This special exception was added by Craig Small in version 0.37 of dh-make.
COREDIR = /usr/local/mgr5
CFLAGS = `dpkg-buildflags --get CFLAGS`
CFLAGS += `dpkg-buildflags --get CPPFLAGS`
LDFLAGS = `dpkg-buildflags --get LDFLAGS`
CFLAGS += -I$(COREDIR)/include
CXXFLAGS = $(CFLAGS)
export CFLAGS LDFLAGS CXXFLAGS
INSTALLDIR = $(CURDIR)/debian/tmp$(COREDIR)
# Uncomment this to turn on verbose mode.
#export DH_VERBOSE=1
export NOEXTERNAL=yes
JOPTS=-j$(shell grep -c processor /proc/cpuinfo)
build:
dh_testdir
make $(JOPTS) NOEXTERNAL=yes BASE=$(COREDIR) RELEASE=yes ; \
override_dh_auto_build: build
clean:
dh_testdir
dh_testroot
make clean
dh_clean
rm -rf $(CURDIR)/debian/tmp
install:
dh_testdir
dh_testroot
mkdir -p $(INSTALLDIR)
make $(JOPTS) dist NOEXTERNAL=yes BASE=$(COREDIR) RELEASE=yes DISTDIR=$(INSTALLDIR); \
override_dh_auto_test:
override_dh_auto_install: install
override_dh_usrlocal:
override_dh_shlibdeps:
LD_LIBRARY_PATH=$(COREDIR)/lib:$(COREDIR)/libexec:$(COREDIR)/external:$(LD_LIBRARY_PATH) dh_shlibdeps
override_dh_strip:
dh_testdir
dh_strip --package=seodns-checker --dbg-package=seodns-checker-dbg
%:
dh $@
seodns-checker.install
debian/tmp
source/format
3.0 (quilt)
seodns-checker.postinst
#!/bin/bash
# postinst script for coremanager
# see: dh_installdeb(1)
#set -e
# summary of how this script can be called:
# * <postinst> `configure' <most-recently-configured-version>
# * <old-postinst> `abort-upgrade' <new version>
# * <conflictor's-postinst> `abort-remove' `in-favour' <package>
# <new-version>
# * <postinst> `abort-remove'
# * <deconfigured's-postinst> `abort-deconfigure' `in-favour'
# <failed-install-package> <version> `removing'
# <conflicting-package> <version>
# for details, see http://www.debian.org/doc/debian-policy/ or
# the debian-policy package
COREDIR=/usr/local/mgr5
MGR=dnsmgr
. ${COREDIR}/lib/pkgsh/core_pkg_funcs.sh
case "$1" in
configure)
ReloadMgr ${MGR}
;;
abort-upgrade|abort-remove|abort-deconfigure)
;;
*)
echo "postinst called with unknown argument \`$1'" >&2
exit 1
;;
esac
# dh_installdeb will replace this with shell code automatically
# generated by other debhelper scripts.
#DEBHELPER#
exit 0
seodns-checker.postrm
#!/bin/sh
# postrm script for coremanager-5.15.0
# see: dh_installdeb(1)
# summary of how this script can be called:
# * <postrm> `remove'
# * <postrm> `purge'
# * <old-postrm> `upgrade' <new-version>
# * <new-postrm> `failed-upgrade' <old-version>
# * <new-postrm> `abort-install'
# * <new-postrm> `abort-install' <old-version>
# * <new-postrm> `abort-upgrade' <old-version>
# * <disappearer's-postrm> `disappear' <overwriter>
# <overwriter-version>
# for details, see http://www.debian.org/doc/debian-policy/ or
# the debian-policy package
COREDIR=/usr/local/mgr5
case "$1" in
purge|remove)
COREDIR=/usr/local/mgr5
MGR=dnsmgr
. ${COREDIR}/lib/pkgsh/core_pkg_funcs.sh
ReloadMgr ${MGR}
;;
upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
;;
*)
echo "postrm called with unknown argument \`$1'" >&2
exit 1
;;
esac
# dh_installdeb will replace this with shell code automatically
# generated by other debhelper scripts.
#DEBHELPER#
exit 0
Сборка
Для установки зависимостей сборки, нужно выполнить:
make pkg-dep
Для сборки пакета, выполните:
make pkg
Пакет будет собран в директории .build/packages.