Ansible template data structure for dnsmasq.conf?

Hi,

I’m currently writing an Ansible role for automatically setting up Dnsmasq on my routerboard running Rocky Linux.

Here’s the actual dnsmasq.conf which I setup manually:

# /etc/dnsmasq.conf
domain-needed
bogus-priv
interface=enp2s0
dhcp-range=192.168.2.100,192.168.2.200,24h
local=/microlinux.lan/
domain=microlinux.lan
expand-hosts
#server=208.67.222.123
#server=208.67.220.123
#server=8.8.8.8
#server=8.8.4.4
server=1.1.1.1
server=1.0.0.1
no-resolv
log-facility=/var/log/dnsmasq.log
address=/proxy.microlinux.lan/192.168.2.1
address=/rocky-el8.microlinux.lan/192.168.2.10
dhcp-host=50:65:F3:4E:DA:77,alphamule,192.168.2.2
dhcp-host=B0:83:FE:90:4D:64,sandbox,192.168.2.3
dhcp-host=68:54:5A:B2:D0:81,dell-xps,192.168.2.4
dhcp-host=D0:37:45:24:61:BD,44:37:E6:DA:44:72,gustave,192.168.2.5
dhcp-host=3C:DC:BC:00:FB:97,smartphone-nico,192.168.2.6
dhcp-host=AC:AF:B9:D6:93:FA,smartphone-clothilde,192.168.2.7
dhcp-host=52:54:00:00:00:01,testbox-el8,192.168.2.10
dhcp-host=52:54:00:00:00:02,testbox-el9,192.168.2.11
dhcp-host=D4:85:64:B2:B2:1B,oldmule,192.168.2.249
dhcp-host=00:22:64:8A:4C:C2,nestor,192.168.2.250
dhcp-host=00:0D:B9:4B:5B:5C,squidbox,192.168.2.251
dhcp-host=10:62:E5:D4:95:60,hp-officejet,192.168.2.252
dhcp-host=00:11:32:26:63:A5,nas,192.168.2.253

And here’s the corresponding /etc/hosts file:

# /etc/hosts
127.0.0.1     localhost.localdomain localhost
192.168.2.1   proxy.microlinux.lan  proxy
192.168.2.2   alphamule
192.168.2.3   sandbox
192.168.2.4   dell-xps
192.168.2.5   gustave
192.168.2.6   smartphone-nico
192.168.2.7   smartphone-clothilde
192.168.2.10  testbox-el8
192.168.2.11  testbox-el9
192.168.2.249 oldmule
192.168.2.250 nestor
192.168.2.251 squidbox
192.168.2.252 hp-officejet
192.168.2.253 nas
192.168.2.254 wifi

For now my playbook for Ansible looks like this:

    - name: Configure hosts file
      ansible.builtin.template:
        src: hosts.j2
        dest: /etc/hosts

    - name: Install Dnsmasq
      ansible.builtin.dnf:
        name: dnsmasq
        state: present

    - name: Enable and start Dnsmasq
      ansible.builtin.service:
        name: dnsmasq
        enabled: true
        state: started

    - name: Configure Dnsmasq
      ansible.builtin.template:
        src: dnsmasq.conf.j2
        dest: /etc/dnsmasq.conf
      notify: Restart Dnsmasq

    - meta: flush_handlers

Here’s the hosts.j2 file:

# /etc/hosts
127.0.0.1 localhost.localdomain localhost
{{address_lan}} {{hostname}}.{{domain_lan}} {{hostname}}

And here’s the dnsmasq.conf.j2 file (using a different network for testing purposes):

# /etc/dnsmasq.conf
domain-needed
bogus-priv
interface={{interface_lan}}
dhcp-range=192.168.3.100,192.168.3.200,24h
local=/{{domain_lan}}/
domain={{domain_lan}}
expand-hosts
server={{dns1}}
server={{dns2}}
no-resolv
log-facility=/var/log/dnsmasq.log
address=/{{hostname}}.{{domain_lan}}/{{address_lan}}

As you can see in the example above, I have a series of clients (sandbox, dell-xps, gustave and so on) each with a corresponding local IP address as well as a MAC address.

Note that the client gustave has two MAC addresses since it can connect by wired interface or by wireless.

Now I could very well hardcode all these clients into my dnsmasq.conf.j2 and hosts.j2 templates.

There’s a slight redundancy here, due to the fact that Dnsmasq needs hostnames in /etc/hosts.

But I think a more elegant solution would be to use variables. And now I wonder how the data structure could look like here. I bluntly admit I’m not very proficient with nested dictionaries, nested lists, lists of dictionaries and dictionaries of lists etc.

How could I write a data structure for these client machines to put in my template ?

Cheers,

Niki

First, I don’t use dnsmasq as separate service, but do set NetworkManager to use it for DNS, rather than the glibc’s resolver. Hence, the config file – if required – is in /etc/NetworkManager/dnsmasq.d/. I don’t have clever templates (yet), just hardcoded files copied. The instance started by NM can be set to act as DHCP/DNS/TFTP server too, but requires some tweaks.


That, however, is not the topic here. The templates can have code that is evaluated. For example:

{% if autofs_direct_keys is defined and autofs_direct_keys|length > 0  %}
/-       /etc/auto.master.d/auto.direct  --timeout=300
{% endif %}
{% for item in autofs_maps %}
{{ item.mount_point }}  /etc/auto.master.d/auto.{{ item.name }}  --timeout=300 {{ item.browse|default('browse') }}
{% endfor %}

That is part of my template for /etc/auto.master.d/local.autofs and shows condition, loop, and filters being used. The autofs_maps is a list:

autofs_maps:
- name: 'backup'
  mount_point: '/backup'
  browse:      'browse'
  entries:
  - key:     'xxx'
    target:  'server:/backup/xxx'
  - key:     'yyy'
    target:  'server:/backup/yyy'

The example above has only one item on the list. The item is a dict with four attributes:
name, mount_point, browse, and entries. The entries is a list (of dicts) and is used to populate ‘auto.backup’ with another template.

The point is that you could have a list of dicts:

myhosts:
- mac:  '50:65:F3:4E:DA:77'
  name: 'alphamule'
  address: 192.168.2.2
- mac:  'B0:83:FE:90:4D:64'
  name: 'sandbox'
  address: 192.168.2.3

and loops in templates:

# other options

{% for item in myhosts %}
dhcp={{ item.mac }},{{ item.name }},{{ item.address }}
{% endfor %}

and

127.0.0.1     localhost.localdomain localhost
{% for item in myhosts %}
{{ item.address }}  {{ item.name }}
{% endfor %}

If there is a dict, like:

autofs_direct_keys:
  /archive:  aaa
  /data/win: bbb

then Jinja2 can turn it into list of (key,value) on the fly:

{% for key, value in autofs_direct_keys.items() %}
{{ key }} {{ value }}
{% endfor %}
1 Like

As always, thank you very much for your detailed replay, Jukka.

In the meantime I’ve experimented quite a lot, and I came up with a perfect solution. I just finished it, and it looks like it’s working very well.

Cheers,

Niki

1 Like