One day after giving a one-hour presentation on what Ansible is capable of, “colleagues” flocked into my office and wanted to see stuff happen, so I showed them each a few odds and ends, in particular how Ansible can template out configuration files. I don’t think I exaggerate when I say that I think I saw tears of joy come to somebody’s eyes. Lovely. Anyhow, just a few days later, I was asked to find a solution for managing the creation (and destruction) of a potential boatload of DNS zones on a rather large number of PowerDNS servers.

I whipped up an Ansible module to create, delete, and list master or slave zones on authoritative PowerDNS servers with enabled REST API.

Unfortunately I had to resort to using urllib2 instead of Requests because I must not touch (i.e. install anything on) these machines. Thanks to James’ comment below, I use Ansible’s built-in fetch_url(). The pdns_zone module is very new, but it seems to do its job.

Create a master zone

In order to create a master zone, I invoke the module like this:

- hosts:
  - t1.prox
  gather_facts: False
  tasks:
  - action: pdns_zone name="ansi.test" action=master
            soa="ns.example.net hostmaster.example.com 1 1800 900 604800 3602"
            nsset="ns.example.net,ns.example.org"

The API then adds the following records to the records table:

mysql> SELECT * FROM records WHERE domain_id = (SELECT id FROM domains WHERE name = 'ansi.test');
+-------+-----------+-----------+------+--------------------------------------------------------------+-------+------+-------------+----------+-----------+------+
| id    | domain_id | name      | type | content                                                      | ttl   | prio | change_date | disabled | ordername | auth |
+-------+-----------+-----------+------+--------------------------------------------------------------+-------+------+-------------+----------+-----------+------+
| 16280 |        50 | ansi.test | SOA  | ns.example.net hostmaster.example.com 1 1800 900 604800 3602 | 86400 |    0 |        NULL |        0 | NULL      |    1 |
| 16281 |        50 | ansi.test | NS   | ns.example.net                                               | 86400 |    0 |        NULL |        0 | NULL      |    1 |
| 16282 |        50 | ansi.test | NS   | ns.example.org                                               | 86400 |    0 |        NULL |        0 | NULL      |    1 |
+-------+-----------+-----------+------+--------------------------------------------------------------+-------+------+-------------+----------+-----------+------+
3 rows in set (0.00 sec)

I can specify options to control how the module connects to the API, but by default it obtains these settings from the pdns.conf file. (See the module documentation.) Simultaenously, the comments table is also modified via the API (even though I’m still not quite understanding the use of this; maybe somebody can help me see that):

mysql> SELECT * FROM comments WHERE domain_id = (SELECT id FROM domains WHERE name = 'ansi.test');
+----+-----------+-----------+------+-------------+---------+-----------------+
| id | domain_id | name      | type | modified_at | account | comment         |
+----+-----------+-----------+------+-------------+---------+-----------------+
| 27 |        50 | ansi.test | SOA  |  1429114613 |         | Ansible-managed |
+----+-----------+-----------+------+-------------+---------+-----------------+

Peter gave me an interesting use-case for the per/RRset comments in PowerDNS: people can add, say, issue-tracking numbers to the records’ comment in order to document how a record came to exist respectively why it was updated. It’s an interesting use-case, but it doesn’t cater for deletions… ;-)

Create a slave zone

Setting up a slave zone is very similar; the API modifies the domains table and, as shown above, the comments table.

- name: Create slave zone
  action: pdns_zone zone="example.org"
          action=slave
          masters="127.0.0.2:5301"
mysql> SELECT * FROM domains WHERE name = 'example.org';
+----+-------------+-----------------+------------+-------+-----------------+---------+
| id | name        | master          | last_check | type  | notified_serial | account |
+----+-------------+-----------------+------------+-------+-----------------+---------+
| 51 | example.org | 127.0.0.2:5301  |       NULL | SLAVE |            NULL | NULL    |
+----+-------------+-----------------+------------+-------+-----------------+---------+

Deleting a zone requires specifying action=delete, and it’s removed from the back-end database. In the case of deletion of a master zone, all records are purged with the zone proper.

List zones

We can use the module to enumerate zones and their types (a.k.a. “kind”). As a special case, when we list zones, we can specify a shell-like glob which will match on names of zones. Consider this Ansible playbook and the associated template:

- hosts:
  - t1.prox
  vars:
  gather_facts: False
  tasks:
  - name: List existing .org zones
    action: pdns_zone action=list zone=*.org
    register: zl

  - name: Create report
    local_action: template src=a-zlist.j2 dest=/tmp/rep.out
{% for z in zl.zones | sort(attribute='name') %}
{{ "%-20s %-10s %s"|format(z.name, z.kind, z.serial) }}
{% endfor %}

The output produced looks like this:

e5.org               master     2015012203
example.org          slave      0

I think the list function is very practical as it allows me to connect to an authoritative server via SSH to enumerate zones, then turn around towards a second authoritative slave server (also via SSH) and create corresponding slave zones. (This is what you’d probably typically do with the PowerDNS superslave capability.)

pdns_zone

The diagram illustrates this: from our management console, we use Ansible via SSH to connect to one server, and use the obtained list of zones to create, via Ansible and the same module of course, appropriate slave zones on a second server. (If this doesn’t make terribly much sense to you, you have my full understanding; trust me: it must be done this way in this particular case, if only because the machines have SSH access only.)

- hosts:
  - t1.prox
  gather_facts: True
  tasks:
  - name: List existing zones on main PowerDNS server
    action: pdns_zone action=list zone=*.org
    register: zl

- hosts:
  - deb.prox
  gather_facts: False
  tasks:
  - name: Create slave zones on secondary PowerDNS server
    action: pdns_zone zone={{item.name}}
                action=slave
                masters="{{ hostvars['t1.prox'].ansible_default_ipv4.address }}:5301"
                api_host=127.0.0.1
                api_port=8083
                api_key="ohoh"
    with_items: hostvars['t1.prox'].zl.zones

The JSON which is returned in the list command looks like this, with kind forced to lower case:

{
  "zones": [
    {
      "serial": 2015012203,
      "name": "e5.org",
      "kind": "master"
    },
    {
      "serial": 0,
      "name": "example.org",
      "kind": "slave"
    }
  ]
}

Now, if only the PowerDNS BIND back-end could be driven thusly, hint, hint ;-)

If this has piqued your interest, I’ve made the code and a few examples available in the pdns_zone module repository.

PowerDNS and Ansible :: 15 Apr 2015 :: e-mail