From b290d3c3d2902f54839dca5bf2d1c8622976c7ec Mon Sep 17 00:00:00 2001 From: void-spark <81029971+void-spark@users.noreply.github.com> Date: Sun, 10 May 2026 18:15:05 +0200 Subject: [PATCH] First attempt at kea dhcp support --- front/plugins/kea_api/README.md | 70 +++++ front/plugins/kea_api/config.json | 455 ++++++++++++++++++++++++++++++ front/plugins/kea_api/script.py | 70 +++++ 3 files changed, 595 insertions(+) create mode 100755 front/plugins/kea_api/README.md create mode 100644 front/plugins/kea_api/config.json create mode 100644 front/plugins/kea_api/script.py diff --git a/front/plugins/kea_api/README.md b/front/plugins/kea_api/README.md new file mode 100755 index 00000000..8a35edf2 --- /dev/null +++ b/front/plugins/kea_api/README.md @@ -0,0 +1,70 @@ +## Overview + +A plugin allowing for importing devices from the Kea DHCP API. +https://www.isc.org/kea/ + +And specifically: +https://kea.readthedocs.io/en/kea-2.6.3/api.html#lease4-get-all + + +### Usage + +To enable the API, first you want to add something like this to your main kea configuration (this is for debian 13): + +```json + "control-socket": { + "socket-type": "unix", + "socket-name": "/run/kea/kea4-ctrl-socket" + }, + + "hooks-libraries": [ + { + "library": "/usr/lib/x86_64-linux-gnu/kea/hooks/libdhcp_lease_cmds.so" + } + ], +``` + + +And you need to install kea-ctrl-agent, with a config that looks something like this: + +```json +{ +"Control-agent": { + "http-host": "127.0.0.1", + "http-port": 8000, + + "authentication": { + "type": "basic", + "realm": "Kea Control Agent", + "directory": "/etc/kea", + "clients": [ + { + "user": "kea-api", + "password-file": "kea-api-password" + } + ] + }, + "control-sockets": { + "dhcp4": { + "socket-type": "unix", + "socket-name": "/run/kea/kea4-ctrl-socket" + } + }, + "loggers": [ + { + "name": "kea-ctrl-agent", + "output-options": [ + { + "output": "stdout", + "pattern": "%-5p %m\n" + } + ], + "severity": "INFO", + "debuglevel": 0 + } + ] +} +} +``` + +You will need to configure the plugin with the URL to the API, and the username and password configured above (from kea-api-password file in the example) diff --git a/front/plugins/kea_api/config.json b/front/plugins/kea_api/config.json new file mode 100644 index 00000000..ffa434bd --- /dev/null +++ b/front/plugins/kea_api/config.json @@ -0,0 +1,455 @@ +{ + "code_name": "kea_api", + "unique_prefix": "KEALSS", + "plugin_type": "device_scanner", + "execution_order" : "Layer_3", + "enabled": true, + "data_source": "script", + "data_filters": [ + { + "compare_column": "objectPrimaryId", + "compare_operator": "==", + "compare_field_id": "txtMacFilter", + "compare_js_template": "'{value}'.toString()", + "compare_use_quotes": true + } + ], + "show_ui": true, + "localized": ["display_name", "description", "icon"], + "mapped_to_table": "CurrentScan", + "display_name": [{"language_code": "en_us", "string": "Kea DHCP API"}], + "icon": [{"language_code": "en_us", "string": ""}], + "description": [{"language_code": "en_us", "string": "Imports leases via Kea Control Agent REST API"}], + "database_column_definitions": [ + { + "column": "index", + "css_classes": "col-sm-2", + "show": true, + "type": "none", + "default_value": "", + "options": [], + "localized": ["name"], + "name": [{"language_code": "en_us", "string": "Index"}] + }, + { + "column": "objectPrimaryId", + "mapped_to_column": "scanMac", + "css_classes": "col-sm-2", + "show": true, + "type": "device_mac", + "default_value": "", + "options": [], + "localized": ["name"], + "name": [{"language_code": "en_us", "string": "MAC address"}] + }, + { + "column": "objectSecondaryId", + "mapped_to_column": "scanLastIP", + "css_classes": "col-sm-2", + "show": true, + "type": "device_ip", + "default_value": "", + "options": [], + "localized": ["name"], + "name": [{"language_code": "en_us", "string": "IP" }] + }, + { + "column": "dateTimeCreated", + "css_classes": "col-sm-2", + "show": true, + "type": "label", + "default_value": "", + "options": [], + "localized": ["name"], + "name": [{"language_code": "en_us", "string": "Created"}] + }, + { + "column": "dateTimeChanged", + "mapped_to_column": "scanLastConnection", + "css_classes": "col-sm-2", + "show": true, + "type": "label", + "default_value": "", + "options": [], + "localized": ["name"], + "name": [{"language_code": "en_us", "string": "Changed"}] + }, + { + "column": "watchedValue1", + "css_classes": "col-sm-2", + "show": true, + "type": "label", + "default_value": "", + "options": [], + "localized": ["name"], + "name": [{"language_code": "en_us", "string": "Is active"}] + }, + { + "column": "watchedValue2", + "mapped_to_column": "scanName", + "css_classes": "col-sm-2", + "show": true, + "type": "label", + "default_value": "", + "options": [], + "localized": ["name"], + "name": [{"language_code": "en_us", "string": "Hostname"}] + }, + { + "column": "watchedValue4", + "css_classes": "col-sm-2", + "show": true, + "type": "label", + "default_value": "", + "options": [], + "localized": ["name"], + "name": [{"language_code": "en_us", "string": "State"}] + }, + { + "column": "userData", + "css_classes": "col-sm-2", + "show": false, + "type": "textbox_save", + "default_value": "", + "options": [], + "localized": ["name"], + "name": [{"language_code": "en_us", "string": "Comments"}] + }, + { + "column": "Dummy", + "mapped_to_column": "scanSourcePlugin", + "mapped_to_column_data": { + "value": "KEALSS" + }, + "css_classes": "col-sm-2", + "show": false, + "type": "label", + "default_value": "", + "options": [], + "localized": ["name"], + "name": [{"language_code": "en_us", "string": "Scan method"}] + }, + { + "column": "status", + "css_classes": "col-sm-1", + "show": true, + "type": "replace", + "default_value": "", + "options": [ + { + "equals": "watched-not-changed", + "replacement": "
Kea API. If you select schedule the scheduling settings from below are applied. If you select once the scan is run only once on start of the application (container) or after you update your settings. ⚠ Use the same schedule if you have multiple Device scanners enabled."
+ }
+ ]
+ },
+ {
+ "function": "CMD",
+ "type": {
+ "dataType": "string",
+ "elements": [
+ { "elementType": "input", "elementOptions": [], "transformers": [] }
+ ]
+ },
+ "default_value": "python3 /app/front/plugins/kea_api/script.py",
+ "options": [],
+ "localized": ["name", "description"],
+ "name": [{"language_code": "en_us", "string": "Command"}],
+ "description": [{"language_code": "en_us", "string": "Command to run"}]
+ },
+ {
+ "function": "URL",
+ "localized": ["name", "description"],
+ "name": [{"language_code": "en_us", "string": "API URL"}],
+ "description": [{"language_code": "en_us", "string": "Kea Control Agent URL"}],
+ "type": {
+ "dataType": "string",
+ "elements": [
+ {
+ "elementType": "input",
+ "elementOptions": [],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": "http://127.0.0.1:8000"
+ },
+ {
+ "function": "USER",
+ "localized": ["name", "description"],
+ "name": [{"language_code": "en_us", "string": "API User"}],
+ "description": [{"language_code": "en_us", "string": "Basic Auth Username"}],
+ "type": {
+ "dataType": "string",
+ "elements": [
+ {
+ "elementType": "input",
+ "elementOptions": [],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": "kea-api"
+ },
+ {
+ "function": "PASS",
+ "localized": ["name", "description"],
+ "name": [{"language_code": "en_us", "string": "API Password"}],
+ "description": [{"language_code": "en_us", "string": "Basic Auth Password"}],
+ "type": {
+ "dataType": "string",
+ "elements": [
+ {
+ "elementType": "input",
+ "elementOptions": [{"type": "password"}],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": ""
+ },
+ {
+ "function": "RUN_SCHD",
+ "type": {
+ "dataType": "string",
+ "elements": [
+ {
+ "elementType": "span",
+ "elementOptions": [
+ {
+ "cssClasses": "input-group-addon validityCheck"
+ },
+ {
+ "getStringKey": "Gen_ValidIcon"
+ }
+ ],
+ "transformers": []
+ },
+ {
+ "elementType": "input",
+ "elementOptions": [
+ {
+ "focusout": "validateRegex(this)"
+ },
+ {
+ "base64Regex": "Xig/OlwqfCg/OlswLTldfFsxLTVdWzAtOV18WzAtOV0rLVswLTldK3xcKi9bMC05XSspKVxzKyg/OlwqfCg/OlswLTldfDFbMC05XXwyWzAtM118WzAtOV0rLVswLTldK3xcKi9bMC05XSspKVxzKyg/OlwqfCg/OlsxLTldfFsxMl1bMC05XXwzWzAxXXxbMC05XSstWzAtOV0rfFwqL1swLTldKykpXHMrKD86XCp8KD86WzEtOV18MVswLTJdfFswLTldKy1bMC05XSt8XCovWzAtOV0rKSlccysoPzpcKnwoPzpbMC02XXxbMC02XS1bMC02XXxcKi9bMC05XSspKSQ="
+ }
+ ],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": "0 2 * * *",
+ "options": [],
+ "localized": ["name", "description"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Schedule"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "Only enabled if you select schedule in the KEALSS_RUN setting. Make sure you enter the schedule in the correct cron-like format (e.g. validate at crontab.guru). For example entering 0 4 * * * will run the scan after 4 am in the TIMEZONE you set above. Will be run NEXT time the time passes. Source = USER or Source = LOCKED)."
+ }
+ ]
+ },
+ {
+ "function": "SET_EMPTY",
+ "type": {
+ "dataType": "array",
+ "elements": [
+ {
+ "elementType": "select",
+ "elementOptions": [{ "multiple": "true", "orderable": "true" }],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": [],
+ "options": [
+ "devMac",
+ "devLastIP",
+ "devName",
+ "devSourcePlugin"
+ ],
+ "localized": ["name", "description"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Set empty columns"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "These columns are only overwritten if they are empty (NULL / empty string) or if their Source is set to NEWDEV"
+ }
+ ]
+ },
+ {
+ "function": "WATCH",
+ "type": {
+ "dataType": "array",
+ "elements": [
+ {
+ "elementType": "select",
+ "elementOptions": [{ "multiple": "true", "orderable": "true"}],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": ["watchedValue1", "watchedValue4"],
+ "options": [
+ "watchedValue1",
+ "watchedValue2",
+ "watchedValue4"
+ ],
+ "localized": ["name", "description"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Watched"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "Send a notification if selected values change. Use CTRL + Click to select/deselect. watchedValue1 is Active watchedValue2 is Hostname watchedValue4 is Statenew means a new unique (unique combination of PrimaryId and SecondaryId) object was discovered. watched-changed means that selected watchedValueN columns changed."
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/front/plugins/kea_api/script.py b/front/plugins/kea_api/script.py
new file mode 100644
index 00000000..1f65253a
--- /dev/null
+++ b/front/plugins/kea_api/script.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python3
+import os
+import sys
+import requests
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../server'))
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../plugins'))
+
+from plugin_helper import Plugin_Objects, mylog, handleEmpty, is_mac
+from helper import get_setting_value
+from const import logPath
+
+pluginName = "KEALSS"
+LOG_PATH = logPath + "/plugins"
+LOG_FILE = os.path.join(LOG_PATH, f'script.{pluginName}.log')
+RESULT_FILE = os.path.join(LOG_PATH, f'last_result.{pluginName}.log')
+
+plugin_objects = Plugin_Objects(RESULT_FILE)
+
+
+def main():
+ try:
+ url = get_setting_value(f'{pluginName}_URL')
+ user = get_setting_value(f'{pluginName}_USER')
+ password = get_setting_value(f'{pluginName}_PASS')
+
+ mylog('verbose', [f'[{pluginName}] Querying Kea API at {url}'])
+
+ payload = {'command': 'lease4-get-all', 'service': ['dhcp4']}
+
+ response = requests.post(url, json=payload, auth=(user, password), timeout=10)
+ response.raise_for_status()
+ data = response.json()
+
+ count = 0
+ for entry in data:
+ # Result: 0 (success), 1 (error), or 3 (empty).
+ if entry.get("result") == 0:
+ leases = entry["arguments"].get("leases", [])
+ for l in leases:
+ mac = l['hw-address']
+ state = l['state']
+ if is_mac(mac):
+ plugin_objects.add_object(
+ primaryId = mac,
+ secondaryId = l['ip-address'],
+ # Active or not, similar to watched 1 of DHCPLSS plugin
+ watched1 = state == 0,
+ watched2 = l['hostname'],
+ watched3 = None,
+ # Default (or assigned) (0), declined (1), expired-reclaimed (2), released (3), and registered (4)).
+ watched4 = state,
+ extra = None,
+ foreignKey = mac
+ )
+ count += 1
+ elif entry.get("result") == 1:
+ mylog('none', [f'[{pluginName}] ⚠ ERROR: Kea API indicated error'])
+
+ plugin_objects.write_result_file()
+
+ mylog('verbose', [f'[{pluginName}] Successfully imported {count} devices reported by Kea API'])
+
+ except Exception as e:
+ mylog('none', [f'[{pluginName}] ⚠ ERROR: {str(e)}'])
+
+
+
+if __name__ == '__main__':
+ main()