mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-02-04 07:51:50 +00:00
kea_command: new module to access an ISC KEA server (#10709)
kea_command: new module to access an ISC KEA server This module can be used to access the JSON API of a KEA DHCP4, DHCP6, DDNS or other services in a generic way, without having to manually format the JSON, with response error code checking. It directly accesses the Unix Domain Socket API so it needs to execute on the system the server is running, with superuser privilegues, but without the hassle of wrapping it into HTTPS and password auth (or client certificates). The integration test uses a predefined setup for convenience, which runs on Debian trixie as well as, on the CI, Ubuntu noble. It makes assumptions about the default package configuration and paths and is therefore tricky to run on other distros/OSes. This only affects running the KEA server as part of the tests, not the module.
This commit is contained in:
parent
3e9f332b9c
commit
f5203aa135
4 changed files with 396 additions and 0 deletions
2
.github/BOTMETA.yml
vendored
2
.github/BOTMETA.yml
vendored
|
|
@ -807,6 +807,8 @@ files:
|
||||||
maintainers: Slezhuk pertoft
|
maintainers: Slezhuk pertoft
|
||||||
$modules/kdeconfig.py:
|
$modules/kdeconfig.py:
|
||||||
maintainers: smeso
|
maintainers: smeso
|
||||||
|
$modules/kea_command.py:
|
||||||
|
maintainers: mirabilos
|
||||||
$modules/kernel_blacklist.py:
|
$modules/kernel_blacklist.py:
|
||||||
maintainers: matze
|
maintainers: matze
|
||||||
$modules/keycloak_:
|
$modules/keycloak_:
|
||||||
|
|
|
||||||
210
plugins/modules/kea_command.py
Normal file
210
plugins/modules/kea_command.py
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
# Copyright © Thorsten Glaser <tglaser@b1-systems.de>
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
|
||||||
|
DOCUMENTATION = r"""
|
||||||
|
---
|
||||||
|
module: kea_command
|
||||||
|
short_description: Submits generic command to ISC KEA server on target
|
||||||
|
description:
|
||||||
|
- Submits a command to the JSON API of an ISC KEA server running on the target and obtains the result.
|
||||||
|
- This module supports sending arbitrary commands and returns the server response unchecked;
|
||||||
|
while it would be possible to write individual modules for specific KEA service commands,
|
||||||
|
that approach would not scale, as the FOSS hooks alone provide dozens of commands.
|
||||||
|
- Between sending the command and parsing the result status, RV(ignore:changed) will register as V(true) if an error occurs,
|
||||||
|
to err on the safe side.
|
||||||
|
version_added: '12.0.0'
|
||||||
|
author: Thorsten Glaser (@mirabilos)
|
||||||
|
options:
|
||||||
|
command:
|
||||||
|
description:
|
||||||
|
- The name of the command to send, for example V(status-get).
|
||||||
|
required: true
|
||||||
|
type: str
|
||||||
|
arguments:
|
||||||
|
description:
|
||||||
|
- The arguments sent along with the command, if any.
|
||||||
|
- Use V({}) to send an empty arguments dict/object instead of omitting it.
|
||||||
|
type: dict
|
||||||
|
rv_unchanged:
|
||||||
|
description:
|
||||||
|
- A list of C(result) codes to indicate success but unchanged system state.
|
||||||
|
- Set this to V([0]) for most acquisition commands.
|
||||||
|
- Use V([3]) for O(command=lease4-del) and similar which have a separate code for this.
|
||||||
|
- Any C(result) codes not listed in either O(rv_unchanged) or O(rv_changed) are interpreted as indicating an error result.
|
||||||
|
- O(rv_unchanged) has precedence over O(rv_changed) if a result code is in both lists.
|
||||||
|
type: list
|
||||||
|
elements: int
|
||||||
|
default: []
|
||||||
|
rv_changed:
|
||||||
|
description:
|
||||||
|
- A list of C(result) codes to indicate success and changed system state.
|
||||||
|
- Omit this for most acquisition commands.
|
||||||
|
- Set it to V([0]) for O(command=lease4-del) and similar which return changed system state that way.
|
||||||
|
- Any C(result) codes not listed in either O(rv_unchanged) or O(rv_changed) are interpreted as indicating an error result.
|
||||||
|
- O(rv_unchanged) has precedence over O(rv_changed) if a result code is in both lists.
|
||||||
|
type: list
|
||||||
|
elements: int
|
||||||
|
default: []
|
||||||
|
socket:
|
||||||
|
description:
|
||||||
|
- The full pathname of the Unix Domain Socket to connect to.
|
||||||
|
- The default value is suitable for C(kea-dhcp4-server) on Debian trixie.
|
||||||
|
- This module directly interfacees via UDS; the HTTP wrappers are not supported.
|
||||||
|
type: path
|
||||||
|
default: /run/kea/kea4-ctrl-socket
|
||||||
|
extends_documentation_fragment:
|
||||||
|
- community.general.attributes
|
||||||
|
- community.general.attributes.platform
|
||||||
|
attributes:
|
||||||
|
check_mode:
|
||||||
|
support: none
|
||||||
|
diff_mode:
|
||||||
|
support: none
|
||||||
|
platform:
|
||||||
|
support: full
|
||||||
|
platforms: posix
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXAMPLES = r"""
|
||||||
|
vars:
|
||||||
|
ipaddr: "192.168.123.45"
|
||||||
|
hwaddr: "00:00:5E:00:53:00"
|
||||||
|
tasks:
|
||||||
|
|
||||||
|
# an example for a request acquiring information
|
||||||
|
- name: Get KEA DHCP6 status
|
||||||
|
kea_command:
|
||||||
|
command: status-get
|
||||||
|
rv_unchanged: [0]
|
||||||
|
socket: /run/kea/kea6-ctrl-socket
|
||||||
|
register: kea6_status
|
||||||
|
- name: Display registered status result
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: KEA DHCP6 running on PID {{ kea6_status.response.arguments.pid }}
|
||||||
|
|
||||||
|
# an example for requests modifying state
|
||||||
|
- name: Remove existing leases for {{ ipaddr }}, if any
|
||||||
|
kea_command:
|
||||||
|
command: lease4-del
|
||||||
|
arguments:
|
||||||
|
ip-address: "{{ ipaddr }}"
|
||||||
|
rv_changed: [0]
|
||||||
|
rv_unchanged: [3]
|
||||||
|
- name: Add DHCP lease for {{ ipaddr }}
|
||||||
|
kea_command:
|
||||||
|
command: lease4-add
|
||||||
|
arguments:
|
||||||
|
ip-address: "{{ ipaddr }}"
|
||||||
|
hw-address: "{{ hwaddr }}"
|
||||||
|
rv_changed: [0]
|
||||||
|
"""
|
||||||
|
|
||||||
|
RETURN = r"""
|
||||||
|
response:
|
||||||
|
description: The server JSON response.
|
||||||
|
returned: when available
|
||||||
|
type: dict
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
|
|
||||||
|
|
||||||
|
# default buffer size for socket I/O
|
||||||
|
BUFSIZ = 8192
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_constant(s):
|
||||||
|
raise ValueError(f'Invalid JSON: "{s}"')
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
command=dict(type="str", required=True),
|
||||||
|
arguments=dict(type="dict"),
|
||||||
|
rv_unchanged=dict(type="list", elements="int", default=[]),
|
||||||
|
rv_changed=dict(type="list", elements="int", default=[]),
|
||||||
|
socket=dict(type="path", default="/run/kea/kea4-ctrl-socket"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd = {}
|
||||||
|
cmd["command"] = module.params["command"]
|
||||||
|
if module.params["arguments"] is not None:
|
||||||
|
cmd["arguments"] = module.params["arguments"]
|
||||||
|
cmdstr = json.dumps(cmd, ensure_ascii=True, allow_nan=False, indent=None, separators=(",", ":"), sort_keys=True)
|
||||||
|
rvok = module.params["rv_unchanged"]
|
||||||
|
rvch = module.params["rv_changed"]
|
||||||
|
sockfn = module.params["socket"]
|
||||||
|
|
||||||
|
r = {"changed": False}
|
||||||
|
rsp = b""
|
||||||
|
|
||||||
|
if not os.path.exists(sockfn):
|
||||||
|
r["msg"] = f"socket ({sockfn}) does not exist"
|
||||||
|
module.fail_json(**r)
|
||||||
|
|
||||||
|
phase = "opening"
|
||||||
|
try:
|
||||||
|
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
|
||||||
|
phase = "connecting"
|
||||||
|
sock.connect(sockfn)
|
||||||
|
# better safe in case anything fails…
|
||||||
|
r["changed"] = True
|
||||||
|
phase = "writing"
|
||||||
|
sock.sendall(cmdstr.encode("ASCII"))
|
||||||
|
phase = "reading"
|
||||||
|
while True:
|
||||||
|
rspnew = sock.recv(BUFSIZ)
|
||||||
|
if len(rspnew) == 0:
|
||||||
|
break
|
||||||
|
rsp += rspnew
|
||||||
|
phase = "closing"
|
||||||
|
except OSError as ex:
|
||||||
|
r["msg"] = f"error {phase} socket ({sockfn}): {ex}"
|
||||||
|
r["exception"] = traceback.format_exc()
|
||||||
|
module.fail_json(**r)
|
||||||
|
|
||||||
|
# 15 is the length of the minimum response {"response":0} as formatted by KEA
|
||||||
|
if len(rsp) < 15:
|
||||||
|
r["msg"] = f"unrealistically short response {rsp!r}"
|
||||||
|
module.fail_json(**r)
|
||||||
|
|
||||||
|
try:
|
||||||
|
r["response"] = json.loads(rsp, parse_constant=_parse_constant)
|
||||||
|
except ValueError as ex:
|
||||||
|
r["msg"] = f"error parsing JSON response: {ex}"
|
||||||
|
r["exception"] = traceback.format_exc()
|
||||||
|
module.fail_json(**r)
|
||||||
|
if not isinstance(r["response"], dict):
|
||||||
|
r["msg"] = "bogus JSON response (JSONObject expected)"
|
||||||
|
module.fail_json(**r)
|
||||||
|
if "result" not in r["response"]:
|
||||||
|
r["msg"] = "bogus JSON response (missing result)"
|
||||||
|
module.fail_json(**r)
|
||||||
|
res = r["response"]["result"]
|
||||||
|
if not isinstance(res, int):
|
||||||
|
r["msg"] = "bogus JSON response (non-integer result)"
|
||||||
|
module.fail_json(**r)
|
||||||
|
|
||||||
|
if res in rvok:
|
||||||
|
r["changed"] = False
|
||||||
|
elif res not in rvch:
|
||||||
|
r["msg"] = f"failure result (code {res})"
|
||||||
|
module.fail_json(**r)
|
||||||
|
|
||||||
|
module.exit_json(**r)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
21
tests/integration/targets/kea_command/aliases
Normal file
21
tests/integration/targets/kea_command/aliases
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
# Copyright © Thorsten Glaser <tglaser@b1-systems.de>
|
||||||
|
# Copyright © Felix Fontein <felix@fontein.de>
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
# sets up networks and services
|
||||||
|
needs/root
|
||||||
|
destructive
|
||||||
|
|
||||||
|
azp/posix/2
|
||||||
|
azp/posix/vm
|
||||||
|
skip/aix
|
||||||
|
skip/alpine # TODO: make this work
|
||||||
|
skip/docker
|
||||||
|
skip/fedora # TODO: make this work (not running in CI right now)
|
||||||
|
skip/freebsd
|
||||||
|
skip/macos
|
||||||
|
skip/osx
|
||||||
|
skip/rhel # TODO: make this work
|
||||||
|
skip/ubuntu22.04
|
||||||
163
tests/integration/targets/kea_command/tasks/main.yml
Normal file
163
tests/integration/targets/kea_command/tasks/main.yml
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
---
|
||||||
|
####################################################################
|
||||||
|
# WARNING: These are designed specifically for Ansible tests #
|
||||||
|
# and should not be used as examples of how to write Ansible roles #
|
||||||
|
####################################################################
|
||||||
|
|
||||||
|
# Copyright © Thorsten Glaser <tglaser@b1-systems.de>
|
||||||
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
- name: Install prerequisites
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name:
|
||||||
|
- iproute2
|
||||||
|
state: present
|
||||||
|
install_recommends: false
|
||||||
|
update_cache: true
|
||||||
|
|
||||||
|
- name: Networking setup, interface
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: "ip link add eth666 type dummy"
|
||||||
|
creates: /proc/sys/net/ipv4/conf/eth666/forwarding
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Networking setup, IPv4
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: "ip addr change 192.0.2.1/24 dev eth666"
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Networking setup, link
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: "ip link set up dev eth666"
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Install KEA servers for DHCP and DHCPv6
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name:
|
||||||
|
- kea-dhcp4-server
|
||||||
|
- kea-dhcp6-server
|
||||||
|
state: present
|
||||||
|
install_recommends: false
|
||||||
|
|
||||||
|
- name: Set up dhcp4 server, network
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
firstmatch: true
|
||||||
|
insertafter: '"interfaces-config": [{]'
|
||||||
|
line: '"interfaces": [ "eth666" ]'
|
||||||
|
path: /etc/kea/kea-dhcp4.conf
|
||||||
|
search_string: '"interfaces": ['
|
||||||
|
|
||||||
|
- name: Set up dhcp4 server, hooks
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
firstmatch: true
|
||||||
|
insertbefore: '"subnet4": '
|
||||||
|
# note: this will fail on architectures other than amd64, but Ubuntu 24.04 does need the full path (Debian trixie is content with just the filename)
|
||||||
|
line: '"hooks-libraries": [ { "library": "/usr/lib/x86_64-linux-gnu/kea/hooks/libdhcp_lease_cmds.so" } ],'
|
||||||
|
path: /etc/kea/kea-dhcp4.conf
|
||||||
|
regexp: '^ *"hooks-libraries":'
|
||||||
|
|
||||||
|
- name: Ensure the dhcp4 server is (re)started
|
||||||
|
ansible.builtin.service:
|
||||||
|
name: kea-dhcp4-server
|
||||||
|
state: restarted
|
||||||
|
|
||||||
|
- name: Ensure the dhcp6 server is (re)started
|
||||||
|
ansible.builtin.service:
|
||||||
|
name: kea-dhcp6-server
|
||||||
|
state: restarted
|
||||||
|
|
||||||
|
# the next tasks are for debugging this integration test if needed
|
||||||
|
- name: Show dhcp4 server config
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: "cat /etc/kea/kea-dhcp4.conf"
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Show dhcp4 server log
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: "journalctl -b -u kea-dhcp4-server"
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Show dhcp6 server log
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: "journalctl -b -u kea-dhcp6-server"
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
# an example for a request acquiring information
|
||||||
|
- name: Get KEA DHCP6 status
|
||||||
|
kea_command:
|
||||||
|
command: status-get
|
||||||
|
rv_unchanged: [0]
|
||||||
|
socket: /run/kea/kea6-ctrl-socket
|
||||||
|
register: kea6_status
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: Display registered status result
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: KEA DHCP6 running on PID {{ kea6_status.response.arguments.pid }}
|
||||||
|
|
||||||
|
# ensure socket option works
|
||||||
|
- name: Get KEA DHCP4 status
|
||||||
|
kea_command:
|
||||||
|
command: status-get
|
||||||
|
rv_unchanged: [0]
|
||||||
|
socket: /run/kea/kea4-ctrl-socket
|
||||||
|
register: kea4_status
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
# an example for requests modifying state
|
||||||
|
- name: Remove existing leases for 192.0.2.66, if any
|
||||||
|
kea_command:
|
||||||
|
command: lease4-del
|
||||||
|
arguments:
|
||||||
|
ip-address: "192.0.2.66"
|
||||||
|
rv_changed: [0]
|
||||||
|
rv_unchanged: [3]
|
||||||
|
register: lease_del
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: Add DHCP lease for 192.0.2.66
|
||||||
|
kea_command:
|
||||||
|
command: lease4-add
|
||||||
|
arguments:
|
||||||
|
ip-address: "192.0.2.66"
|
||||||
|
hw-address: "00:00:5E:00:53:00"
|
||||||
|
rv_changed: [0]
|
||||||
|
register: lease_add
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
# these all ignore_errors so the network teardown runs in all cases
|
||||||
|
- name: An unknown command
|
||||||
|
kea_command:
|
||||||
|
command: get-status
|
||||||
|
rv_unchanged: [0]
|
||||||
|
register: uc_status
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: Networking setup, teardown
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: "ip link del eth666"
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Ensure dhcp4 and dhcp6 PIDs are different
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- kea4_status.response.arguments.pid is integer
|
||||||
|
- kea4_status.response.arguments.pid > 0
|
||||||
|
- kea6_status.response.arguments.pid is integer
|
||||||
|
- kea6_status.response.arguments.pid > 0
|
||||||
|
- kea4_status.response.arguments.pid != kea6_status.response.arguments.pid
|
||||||
|
fail_msg: 'PIDs are invalid or do not differ (4: {{ kea4_status.response.arguments.pid | default("unknown") }}, 6: {{ kea6_status.response.arguments.pid | default("unknown") }})'
|
||||||
|
success_msg: 'PIDs differ (4: {{ kea4_status.response.arguments.pid | default("unknown") }}, 6: {{ kea6_status.response.arguments.pid | default("unknown") }})'
|
||||||
|
|
||||||
|
- name: Check results
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- kea6_status is not changed
|
||||||
|
- kea6_status is not failed
|
||||||
|
- kea4_status is not changed
|
||||||
|
- kea4_status is not failed
|
||||||
|
- lease_del is not failed
|
||||||
|
- lease_add is changed
|
||||||
|
- lease_add is not failed
|
||||||
|
- uc_status is failed
|
||||||
Loading…
Add table
Add a link
Reference in a new issue