DNS Records from NetXMS

Some time ago, our team of engineers got hooked on the NetXMS monitoring system . The killer feature turned out to be a graphical console:

image

There is nothing so intuitive and convenient (yes, this is a very subjective criterion) in the familiar OpenNMS and Zabbix that are close. It has become possible to use the monitoring system not just as a mechanism for notifying of emerging problems, but primarily to analyze the current state of the network.

We have already used several completely unrelated databases, in which on one side or another all or part of the equipment operated by us was taken into account. There was a temptation to reduce the number of these databases, taking the NetXMS database as the primary source of information. The first victim was an internal DNS server.

Indeed, what is the point for each of several hundred hosts to have corresponding A and PTR records? We may not be too tense, just editing the / etc / hosts file for dnsmasq , but you can avoid this completely unnecessary work.

We chose PostgreSQL as the DBMS for NetXMS; accordingly, we needed a DNS server that could get at least A and PTR from PostgreSQL. The following options were immediately found:


We discarded too exotic solutions, we have an irrational aversion to bind, so PowerDNS remains. It was required to add to its configuration:

load-modules=gpgsql
launch=gpgsql
gpgsql-host=nxhost
gpgsql-dbname=nxdb
gpgsql-user=nxreader
gpgsql-password=nxreaderpwd
recursor=8.8.8.8

PowerDNS expects to see the following tables in the nxdb database:

CREATETABLE domains (
    idSERIAL PRIMARY KEY,
    nameVARCHAR(255) NOTNULL,
    masterVARCHAR(128) DEFAULTNULL,
    last_check      INTDEFAULTNULL,
    typeVARCHAR(6) NOTNULL,
    notified_serial INTDEFAULTNULL, 
    accountVARCHAR(40) DEFAULTNULL
);
CREATETABLErecords (
    idSERIAL PRIMARY KEY,
    domain_id       INTDEFAULTNULL,
    nameVARCHAR(255) DEFAULTNULL,
    typeVARCHAR(10) DEFAULTNULL,
    contentVARCHAR(65535) DEFAULTNULL,
    ttl             INTDEFAULTNULL,
    prio            INTDEFAULTNULL,
    change_date     INTDEFAULTNULL
);

Of course, such tables are not there, but instead of them there is the following:

nxdb=> SELECTid, name, primary_ip, last_modified FROM nodes INNERJOIN object_properties ONid = object_id;
  id  |          name          |   primary_ip    | last_modified 
------+------------------------+-----------------+---------------
    1 | ns                      | 10.1.1.1        |    1376221181

Therefore, the required tables are easily replaced by views:

CREATEORREPLACEVIEW domains ASSELECT0::INTASid,
        'domain.local'::TEXTASname,
        NULL::TEXTASmaster,
        NULL::INTas last_check,
        'NATIVE'::TEXTastype,
        NULL::INTas notified_serial,
        NULL::TEXTasaccount;
CREATEORREPLACEVIEWrecordsASSELECT0::INTASid,
        0AS domain_id,
       'domain.local'ASname,
       'SOA'::TEXTAStype,
       'ns.domain.local'::TEXTAScontent,
        0AS ttl,
        NULL::INTAS prio,
        NULL::TEXTAS change_date
    UNIONSELECTid,
        0AS domain_id,
        LOWER(name) ASname,
        'A'::TEXTAStype,
        primary_ip::TEXTAScontent,
        0AS ttl,
        NULL::INTAS prio,
        last_modified::TEXTAS change_date
    FROM nodes
        INNERJOIN object_properties ONid = object_id;

Result:

$ nslookup 
> ns
Server:         10.1.1.1
Address:        10.1.1.1#53
Name:   ns.domain.local
Address: 10.1.1.1

The reverse zone can be implemented in a similar way, but it can be a little more interesting. Any host can have several interfaces with different addresses, so the name corresponding to the address can be composed of the host name itself and the interface name. Then the last view will change as follows:

CREATEORREPLACEVIEW domains ASSELECT0::INTASid,
        'domain.local'::TEXTASname,
        NULL::TEXTASmaster,
        NULL::INTas last_check,
        'NATIVE'::TEXTastype,
        NULL::INTas notified_serial,
        NULL::TEXTasaccount;
CREATEORREPLACEVIEWrecordsASSELECT0::INTASid,
        0AS domain_id,
       'domain.local'ASname,
       'SOA'::TEXTAStype,
       'ns.domain.local'::TEXTAScontent,
        0AS ttl,
        NULL::INTAS prio,
        NULL::TEXTAS change_date
    UNIONSELECTid,
        0AS domain_id,
        LOWER(name) ASname,
        'A'::TEXTAStype,
        primary_ip::TEXTAScontent,
        0AS ttl,
        NULL::INTAS prio,
        last_modified::TEXTAS change_date
    FROM nodes
        INNERJOIN object_properties ONid = object_id
    UNIONSELECT0::INTASid,
        0AS domain_id,
        'in-addr.arpa'ASname,
        'SOA'::TEXTAStype,
        'ns.domain.local'::TEXTAScontent,
        0AS ttl,
        NULL::INTAS prio,
        NULL::TEXTAS change_date
    UNIONSELECT
        interfaces.id,
        0AS domain_id,
        reverse_address(interfaces.ip_addr::INET) ASname,
        'PTR'::TEXTAStype,
        (object_properties.name||'.'||interfaces.description)::TEXTAScontent,
        0AS ttl,
        NULL::INTAS prio,
        last_modified::TEXTAS change_date
    FROM interfaces
        INNERJOIN nodes on nodes.id = interfaces.node_id
        INNERJOIN object_properties on nodes.id = object_id;

Result:

$ nslookup 
> 10.7.1.1
Server:         10.1.1.1
Address:        10.1.1.1#53
1.1.1.10.in-addr.arpa   name = ns.eth0.

The reverse_address function is mentioned in the code, its implementation looks like this:

CREATEORREPLACEFUNCTION reverse_address(addr inet)
RETURNStextAS
$t$
DECLAREmaskinteger;
    parts text[];
    part text;
    retval text;
    rounds integer;
    host text;
BEGIN
    retval := '';
    rounds := 0;
    mask := masklen(addr);
    host := host(addr);
    IF (family(addr) = 4) THEN
        IF (mask < 8) THEN
            rounds := 4;
        ELSIF (mask < 16) THEN
            rounds := 3;
        ELSIF (mask < 24) THEN
            rounds := 2;
        ELSIF (mask < 32) THEN
            rounds := 1;
        ENDIF;
        FOREACH part in ARRAY regexp_split_to_array(host, '\.') LOOP
            rounds := rounds + 1;
            IF (rounds <= 4) THEN
                retval := part || '.' || retval;
            ENDIF;
        ENDLOOP;
        RETURN retval || 'in-addr.arpa';
    ELSE
        RETURN 'ip6.arpa';
    ENDIF;
END;
$t$
LANGUAGE 'plpgsql'
IMMUTABLE;

And this is the only part of the implementation specific to PostgreSQL. Everything else can be done in a similar way on other DBMSs supported by NetXMS and PowerDNS.

Finally, performance: this solution, of course, is not suitable for public DNS servers. For internal use - completely. At the first execution, all requests fit into 100 msec, then the cache works. There is a field for optimization using the gpgsql - * - query parameters instead of representations, but for now we have enough.

Also popular now: