Взаимодействие на низком уровне, плагины с++

Материал из ISPWiki
Перейти к: навигация, поиск

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

Перечислим основные причины:

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

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

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

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


Предполагается что читатель знаком с основами языка c++, синтаксисом 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

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

Рассмотрим создание плагина на конкретном примере.

Необходимо написать плагин для 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 описанием плагина

создаем директорию, где будут хранится наши xml файлы

mkdir xml

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

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

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

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

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

Позже мне понадобилось сделать несколько доработок:

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

Можете потренироваться сделать это сами и проверить результат, сравнив с моим кодом на github

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

НЕ ОКОНЧЕНО

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

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

RPM

Если нужно собрать RPM, пакет, то нужно создать файл pkgs/rpm/specs/ИМЯ_ПАКЕТА.spec.in по правилам создания spec файлов [1][2] с некоторыми особенностями: поля 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


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

make pkg-dep

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

make pkg

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


DEB

Если нужно собрать DEB пакет, то нужно создать директорию pkgs/debian по правилам создания deb пакета[3] с отличием: в файле 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

compat

8


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/


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

Примечания

<references>