Lower-level interaction, C++ plug-ins
In most cases you can write a custom plug-in in your popular programming language, however, sometimes you may need to write plug-ins in the native language for our software products - C++.
The main reasons are as follows:
- speed, a plug-in's code is already uploaded and doesn't require additional resources to start scripts during each function call or event handling.
- you will need to modify data within one transaction.
- access to internal data structures, which are not accessible from external scripts.
The main disadvantages of c++:
- You need to understand c++; study our libraries and their internal structure
- There is no binary compatibility for different OS and platforms. CentOS and Debian cannot run the same code
- Binary compatibility issues with your main product, i.e. the plug-in may not start after the main product has been updated, and will require re-compilation after every update.
However, all the problems described above can be resolved. Besides, usage of plug-ins has more advantages in comparison with external scripts.
Our reader is supposed to know the basics of c++, Makefile syntax, program compilation, and command line.
Preparing the environment
First, install the developer package (examples are given for Debian)
apt-get install coremanager-dev
To work with a certain product, install the developer package for the corresponding product. E.g.
apt-get install dnsmanager-dev
You are supposed to have installed and configured the corresponding software product. If not, install the product for which you want to create a plug-in
Second, set up the compiler and add required libraries.
cd /usr/local/mgr5/src
make -f isp.mk debian-prepare
If you run CentOS
make -f isp.mk centos-prepare
Task
Consider the following example.
We need to create a DNSmanager plug-in that will add users' domains that have been just deleted to their reseller (hosting company's user). If a user creates that domain, we should make it possible for him to create such a domain without telling him that the domain is unavailable. The first part of this task can be resolved with an external plug-in, but the second one requires only low-level plug-ins.
Preparing files
Create a separate directory to locate and compile the plug-in. Our project is called seodns, and the directory has the same name
mkdir /usr/local/mgr5/src/seodns
Open the directory
cd /usr/local/mgr5/src/seodns
and create Makefile with the following contents
MGR = dnsmgr
PLUGIN = seodns
VERSION = 0.1
LIB += seodns
seodns_SOURCES = seodns.cpp
BASE ?= /usr/local/mgr5
include $(BASE)/src/isp.mk
For a full description of Makefile please refer to the article How to build custom components.
Create a file of the source code
#include <api/module.h>
#include <mgr/mgrlog.h>
MODULE("seodns");
namespace {
using namespace isp_api;
MODULE_INIT(seodns, "") {
}
} // end of private namespace
For more information about macros that are used in this example, refer to our Documentation
- MODULE - File mgrlog.h
- MODULE_INIT - File module.h
Create a file with the XML description of your plug-in
create a directory to store our XML files
mkdir xml
create the dnsmgr_mod_seodns.xml file:
<?xml version="1.0" encoding="UTF-8"?>
<mgrdata>
<library name="seodns"/>
</mgrdata>
You can find a detailed description of the file structure in this article.
In the above description, we only specify that the seodns library should be uploaded.
Execute the command below to set up the module (this command will set up the module and restart the product specified in Makefile in the MGR variable)
make install
In the log DNSmanager dnsmgr.log you will see something like this
May 21 09:51:32 [22041:1] core INFO Module 'seodns' loaded
We have completed the preliminary steps: we have created the plug-in, which can be only initiated and can log information into the log file.
Plug-in functionality
Let's start with the most simple task - catch the domain deletion event.
Write a class of the event handler
class EventDomainDelete : public Event {
public:
EventDomainDelete(): Event("domain.delete.one", "seodns") { }
void AfterExecute(Session& ses) const {
STrace();
}
};
In the module initialization procedure specify initialization of that module.
MODULE_INIT(seodns, "") {
new EventDomainDelete();
}
We can see that our event is called when deleting the domain
May 21 12:10:44 [31617:7] seodns TRACE virtual void {anonymous}::EventDomainDelete::AfterExecute(isp_api::Session&) const
set the maximum logging level into debug.conf by adding
dnsmgr.seodns 9
When deleting the domain, we need to know its owner. To be more exact, the owner (reseller) of the domain's owner. That's why the AfterExecute method cannot be used, we won't be able to get information about the domain after it has been deleted.
Use the BeforeExecute method to define a user to assign the domain and save its properties in the session
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");
}
In the example below I used table lookup. I assigned the core header files to work with databases (for more information refer to the corresponding article).
#include <mgr/mgrdb_struct.h>
#include <api/stddb.h>
#include <dnsmgr/db.h>
Unfortunately, we cannot publish the internal database structure, so we will use intuitive names for tables and fields. Besides, the database structure is described in header files.
Besides, in the module initialization procedure, I have initialized the db variable. First, I described it as follows
mgr_db::JobCache *db;
db = GetDb();
So, we know the id of the user (reseller) to park the domain. After the domain has been deleted, we will capture the management procedure, and create the domain for another user by using a build-in domain creation function (we will call it through 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 (...) { }
}
}
Let's install our plug-in (make install) and delete the domain in the panel.
Generate test data. We won't use them in our example, we just manually perform operations in the control panel:
- create the reseller rs
- log in as rs
- create the user user1
- log in as user1
- create several domains
- delete a domain, make sure that this domain was removed from the user's account
- make sure that our plug-in completed the task successfully. Return to the reseller panel to make sure that the domain we have just deleted is assigned to that reseller.
Disadvantages of this functionality: we have hard-coded the IP address to park domains, so if you have multiple resellers, you may need to have different IPs. Besides, you will need to mark parked domains (to release them automatically).
Let's start with configuring an IP address to park a domain. You can add the corresponding field into the reseller edit form, or add it into the "DNS settings" form. They are specific for each reseller, and there you can set other parameters for domain zone creation.
Add a field into the form by adding the existing 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>
We need to save a new parameter, for example, we can do so in the database table, which contains other parameters for domain zone creation. To add your custom field into the table, create the file
dist/etc/sql/dnsmgr.user.addon/seodnsip
with the following content
type=string
size=40
For more information on how to add custom fields into existing tables, please refer to the article How to add additional table fields.
Add an event handler to transfer data between the form and the database.
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();
}
}
};
be sure to initialize it in the module initialization procedure.
new EventDnsParam();
We'll solve the second issue with domain parking by creating an additional field in the domain description table. Create the file
dist/etc/sql/dnsmgr.domain.addon/seodnsparked
with the following content
type=bool
after creating a parked domain we will mark it parked.
Now the event looks like the following:
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 (...) { }
}
}
I have included all potentially dangerous actions that can lead to exceptions into try catch so that a user will be able to delete his domain even if domain parking fails. You may add corresponding notifications for administrator in the catch block, if needed.
And the last operation is to release parked domains automatically if a user wants to create them. Write an event handler to create a domain
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"));
}
}
}
};
make sure to initialize it. My module initialization function looks like the following:
MODULE_INIT(seodns, "") {
db = GetDb();
new EventDnsParam();
new EventDomainCreate();
new EventDomainDelete();
}
The full code with all the files can be downloaded from github
cd /usr/local/mgr5/src/
git clone https://github.com/ispsystem/seodns|https://github.com/ispsystem/seodns
You should also complete the following steps:
- process the user deletion event and catch his domains
- check that the domain being deleted is delegated to provider's name servers
- delete those domains on a regular basis if they were later delegated to other name servers
Creating a package
NOT COMPLETED
If you want to use the newly installed plug-in on multiple servers, you'd better create it in the form of a package.
Create several file scenarios for packages.
RPM
If you need to create an RPM package, create the pkgs/rpm/specs/PACKAGE_NAME.spec.in observing to rules for the spec package building process. Please note: you may not fill out the Source fields and do not specify %prep.
In the %files section for the RPM package you should specify all files resulting from the building process.
Use the %%VERSION%% macro to specify a version, and %%REL%%% to specify a revision
Example of the spec.in file for this plug-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
Execute the following command to build dependencies
make pkg-dep
To build the package
make pkg
The package will be set up in the .build/packages directory
DEB
If you need to create a DEB package, create the pkgs/debian directory observing to rules for the deb package building process. Please note: in the control file you need to specify only build dependencies, not the package description; describe the package in the control.PACKAGE_NAME file
Use the __VERSION__ macro, which includes a version and revision.
Examples of files in the pkgs/debian directory required for the DEB package creation.
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
Building
To build dependencies, execute
make pkg-dep
To build the package
make pkg
The package will be set up in .build/packages