Lower-level interaction, C++ plug-ins

From ISPWiki
Jump to: navigation, search

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 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 strictures, 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 basics of c++, Makefile syntax, program compilation, and command line.

Preparing 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 full description of Makefile please refer to the article Building 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

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 [[1]]

In the above description we only specify that the seodns library should be uploaded. For more information about XML structure please refer to the article Category:XML

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

For more information about events please, refer to the article My first event handler


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 the domain's 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 dist/etc/sql/dnsmgr.user.addon/seodnsip file (paths are relative to our source code directory) with the following content

type=string
size=40

For more information 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

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 at 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.inobserving to rules for the spec package building process [1][2]. 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 [3]. 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

Notes

<references>