Несмотря на то, что для подавляющего большинства случаев можно воспользоваться внешними плагинами, написанными на любом удобном вам языке программирования, иногда возникает потребность взаимодействия с системой на более низком уровне. В этом случае необходимо писать плагины на родном для продуктов ISPsystem языке — C++.

Основные причины:

  • быстродействие — код встроенных плагинов уже загружен и не требуется накладных расходов на запуск скриптов по время каждого вызова функции или обработки события;
  • необходимость модифицировать данные внутри одной транзакции;
  • доступ к внутренним структурам данных, не доступным из внешних скриптов.

К основным проблемам и недостаткам использования С++ можно отнести:

  • относительно высокий порог вхождения, знание С++, необходимость осваивать библиотеки ISPsystem и внутреннюю структуру;
  • отсутствие бинарной совместимости для разных ОС и платформ. Код скомпилированный, например, на CentOS, требует повторной компиляции на Debian;
  • возможные проблемы бинарной совместимости с основным продуктом, к которому пишется плагин. После обновления основного продукта плагин может не загрузится и его нужно компилировать заново после каждого обновления. С версии 5.53 также существует каталог src/<имя библитеки> и при загрузке этой библиотеки возникают ошибки, панель попытается её пересобрать при помощи команды make. Если ошибка осталась, повторная попытка пересборки может быть предпринята не раньше чем через час после предыдущей.

Предполагается что читатель знаком с основами языка С++, синтаксисом Makefile и процессами компиляции программ, а также базовыми навыками работы в командной строке.

Подготовка окружения


В первую очередь установите продукт, под который вы собираетесь разрабатывать плагин и настройте окружение.

Затем установите пакет для разработчиков:

Debian, Ubuntu

apt-get install coremanager-dev
CODE

CentOS

yum install coremanager-devel 
CODE

Если требуется взаимодействие на низком уровне с конкретным продуктом, то установите пакет разработчика для соответствующего продукта. Например:

Debian, Ubuntu

apt-get install dnsmanager-dev
CODE

CentOS

yum install dnsmanager-devel
CODE

Далее необходимо установите компилятор и все необходимые библиотеки:

cd /usr/local/mgr5/src
make -f isp.mk debian-prepare
CODE

Для CentOS это будет соответственно:

make -f isp.mk centos-prepare
CODE

Описание задачи


Cоздание плагина на конкретном примере:

Необходимо написать плагин для DNSmanager, который при удалении доменов пользователями будет добавлять их на реселлера (пользователя хостера) с определенными параметрам. А если кто-то создаёт этот временно прикреплённый к хостеру домен, нужно беспрепятственно позволить создать его пользователю, не говоря о том, что он уже кем-то занят. Если первую часть этой задачи можно просто решить с помощью внешнего плагина, то вторая требует вмешательство в работу программы в рамках одной транзакции, а это возможно только используя низкоуровневые плагины.

Подготовка файлов


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

mkdir /usr/local/mgr5/src/seodns
CODE

Перейдите в созданную директорию:

cd /usr/local/mgr5/src/seodns
CODE

и создайте там Makefile со следующим содержимым:

MGR = dnsmgr
PLUGIN = seodns
VERSION = 0.1
LIB += seodns
seodns_SOURCES = seodns.cpp

BASE ?= /usr/local/mgr5
include $(BASE)/src/isp.mk
CODE

Полное описание правил формирования Makefile описано в статье Сборка собственных компонентов.

Далее создайте минимальный компилируемый файл исходного кода:

#include <api/module.h>
#include <mgr/mgrlog.h>

MODULE("seodns");

namespace {
using namespace isp_api;

MODULE_INIT(seodns, "") {

}

} // end of private namespace
CODE

Cоздайте XML-файл c описанием плагина.

Cоздайте директорию, где будут хранится наши XML-файлы:

mkdir xml
CODE

и в ней файл dnsmgr_mod_seodns.xml со следующим содержимым:

<?xml version="1.0" encoding="UTF-8"?>
<mgrdata>
	<library name="seodns"/>
</mgrdata>
CODE

Подробное описание структуры файла смотрите в соответствующей статье.

В этом описании ничего не объявляется, а лишь указывается, что нужно загрузить библиотеку с именем seodns.

Собрерите и установите свой модуль. Эта команда соберёт и установит модуль, а заодно перезапустит продукт указанный в Makefile в переменной MGR.

make install
CODE

После чего в логе DNSmanager dnsmgr.log во время его старта можно будет увидеть примерно такую строчку:

May 21 09:51:32 [22041:1] core INFO Module 'seodns' loaded
CODE

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

Разработка функционала плагина


Самая простая задача — перехватить событие удаления домена.

Для этого напишите класс обработчика события:

class EventDomainDelete : public Event {
public:
        EventDomainDelete(): Event("domain.delete.one", "seodns") { }

void AfterExecute(Session& ses) const {
                STrace();
        }
};
CODE

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

MODULE_INIT(seodns, "") {
        new EventDomainDelete();
}
CODE

Теперь при удалении домена в логе будет вызов этого события:

May 21 12:10:44 [31617:7] seodns TRACE virtual void {anonymous}::EventDomainDelete::AfterExecute(isp_api::Session&) const
CODE

Но для этого предварительно в конфигурации логирования debug.conf нужно включить максимальный уровень отладки для модуля, добавив строчку:

dnsmgr.seodns   9
CODE

Событие заглушка создана, теперь нужно наполнить её функционалом.

Поскольку при удалении домена нужно знать его владельца, а точнее владельца (реселлера) владельца домена, то метод 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");
}
CODE

Здесь используйте поиск по таблицам. Для этого нужно подключить заголовочные файлы core для работы с базами данных, а так же описание структуры данных DNSmanager 'dnsmgr/db.h'.

#include <mgr/mgrdb_struct.h>
#include <api/stddb.h>
#include <dnsmgr/db.h>
CODE

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

Кроме этого в процедуре инициализации модуля инициализирована переменная db, предварительно описанная глобально:

mgr_db::JobCache *db;
CODE
db = GetDb();
CODE

 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 (...) { }
                }
}
CODE

Попробуйте установить плагин (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>
CODE

Теперь нужно сохранять где-то новый параметр. Самое логичное место — та же таблица в базе данных, которая содержит и остальные параметры создания доменных зон. Для того, чтобы добавить в описание таблицы свое поле, создайте файл (путь относительно рабочего каталога с исходным кодом).

dist/etc/sql/dnsmgr.user.addon/seodnsip
CODE

со следующим содержимым:

type=string
size=40
CODE

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

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

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();
                }
        }
};
CODE

Не забудьте инициализировать его в процедуре инициализации модуля:

new EventDnsParam();
CODE

Вторая проблема с запоминанием признака прикрепления решается аналогичным образом. Создайте дополнительное поле в таблице описания доменов .

Создайте файл:

dist/etc/sql/dnsmgr.domain.addon/seodnsparked
CODE

со следующим содержимым:

type=bool
CODE

И после создания прикреплённого домена выставите признак прикрепления.

Итого, после внесения дополнений событие прикрепления домена выглядит следующим образом:

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 (...) { }
                }
}
CODE

Все потенциально опасные действия, которые могут сгенерировать исключения, обернуты в 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"));
                        }
                }
        }
};
CODE

Не забудьте инициализировать его. В конечном варианте функция инициализации модуля, будет выглядеть подобным образом:

MODULE_INIT(seodns, "") {
        db = GetDb();

new EventDnsParam();

new EventDomainCreate();
        new EventDomainDelete();
}
CODE

Полный код, со всеми вспомогательными файлами можно скачать с github:

cd /usr/local/mgr5/src/
git clone https://github.com/ispsystem/seodns|https://github.com/ispsystem/seodns
CODE

Позже было внесено несколько доработок:

  • обработать событие удаления пользователя и перехват его доменов;
  • сделать проверку, что удаляемый домен делегирован на сервера имён реселлера (проверяется, что домен в пространстве имён реселлера);
  • сделать периодическую очистку перехваченных доменов, если они позже были делегированы на другие сервера имён.

Создание пакета для распространения


После того, как вы закончили разработку плагина, если вы предполагаете его использование не на одном сервере, то разумнее всего будет оформить его в виде пакета.

Для этого необходимо создать несколько файлов сценариев для пакетов.

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://ispsystem.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
CODE

Для установки зависимостей сборки, нужно выполнить:

make pkg-dep
CODE

Для сборки пакета:

make pkg
CODE

Пакет будет собран в директории .build/packages.

DEB

Если нужно собрать DEB пакет, создайте директорию pkgs/debian по правилам создания deb пакета с отличием: в файле control указываются только зависимости сборки, но не указывается описание самого пакета. Описание же самого пакета делается в файле control.ИМЯ_ПАКЕТА.

Также, используются макрос _VERSION_ в который входит версия и "ревизия".

Примеры файлов в директории pkgs/debian, необходимых для сборки DEB пакета.

changelog

seodns-checker (__VERSION__) unstable; urgency=low

* Release release (Closes: #0)

-- ISPsystem <sales@ispsystem.com>  Fri, 04 Apr 2014 18:25:38 +0900
CODE

compat

8
CODE

control

Source: seodns-checker
Priority: extra
Maintainer: ISPsystem <sales@ispsystem.com>
Build-Depends: debhelper (>= 8.0.0),
        coremanager-dev,
        dnsmanager-dev
Standards-Version: 3.9.3
Section: libs
Homepage: http://ispsystem.com/
CODE

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
CODE

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 $@

CODE

seodns-checker.install

debian/tmp
CODE

source/format

3.0 (quilt)
CODE

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
CODE

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
CODE

Сборка

Для установки зависимостей сборки, нужно выполнить:

make pkg-dep
CODE

Для сборки пакета, выполните:

make pkg
CODE

Пакет будет собран в директории .build/packages.

Примечания


  1.  http://www.rpm.org/max-rpm/s1-rpm-build-creating-spec-file.html
  2. https://fedoraproject.org/wiki/How_to_create_an_RPM_package#Creating_a_SPEC_file
  3. https://www.debian.org/doc/manuals/maint-guide/dreq.ru.html