PLG: ADGUARDIMP #1341

Signed-off-by: jokob-sk <jokob.sk@gmail.com>
This commit is contained in:
jokob-sk
2025-12-12 10:27:50 +11:00
parent 899c195d27
commit ed24b4dc18
4 changed files with 690 additions and 1 deletions

View File

@@ -0,0 +1,27 @@
## Overview
Plugin functionality overview and links to external resources if relevant. Include use cases if available.
> [!TIP]
> Some tip.
### Quick setup guide
To set up the plugin correctly, make sure...
#### Required Settings
- When to run `PREF_RUN`
-
### Usage
- Head to **Settings** > **Plugin name** to adjust the default values.
### Notes
- Additional notes, limitations, Author info.
- Version: 1.0.0
- Author: `<your github handle>`
- Release Date: `<release date>`

View File

@@ -0,0 +1,158 @@
#!/usr/bin/env python
import os
import sys
import requests
from pytz import timezone
# Define the installation path and extend the system path for plugin imports
INSTALL_PATH = os.getenv('NETALERTX_APP', '/app')
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from const import logPath # noqa: E402, E261
from plugin_helper import Plugin_Objects # noqa: E402, E261
from utils.crypto_utils import string_to_mac_hash # noqa: E402 [flake8 lint suppression]
from logger import mylog, Logger # noqa: E402, E261
from helper import get_setting_value # noqa: E402, E261
import conf # noqa: E402, E261
# ----------------------------
# Plugin metadata
# ----------------------------
pluginName = "ADGUARDIMP"
# Make sure the TIMEZONE for logging is correct
conf.tz = timezone(get_setting_value("TIMEZONE"))
# Make sure log level is initialized correctly
Logger(get_setting_value("LOG_LEVEL"))
# Define paths
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)
# ----------------------------
# Helpers
# ----------------------------
def ag_request(path, server, port, protocol, auth, timeout):
"""Unified request handler"""
url = f"{protocol}://{server}:{port}{path}"
try:
r = requests.get(url, auth=auth, timeout=timeout, verify=False)
if r.status_code != 200:
mylog("none", [f"[{pluginName}] Failed request {url} -> {r.status_code}"])
return None
return r.json()
except Exception as e:
mylog("none", [f"[{pluginName}] Exception accessing {url}: {e}"])
return None
# ----------------------------
# MAIN
# ----------------------------
def main():
mylog("verbose", [f"[{pluginName}] In script"])
# Retrieve plugin settings
server = get_setting_value("ADGUARDIMP_SERVER")
port = get_setting_value("ADGUARDIMP_PORT")
protocol = get_setting_value("ADGUARDIMP_PROTOCOL") or "http"
user = get_setting_value("ADGUARDIMP_USER")
pw = get_setting_value("ADGUARDIMP_PASS")
fake_mac_enabled = get_setting_value("ADGUARDIMP_FAKE_MAC")
timeout = int(get_setting_value("ADGUARDIMP_RUN_TIMEOUT") or 5)
auth = (user, pw) if user or pw else None
# -------------------------------------------
# Fetch clients from AdGuard Home
# -------------------------------------------
clients_json = ag_request(
"/control/clients",
server, port, protocol, auth, timeout
)
if not clients_json:
mylog("none", [f"[{pluginName}] No clients returned"])
plugin_objects.write_result_file()
return 1
raw_clients = clients_json.get("auto_clients", []) or []
# -------------------------------------------
# Fetch DHCP leases if DHCP enabled
# -------------------------------------------
dhcp_json = ag_request(
"/control/dhcp/status",
server, port, protocol, auth, timeout
)
dhcp_leases = []
if dhcp_json and dhcp_json.get("enabled"):
dhcp_leases = dhcp_json.get("leases", [])
# Build MAC lookup table for DHCP
dhcp_mac_map = {}
for lease in dhcp_leases:
ip = lease.get("ip")
mac = lease.get("mac")
if ip and mac:
dhcp_mac_map[ip] = mac.upper()
# -------------------------------------------
# Process devices
# -------------------------------------------
device_data = []
for cl in raw_clients:
ip = cl.get("ip")
hostname = cl.get("name") or ""
dsource = cl.get("source") or ""
# Determine MAC
mac = dhcp_mac_map.get(ip)
if not mac and fake_mac_enabled:
mylog("verbose", [f"[{pluginName}] Generating FAKE MAC for ip: {ip}"])
mac = string_to_mac_hash(ip)
if not mac:
# Skip devices without MAC if fake MAC not allowed
mylog("verbose", [f"[{pluginName}] Skipping device with {ip} as no MAC supplied and ADGUARDIMP_FAKE_MAC set to False"])
continue
device_data.append({
"mac_address": mac,
"ip_address": ip,
"hostname": hostname,
"device_type": dsource
})
# -------------------------------------------
# Write plugin objects
# -------------------------------------------
for dev in device_data:
plugin_objects.add_object(
primaryId = dev["mac_address"],
secondaryId = dev["ip_address"],
watched1 = dev["hostname"],
watched2 = dev["device_type"],
watched3 = '',
watched4 = '',
extra = '',
foreignKey = dev["mac_address"],
)
mylog("verbose", [f"[{pluginName}] New entries: {len(device_data)}"])
plugin_objects.write_result_file()
return 0
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,504 @@
{
"code_name": "adguard_import",
"unique_prefix": "ADGUARDIMP",
"plugin_type": "device_scanner",
"execution_order" : "Layer_0",
"enabled": true,
"data_source": "script",
"mapped_to_table": "CurrentScan",
"data_filters": [
{
"compare_column": "Object_PrimaryID",
"compare_operator": "==",
"compare_field_id": "txtMacFilter",
"compare_js_template": "'{value}'.toString()",
"compare_use_quotes": true
}
],
"show_ui": true,
"localized": ["display_name", "description", "icon"],
"display_name": [
{
"language_code": "en_us",
"string": "AdGuard (Device import)"
}
],
"description": [
{
"language_code": "en_us",
"string": "Plugin to ..."
}
],
"icon": [
{
"language_code": "en_us",
"string": "<i class=\"fa fa-search\"></i>"
}
],
"params": [],
"settings": [
{
"function": "RUN",
"events": ["run"],
"type": {
"dataType": "string",
"elements": [
{ "elementType": "select", "elementOptions": [], "transformers": [] }
]
},
"default_value": "disabled",
"options": [
"disabled",
"before_name_updates",
"on_new_device",
"once",
"schedule",
"always_after_scan"
],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "When to run"
}
],
"description": [
{
"language_code": "en_us",
"string": "When the plugin should run. A good option is <code>schedule</code> for device scanners."
}
]
},
{
"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": "*/5 * * * *",
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Schedule"
}
],
"description": [
{
"language_code": "en_us",
"string": "Only enabled if you select <code>schedule</code> in the <a href=\"#SYNC_RUN\"><code>SYNC_RUN</code> setting</a>. Make sure you enter the schedule in the correct cron-like format (e.g. validate at <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). For example entering <code>0 4 * * *</code> will run the scan after 4 am in the <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</code> you set above</a>. Will be run NEXT time the time passes."
}
]
},
{
"function": "CMD",
"type": {
"dataType": "string",
"elements": [
{
"elementType": "input",
"elementOptions": [{ "readonly": "true" }],
"transformers": []
}
]
},
"default_value": "python3 /app/front/plugins/adguard_import/adguard_import.py",
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Command"
}
],
"description": [
{
"language_code": "en_us",
"string": "Command to run. This can not be changed"
}
]
},
{
"function": "RUN_TIMEOUT",
"type": {
"dataType": "integer",
"elements": [
{
"elementType": "input",
"elementOptions": [{ "type": "number" }],
"transformers": []
}
]
},
"default_value": 30,
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Run timeout"
}
],
"description": [
{
"language_code": "en_us",
"string": "Maximum time in seconds to wait for the script to finish. If this time is exceeded the script is aborted."
}
]
},
{
"function": "SERVER",
"type": {
"dataType": "string",
"elements": [
{ "elementType": "input", "elementOptions": [], "transformers": [] }
]
},
"maxLength": 200,
"default_value": "",
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "AdGuard Home Server"
}
],
"description": [
{
"language_code": "en_us",
"string": "Hostname or IP of your AdGuard Home server. <br>Example: <code>192.168.1.10</code> or <code>adguard.local</code>"
}
]
},
{
"function": "PORT",
"type": {
"dataType": "integer",
"elements": [
{ "elementType": "input", "elementOptions": [{ "type": "number" }], "transformers": [] }
]
},
"default_value": 3000,
"localized": ["name", "description"],
"name": [
{ "language_code": "en_us", "string": "Port" }
],
"description": [
{
"language_code": "en_us",
"string": "Port used by AdGuard Home API. Default is normally <code>3000</code>."
}
]
},
{
"function": "PROTOCOL",
"type": {
"dataType": "string",
"elements": [
{ "elementType": "select", "elementOptions": [], "transformers": [] }
]
},
"default_value": "http",
"options": ["http", "https"],
"localized": ["name", "description"],
"name": [
{ "language_code": "en_us", "string": "Protocol" }
],
"description": [
{
"language_code": "en_us",
"string": "Choose whether to use HTTP or HTTPS to connect to the AdGuard Home API."
}
]
},
{
"function": "USER",
"type": {
"dataType": "string",
"elements": [
{ "elementType": "input", "elementOptions": [], "transformers": [] }
]
},
"maxLength": 200,
"default_value": "",
"localized": ["name", "description"],
"name": [
{ "language_code": "en_us", "string": "Username" }
],
"description": [
{
"language_code": "en_us",
"string": "API username for AdGuard Home. Leave empty if your API does not require login."
}
]
},
{
"function": "PASS",
"type": {
"dataType": "string",
"elements": [
{
"elementType": "input",
"elementOptions": [{ "type": "password" }],
"transformers": []
}
]
},
"maxLength": 200,
"default_value": "",
"localized": ["name", "description"],
"name": [
{ "language_code": "en_us", "string": "Password" }
],
"description": [
{
"language_code": "en_us",
"string": "API password for AdGuard Home. Leave empty if authentication is disabled."
}
]
},
{
"function": "FAKE_MAC",
"type": {
"dataType": "boolean",
"elements": [
{
"elementType": "input",
"elementOptions": [{ "type": "checkbox" }],
"transformers": []
}
]
},
"default_value": false,
"localized": ["name", "description"],
"name": [
{ "language_code": "en_us", "string": "Generate Fake MACs" }
],
"description": [
{
"language_code": "en_us",
"string": "Some devices don't have a MAC assigned. Enabling the FAKE_MAC setting generates a fake MAC address from the IP address to track devices, but it may cause inconsistencies if IPs change or devices are re-discovered with a different MAC. Static IPs are recommended. Device type and icon might not be detected correctly and some plugins might fail if they depend on a valid MAC address. When unchecked, devices with empty MAC addresses are skipped."
}
]
}
],
"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": "Object_PrimaryID",
"mapped_to_column": "cur_MAC",
"css_classes": "col-sm-3",
"show": true,
"type": "device_name_mac",
"default_value": "",
"options": [],
"localized": ["name"],
"name": [
{
"language_code": "en_us",
"string": "MAC (name)"
}
]
},
{
"column": "Object_SecondaryID",
"mapped_to_column": "cur_IP",
"css_classes": "col-sm-2",
"show": true,
"type": "device_ip",
"default_value": "",
"options": [],
"localized": ["name"],
"name": [
{
"language_code": "en_us",
"string": "IP"
}
]
},
{
"column": "Watched_Value1",
"mapped_to_column": "cur_Name",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": ["name"],
"name": [
{
"language_code": "en_us",
"string": "Name"
}
]
},
{
"column": "Watched_Value2",
"mapped_to_column": "cur_Type",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": ["name"],
"name": [
{
"language_code": "en_us",
"string": "Device Type"
}
]
},
{
"column": "Watched_Value3",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value": "",
"options": [],
"localized": ["name"],
"name": [
{
"language_code": "en_us",
"string": "N/A"
}
]
},
{
"column": "Watched_Value4",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value": "",
"options": [],
"localized": ["name"],
"name": [
{
"language_code": "en_us",
"string": "N/A"
}
]
},
{
"column": "Dummy",
"mapped_to_column": "cur_ScanMethod",
"mapped_to_column_data": {
"value": "Example Plugin"
},
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value": "",
"options": [],
"localized": ["name"],
"name": [
{
"language_code": "en_us",
"string": "ADGUARDIMP"
}
]
},
{
"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",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": ["name"],
"name": [
{
"language_code": "en_us",
"string": "Changed"
}
]
},
{
"column": "Status",
"css_classes": "col-sm-1",
"show": true,
"type": "replace",
"default_value": "",
"options": [
{
"equals": "watched-not-changed",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-square-check'></i><div></div>"
},
{
"equals": "watched-changed",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-triangle-exclamation'></i></div>"
},
{
"equals": "new",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-circle-plus'></i></div>"
},
{
"equals": "missing-in-last-scan",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-question'></i></div>"
}
],
"localized": ["name"],
"name": [
{
"language_code": "en_us",
"string": "Status"
}
]
}
]
}