Ansible (https://github.com/ansible/ansible) is a popular automation tool to manage server configurations (among other things).
Below are two example Ansible playbook scripts that show how to make changes to the Kea-DHCP configuration via the Kea REST API.
Inventory
# The Kea DHCPv6 control API listens on [::1] (localhost only), so the playbook # must run on the Kea server itself. When running it directly on that host: [kea_dhcp6] localhost ansible_connection=local # For a remote Kea server, reach it over SSH instead and point kea_api_url at a # control channel that is reachable from that host, e.g.: # [kea_dhcp6] # kea001a.dane.onl
Adding a DHCPv6 subnet
The Ansible playbook below adds a new DHCPv6 subnet to a Kea-DHCPv6
server. The Kea-Server must be able to write to it's own configuration
file under /etc/kea/ for the changes to persist (else the changes
are only in the running config and will be gone after a restart of
the Kea-DHCP-Server service.
---
# ============================================================================
# Add an IPv6 subnet to a running Kea DHCPv6 server via its REST/Control API.
#
# Workflow:
# 1. config-get - download the current in-memory configuration
# 2. (local) - add a new subnet6 entry to the Dhcp6 object
# 3. config-set - push the updated configuration back to the server
# 4. config-write - tell Kea to write the in-memory config to the filesystem
#
# The play is idempotent: if a subnet with the same prefix already exists it
# performs steps 3 and 4 as no-ops and reports "ok" instead of "changed".
#
# Run it (Kea API listens on [::1], so run on the Kea host):
# ansible-playbook -i inventory.ini kea-add-subnet6.yml
#
# Override the subnet to add:
# ansible-playbook -i inventory.ini kea-add-subnet6.yml \
# -e '{"new_subnet": {"id": 3100, "subnet": "fd00:310::/64",
# "pools": [{"pool": "fd00:310::1-fd00:310::ffff"}]}}'
#
# ============================================================================
- name: Add an IPv6 subnet to the running Kea DHCPv6 server
hosts: kea_dhcp6
gather_facts: false
vars:
# Kea DHCPv6 control channel (HTTP control agent or built-in HTTP listener).
kea_api_url: "http://[::1]:8005/"
# Where to save the downloaded configuration (a valid Kea config file).
kea_config_backup: "{{ playbook_dir }}/kea-dhcp6.current.json"
# File Kea should (re)write on config-write. Defaulting to the file the
# server was started with makes the change survive a restart.
# NOTE: Kea rewrites this file without comments and with its own formatting.
# NOTE: the file must be writable by the Kea runtime user (e.g. _kea); if
# only the directory is owned by that user, point this at a *new*
# filename such as /etc/kea/kea-dhcp6.conf.new and swap it in.
kea_config_file: "/etc/kea/kea-dhcp6.conf"
# Run config-write even when nothing changed (e.g. to persist a previous
# in-memory-only change). Pass -e force_persist=true to force it.
force_persist: false
# The subnet to add. Only "id" and "subnet" are required; Kea fills in the
# remaining per-subnet defaults itself.
new_subnet:
id: 3000
subnet: "fd00:300::/64"
pools:
- pool: "fd00:300::1-fd00:300::ffff"
tasks:
# ----- 1. Download the current configuration ---------------------------
- name: Get the current Kea DHCPv6 configuration (config-get)
ansible.builtin.uri:
url: "{{ kea_api_url }}"
method: POST
body_format: json
body:
command: config-get
return_content: true
register: kea_config_get
- name: Assert config-get succeeded
ansible.builtin.assert:
that:
- kea_config_get.json is defined
- kea_config_get.json[0].result == 0
fail_msg: "config-get failed: {{ kea_config_get.content | default(kea_config_get) }}"
- name: Save the downloaded configuration to a local backup file
ansible.builtin.copy:
content: "{{ kea_config_get.json[0].arguments | to_nice_json }}\n"
dest: "{{ kea_config_backup }}"
mode: "0640"
delegate_to: localhost
- name: Extract the Dhcp6 object
ansible.builtin.set_fact:
kea_dhcp6: "{{ kea_config_get.json[0].arguments.Dhcp6 }}"
# ----- 2. Add the new subnet (locally) --------------------------------
- name: Determine whether the subnet already exists
ansible.builtin.set_fact:
kea_subnet_present: >-
{{
(kea_dhcp6.subnet6 | default([]))
| selectattr('subnet', 'equalto', new_subnet.subnet)
| list | length > 0
}}
- name: Report that the subnet is already configured
ansible.builtin.debug:
msg: "Subnet {{ new_subnet.subnet }} already present - config-set / config-write will be skipped."
when: kea_subnet_present
- name: Fail if the requested subnet id is used by a different prefix
ansible.builtin.assert:
that:
- >-
(kea_dhcp6.subnet6 | default([]))
| selectattr('id', 'equalto', new_subnet.id)
| rejectattr('subnet', 'equalto', new_subnet.subnet)
| list | length == 0
fail_msg: "Subnet id {{ new_subnet.id }} is already used by another prefix."
when: not kea_subnet_present
- name: Build the updated Dhcp6 object with the new subnet appended
ansible.builtin.set_fact:
kea_dhcp6_new: >-
{{
kea_dhcp6 | combine({'subnet6': (kea_dhcp6.subnet6 | default([])) + [new_subnet]})
}}
when: not kea_subnet_present
# ----- 3. Push the new configuration back -----------------------------
- name: Apply the updated configuration in memory (config-set)
ansible.builtin.uri:
url: "{{ kea_api_url }}"
method: POST
body_format: json
body:
command: config-set
arguments:
Dhcp6: "{{ kea_dhcp6_new }}"
return_content: true
register: kea_config_set
when: not kea_subnet_present
changed_when: not kea_subnet_present
- name: Assert config-set succeeded
ansible.builtin.assert:
that:
- kea_config_set.json[0].result == 0
fail_msg: "config-set failed: {{ kea_config_set.content | default(kea_config_set) }}"
when: not kea_subnet_present
# ----- 4. Make the change persistent ---------------------------------
- name: Write the in-memory configuration to disk (config-write)
ansible.builtin.uri:
url: "{{ kea_api_url }}"
method: POST
body_format: json
body:
command: config-write
arguments:
filename: "{{ kea_config_file }}"
return_content: true
register: kea_config_write
when: not kea_subnet_present or force_persist
changed_when: not kea_subnet_present or force_persist
- name: Assert config-write succeeded
ansible.builtin.assert:
that:
- kea_config_write.json[0].result == 0
fail_msg: >-
config-write failed: {{ kea_config_write.json[0].text | default(kea_config_write.content) }}.
The Kea runtime user must be able to write '{{ kea_config_file }}'
(e.g. chown _kea:_kea, or set kea_config_file to a new path in a
directory that user owns).
when: not kea_subnet_present or force_persist
- name: Show the persistence result
ansible.builtin.debug:
msg: "{{ kea_config_write.json[0].text }} ({{ kea_config_write.json[0].arguments.size }} bytes)"
when: not kea_subnet_present or force_persist
Removing a DHCPv6 subnet
This Ansible playbook removes an DHCPv6 subnet from the Kea-DHCPv6 configuration. The subnet to be removed can either be selected by its ID or by the subnet address.
Examples:
$ ansible-playbook -i inventory.ini kea-del-subnet6.yml \
-e del_subnet_prefix=fd00:300::/64
$ ansible-playbook -i inventory.ini kea-del-subnet6.yml \
-e del_subnet_id=1000
The playbook:
---
# ============================================================================
# Delete an IPv6 subnet from a running Kea DHCPv6 server via its Control API.
#
# Workflow:
# 1. config-get - download the current in-memory configuration
# 2. (local) - remove the matching subnet6 entry from the Dhcp6 object
# 3. config-set - push the updated configuration back to the server
# 4. config-write - tell Kea to write the in-memory config to the filesystem
#
# The play is idempotent: if no subnet matches it performs steps 3 and 4 as
# no-ops and reports "ok" instead of "changed".
#
# Run it (Kea API listens on [::1], so run on the Kea host):
# ansible-playbook -i inventory.ini kea-del-subnet6.yml \
# -e del_subnet_prefix=fd00:300::/64
#
# Or match by numeric id instead of prefix:
# ansible-playbook -i inventory.ini kea-del-subnet6.yml -e del_subnet_id=3000
#
# config-write needs the Kea runtime user (_kea) to be able to write
# kea_config_file. If a run removed the subnet in memory but could not persist
# it, fix the permissions and re-run with -e force_persist=true.
# ============================================================================
- name: Delete an IPv6 subnet from the running Kea DHCPv6 server
hosts: kea_dhcp6
gather_facts: false
vars:
# Kea DHCPv6 control channel (HTTP control agent or built-in HTTP listener).
kea_api_url: "http://[::1]:8005/"
# Where to save the downloaded configuration (a valid Kea config file).
kea_config_backup: "{{ playbook_dir }}/kea-dhcp6.current.json"
# File Kea should (re)write on config-write. Defaulting to the file the
# server was started with makes the change survive a restart.
# NOTE: Kea rewrites this file without comments and with its own formatting.
# NOTE: the file must be writable by the Kea runtime user (e.g. _kea); if
# only the directory is owned by that user, point this at a *new*
# filename such as /etc/kea/kea-dhcp6.conf.new and swap it in.
kea_config_file: "/etc/kea/kea-dhcp6.conf"
# Run config-write even when nothing changed. Pass -e force_persist=true.
force_persist: false
# Which subnet to delete. Set exactly one of these on the command line:
# -e del_subnet_prefix=fd00:300::/64 (match on the "subnet" field)
# -e del_subnet_id=3000 (match on the numeric "id")
del_subnet_prefix: ""
del_subnet_id: ""
tasks:
- name: Validate that a subnet selector was given
ansible.builtin.assert:
that:
- (del_subnet_prefix | string | length > 0) or (del_subnet_id | string | length > 0)
- not ((del_subnet_prefix | string | length > 0) and (del_subnet_id | string | length > 0))
fail_msg: "Set exactly one of del_subnet_prefix or del_subnet_id (-e ...)."
# ----- 1. Download the current configuration ---------------------------
- name: Get the current Kea DHCPv6 configuration (config-get)
ansible.builtin.uri:
url: "{{ kea_api_url }}"
method: POST
body_format: json
body:
command: config-get
return_content: true
register: kea_config_get
- name: Assert config-get succeeded
ansible.builtin.assert:
that:
- kea_config_get.json is defined
- kea_config_get.json[0].result == 0
fail_msg: "config-get failed: {{ kea_config_get.content | default(kea_config_get) }}"
- name: Save the downloaded configuration to a local backup file
ansible.builtin.copy:
content: "{{ kea_config_get.json[0].arguments | to_nice_json }}\n"
dest: "{{ kea_config_backup }}"
mode: "0640"
delegate_to: localhost
- name: Extract the Dhcp6 object
ansible.builtin.set_fact:
kea_dhcp6: "{{ kea_config_get.json[0].arguments.Dhcp6 }}"
# ----- 2. Remove the matching subnet (locally) -----------------------
- name: Select the subnets that match the deletion request
ansible.builtin.set_fact:
kea_subnet_matches: >-
{{
(kea_dhcp6.subnet6 | default([]))
| selectattr('subnet', 'equalto', del_subnet_prefix) | list
if (del_subnet_prefix | string | length > 0)
else (kea_dhcp6.subnet6 | default([]))
| selectattr('id', 'equalto', del_subnet_id | int) | list
}}
- name: Report that no matching subnet exists
ansible.builtin.debug:
msg: >-
No subnet matches
{{ ('prefix ' ~ del_subnet_prefix) if (del_subnet_prefix | length > 0)
else ('id ' ~ del_subnet_id) }} -
config-set / config-write will be skipped.
when: kea_subnet_matches | length == 0
- name: Show the subnet that will be deleted
ansible.builtin.debug:
msg: "Deleting subnet id={{ item.id }} prefix={{ item.subnet }}"
loop: "{{ kea_subnet_matches }}"
loop_control:
label: "{{ item.subnet }}"
when: kea_subnet_matches | length > 0
- name: Build the updated Dhcp6 object without the matching subnet
ansible.builtin.set_fact:
kea_dhcp6_new: >-
{{
kea_dhcp6 | combine({'subnet6':
((kea_dhcp6.subnet6 | default([]))
| rejectattr('subnet', 'equalto', del_subnet_prefix) | list)
if (del_subnet_prefix | string | length > 0)
else ((kea_dhcp6.subnet6 | default([]))
| rejectattr('id', 'equalto', del_subnet_id | int) | list)
})
}}
when: kea_subnet_matches | length > 0
# ----- 3. Push the new configuration back -----------------------------
- name: Apply the updated configuration in memory (config-set)
ansible.builtin.uri:
url: "{{ kea_api_url }}"
method: POST
body_format: json
body:
command: config-set
arguments:
Dhcp6: "{{ kea_dhcp6_new }}"
return_content: true
register: kea_config_set
when: kea_subnet_matches | length > 0
changed_when: kea_subnet_matches | length > 0
- name: Assert config-set succeeded
ansible.builtin.assert:
that:
- kea_config_set.json[0].result == 0
fail_msg: "config-set failed: {{ kea_config_set.content | default(kea_config_set) }}"
when: kea_subnet_matches | length > 0
# ----- 4. Make the change persistent ---------------------------------
- name: Write the in-memory configuration to disk (config-write)
ansible.builtin.uri:
url: "{{ kea_api_url }}"
method: POST
body_format: json
body:
command: config-write
arguments:
filename: "{{ kea_config_file }}"
return_content: true
register: kea_config_write
when: (kea_subnet_matches | length > 0) or force_persist
changed_when: (kea_subnet_matches | length > 0) or force_persist
- name: Assert config-write succeeded
ansible.builtin.assert:
that:
- kea_config_write.json[0].result == 0
fail_msg: >-
config-write failed: {{ kea_config_write.json[0].text | default(kea_config_write.content) }}.
The Kea runtime user must be able to write '{{ kea_config_file }}'
(e.g. chown _kea:_kea, or set kea_config_file to a new path in a
directory that user owns).
when: (kea_subnet_matches | length > 0) or force_persist
- name: Show the persistence result
ansible.builtin.debug:
msg: "{{ kea_config_write.json[0].text }} ({{ kea_config_write.json[0].arguments.size }} bytes)"
when: (kea_subnet_matches | length > 0) or force_persist