The PowerDNS Authoritative DNS server supports multiple back-ends for data storage, and I believe the most popular are the MySQL and PostgreSQL back-ends. One reason for this popularity is that people can use SQL updates to add or modify DNS data by connecting to the database PowerDNS accesses and do relatively simple INSERT and UPDATE manipulations to their data which PowerDNS then serves as responses to DNS queries. I personally am not terribly fond of that because it's error-prone: "Garbage in -> Garage out" [sic], as somebody recently said ...

We've discussed some of the pitfalls of adding bad data to PowerDNS earlier, but the proposed solution is iffy in itself. It's much safer to add data to a back-end if the program which is receiving that data (PowerDNS) can check it when it's incoming. One method for doing that is by using dynamic DNS updates (RFC 2136) for PowerDNS, but using nsupdate et. al. is not everybody's cup of tea.

I was telling a client about PowerDNS' REST API earlier this week and realized I'd never actually played with it myself, so here goes.

The built-in (experimental) API is available in versions 3.4 and higher, and we talk to it using HTTP and JSON.

launch=gmysql
gmysql-dbname=pdns
gmysql-dnssec
...
slave=yes
#
webserver-address=172.16.153.110
webserver-allow-from=127.0.0.0/8,172.16.153.0/24
webserver-port=8081
webserver=yes
#
experimental-api-key=otto
experimental-json-interface=yes

The "old" Web server interface is still there, and I can look at the statistics it issues with a Web browser.

Web server interface

The REST service listens on the TCP address/port used by the webserver component. It is enabled with experimental-json-interface, and access to it is protected with an API key you specify in the experimental-api-key setting. Let's launch PowerDNS and use the API to see what we have, using curl or resty. Note, that we specify the API key (the value of the configured experimental-api-key) on each invocation, using a header.

curl -s -H 'X-API-Key: otto' http://127.0.0.1:8081/servers/localhost
{
    "config_url": "/servers/localhost/config{/config_setting}",
    "daemon_type": "authoritative",
    "id": "localhost",
    "type": "Server",
    "url": "/servers/localhost",
    "version": "git-20150108-5369-943b2f7",
    "zones_url": "/servers/localhost/zones{/zone}"
}

So, which zones does this server serve?

curl -s -H 'X-API-Key: otto' http://172.16.153.110:8081/servers/localhost/zones
[
    {
        "dnssec": true,
        "id": "d2.aa.",
        "kind": "Slave",
        "last_check": 1420793755,
        "masters": [
            "172.16.153.112"
        ],
        "name": "d2.aa",
        "notified_serial": 0,
        "serial": 1420793754,
        "url": "/servers/localhost/zones/d2.aa."
    },
    {
        "dnssec": false,
        "id": "ww.mens.de.",
        "kind": "Native",
        "last_check": 0,
        "masters": [],
        "name": "ww.mens.de",
        "notified_serial": 0,
        "serial": 201405202,
        "url": "/servers/localhost/zones/ww.mens.de."
    }
]

Looking at zones_url in the first response, it looks as though we can query an individual zone, and we can. (Output truncated.)

curl -s -H 'X-API-Key: otto' http://172.16.153.110:8081/servers/localhost/zones/d2.aa
{
    "comments": [],
    "dnssec": true,
    "id": "d2.aa.",
    "kind": "Slave",
    "last_check": 1420793755,
    "masters": [
        "172.16.153.112"
    ],
    "name": "d2.aa",
    "notified_serial": 0,
    "records": [
        {
            "content": "127.0.0.1",
            "disabled": false,
            "name": "a.d2.aa",
            "ttl": 60,
            "type": "A"
        },
        {
            "content": "localhost. root.localhost. 1420793754 10800 3600 604800 300",
            "disabled": false,
            "name": "d2.aa",
            "ttl": 300,
            "type": "SOA"
        },
    ],
    "serial": 1420793754,
    "soa_edit": "",
    "soa_edit_api": "",
    "type": "Zone",
    "url": "/servers/localhost/zones/d2.aa."
}

So far so good, but I really prefer looking at what my DNS servers have to say using dig. :-)

The API promises to be able to create new zones, update and delete individual records, etc. How does that turn out? We'll add a new zone. Instead of putting everything on the command line, I'll create the JSON in a file and feed that to curl. Here's the zone definition:

{
    "comments": [
        {
            "account": "JP",
            "content": "My first API-created zone",
            "name": "uhuh",
            "type": "dunno"
        }
    ],
    "kind": "Native",
    "masters": [],
    "name": "example.net",
    "nameservers": [
        "ns1.example.net",
        "ns2.example.net"
    ],
    "records": [
        {
            "content": "ns.example.net. hostmaster.example.com. 1 1800 900 604800 86400",
            "disabled": false,
            "name": "example.net",
            "ttl": 86400,
            "type": "SOA"
        },
        {
            "content": "192.168.1.42",
            "disabled": false,
            "name": "www.example.net",
            "ttl": 3600,
            "type": "A"
        }
    ]
}

So I submit that, and the API returns a confirmation

curl -s -H 'X-API-Key: otto' --data @/tmp/zone http://172.16.153.110:8081/servers/localhost/zones
{
    "comments": [
        {
            "account": "JP",
            "content": "My first API-created zone",
            "modified_at": 1420806114,
            "name": "uhuh",
            "type": "TYPE0"
        }
    ],
    "dnssec": false,
    "id": "example.net.",
    "kind": "Native",
    "last_check": 0,
    "masters": [],
    "name": "example.net",
    "notified_serial": 0,
    "records": [
        {
            "content": "ns1.example.net",
            "disabled": false,
            "name": "example.net",
            "ttl": 3600,
            "type": "NS"
        },
        {
            "content": "ns2.example.net",
            "disabled": false,
            "name": "example.net",
            "ttl": 3600,
            "type": "NS"
        },
        {
            "content": "ns.example.net. hostmaster.example.com. 1 1800 900 604800 86400",
            "disabled": false,
            "name": "example.net",
            "ttl": 86400,
            "type": "SOA"
        },
        {
            "content": "192.168.1.42",
            "disabled": false,
            "name": "www.example.net",
            "ttl": 3600,
            "type": "A"
        }
    ],
    "serial": 1,
    "soa_edit": "",
    "soa_edit_api": "",
    "type": "Zone",
    "url": "/servers/localhost/zones/example.net."
}

If the type is specified in lowercase (e.g. soa) I get some weird error-message which doesn't really make sense to me:

{"error":"Record example.net/TYPE0 ns.example.net. hostmaster.example.com. 1 1800 900 604800 86400: Unknown record was stored incorrectly, need 3 fields, got 7: ns.example.net. hostmaster.example.com. 1 1800 900 604800 86400"}

So, does it work? Yes, it does:

dig @127.0.0.1 +noall +answer example.net any
example.net.            86400   IN      SOA     ns.example.net. hostmaster.example.com. 1 1800 900 604800 86400
example.net.            3600    IN      NS      ns1.example.net.
example.net.            3600    IN      NS      ns2.example.net.

and our database tables? domains and records have been populated as they'd have been with manual SQL inserts. A new table, comments, holds the comments:

mysql> select * from comments;
+----+-----------+------+-------+-------------+---------+---------------------------+
| id | domain_id | name | type  | modified_at | account | comment                   |
+----+-----------+------+-------+-------------+---------+---------------------------+
|  2 |         6 | uhuh | TYPE0 |  1420806114 | JP      | My first API-created zone |
+----+-----------+------+-------+-------------+---------+---------------------------+

It's unclear to me what type and name are for, and the documentation isn't really clear on that. Judging by the fact that the type column contains "TYPE0" I assume it should have been a record type ("SOA", "AAAA", etc). Also account is what? Anyway, this may be nice to have.

I can also create a slave zone, and support for zone deletion is also implemented. Assuming I want to add a new zone to a PowerDNS slave server, this snippet will do:

{
    "kind": "Slave",
    "masters": [ "172.16.153.112:53"],
    "name": "jp.aa"
}
curl  -s -XPOST -H 'X-API-Key: otto' --data @new-zone.json http://172.16.153.110:8081/servers/localhost/zones

To remove a zone, I use the DELETE method which causes PowerDNS to drop the zone from its back-end, and wipe all its associated data.

curl -XDELETE -H 'X-API-Key: otto' http://172.16.153.110:8081/servers/localhost/zones/jp.aa

RRsets can be added, deleted or replaced as well. Here is an example using Python to replace a TXT record which will be added if it doesn't exist.

#!/usr/bin/env python
 
import requests
import json
 
uri = 'http://172.16.153.110:8081/servers/localhost/zones/example.net'
headers = { 'X-API-Key':  'otto' }
 
payload = {
    "rrsets": [
        {
            "name": "hola.example.net",
            "type": "TXT",
            "changetype": "REPLACE",
            "records": [
                {
                    "content": '"Hello world"',
                    "disabled": False,
                    "name": "hola.example.net",
                    "ttl": 3600,
                    "type": "TXT",
                    "priority": 0
                }
            ]
        }
    ]
}
 
r = requests.patch(uri, data=json.dumps(payload), headers=headers)
print r.text

The SOA serial number will automatically be incremented for the zone if, and only if, the administrator has configured SOA-EDIT-API for the zone in the domainmetadata table. Something like this:

+----+-----------+--------------+---------------------+
| id | domain_id | kind         | content             |
+----+-----------+--------------+---------------------+
|  1 |         6 | SOA-EDIT-API | INCEPTION-INCREMENT |
+----+-----------+--------------+---------------------+

For the record, you can define this at time of zone-creation by adding the soa_edit_api element to the JSON upon submission:

"kind": "Master",
"soa_edit_api" : "INCEPTION-INCREMENT",

As to documentation: there's a very simple introductory blurb with a few examples (the docs really need a lot of work, so you may wish to contribute as you discover features).

All in all, this is pretty useful as it ensures data introduced into the PowerDNS back-ends is "clean". Most of the API status errors are encountered are pretty hard to interpret (it took me a minute to determine by staring at my JSON out why {"error":"Container was not an object."} was returned to me), but a bit of trial and error gets us going quickly enough. The API supports zone deletion, adding, replacing, and deleting individual resource records, and there's nothing that I personally missed in terms of features. I can well imagine people are going to like this very much, and there is maybe hope that somebody takes this as a basis to build yet another, but hopefully really good Web-based front-end for the PowerDNS database back-ends. Get to work! ;-)

Related: nsedit - a DNS zone and record editor for PowerDNS

Flattr this
DNS, PowerDNS, API, and REST :: 09 Jan 2015 :: e-mail

Comments

blog comments powered by Disqus