1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2026-02-04 07:51:50 +00:00
community.general/plugins/modules/kea_command.py
Felix Fontein 3478863ef0
Address issues reported by ruff check (#11043)
* Resolve E713 and E714 (not in/is tests).

* Address UP018 (unnecessary str call).

* UP045 requires Python 3.10+.

* Address UP007 (X | Y for type annotations).

* Address UP035 (import Callable from collections.abc).

* Address UP006 (t.Dict -> dict).

* Address UP009 (UTF-8 encoding comment).

* Address UP034 (extraneous parantheses).

* Address SIM910 (dict.get() with None default).

* Address F401 (unused import).

* Address UP020 (use builtin open).

* Address B009 and B010 (getattr/setattr with constant name).

* Address SIM300 (Yoda conditions).

* UP029 isn't in use anyway.

* Address FLY002 (static join).

* Address B034 (re.sub positional args).

* Address B020 (loop variable overrides input).

* Address B017 (assert raise Exception).

* Address SIM211 (if expression with false/true).

* Address SIM113 (enumerate for loop).

* Address UP036 (sys.version_info checks).

* Remove unnecessary UP039.

* Address SIM201 (not ==).

* Address SIM212 (if expr with twisted arms).

* Add changelog fragment.

* Reformat.
2025-11-08 17:05:21 +13:00

209 lines
6.7 KiB
Python

#!/usr/bin/python
# 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) registers 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 interfaces using 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()