appending to remote dhcpd.conf

I’m trying to set up a playbook to create KVM clients, and am having a problem setting up dhcp static leases.

I’m adding an entry for the mac address allocated to the new KVM client, and isc-dhcp-server requires this to be added to the dhcpd.conf file, rather than adding a dedicated file to a subdirectory.

The problem comes when I try and create multiple targets concurrently. Appending one target to dhcpd.conf isn’t guaranteed to preserve the appended text from other targets, so if you’re adding client1 and client2, you often only see the entry for client1 ( or 2 )

The only way I’ve managed to get it to work is to run the client creation serially. Whilst this may be a good idea anyway for the additional load on the kvm server, I would like the opportunity to add some level of parallelism to this.

Any suggestions on how to ensure all client details are appended to the remote dhcpd.conf, or should I maybe look for an alternative dhcp server that does allow multiple config files so I can name them uniquely for each target?

Can you share the relevant part of your playbook?

This very much depend on the way you update dhcpd.conf.

The relevant tasks are ( yeah I need to block the delegation together… ):

- name: Update DHCP if necessary
  template:
    src: templates/dhcp.host.conf.j2
    dest: /etc/dhcp/hosts/{{ inventory_hostname_short }}.conf
  register: dhcp_entry
  delegate_to: "{{ dns_server }}"

- name: Register dhcpd
  slurp: 
    src: /etc/dhcp/hosts/{{ inventory_hostname_short }}.conf
  register: dhcp_client
  delegate_to: "{{ dns_server }}"
  when: dhcp_entry.changed

- name: Check for DHCP entry
  lineinfile:
    path: "/etc/dhcp/dhcpd.conf"
    state: present
    line: "  fixed-address {{ inventory_hostname_short }}.{{ domain }};"
  check_mode: yes
  register: dhcp_config
  delegate_to: "{{ dns_server }}"

- name: Add DHCP entry
  blockinfile:
    path: "/etc/dhcp/dhcpd.conf"
    insertafter: EOF
    block: "{{ dhcp_client.content | b64decode }}"
    marker: "# Ansible added {{ inventory_hostname_short }}"
    backup: yes
  register: dhcp_added
  delegate_to: "{{ dns_server }}"
  when: dhcp_config.changed

- name: Restart dhcpd
  service:
    name: isc-dhcp-server
    state: restarted
  delegate_to: "{{ dns_server }}"
  when: dhcp_added.changed

OK. You are using lineinfile and blockinfile to manage your dhcpd.conf.

So this is what is happening with your playbook. You are running it against multiple hosts but you delegate each task to a single host. This means that each task runs in parallel - one instance of a task per host. All of these task instances are trying to update the same file on a single host (your DHCP server). There is a race condition. Task instances are fighting with one another to make a change in the config file and the last one to win will make the change. Changes from other task instances will be lost. This also depends on the fork level (-f option) but let’s not go into that.

Now, Ansible does have some guards against race condition but judging by your example, it is not working correctly. I wonder if race condition prevention only works for delegate_to: localhost? Someone from the dev team can potentially say more on the subject.

To resolve this, you have to make your Ansible code update the config file only once. There are two ways to do this. One is to move your tasks to a separate play that will target only your DHCP server like so:

- hosts: your_dhcp_server

  tasks:
  # your tasks without delegate_to

The other way is to add run_once: true to each task.

Regardless of the way you choose, you also have to loop over a list of all of your hosts in each task.

So here is an example of the code using run_once:

...
- name: Add DHCP entry
  blockinfile:
    path: "/etc/dhcp/dhcpd.conf"
    insertafter: EOF
    block: "{{ hostvars[item]['dhcp_client']['content'] | b64decode }}"
    marker: "# Ansible added {{ item }}"
  delegate_to: "{{ dns_server }}"
  run_once: true
  loop: "{{ groups['all'] | map('extract', hostvars, 'inventory_hostname_short') | list }}"
...

Final note. lineinfile and blockinfile are unreliable especially if you are making multiple changes in the config file. It’s best to use template to reliably manage the whole config file.