Skip to content

Commit 3e13fb9

Browse files
authored
Add AdGuard Home snmp extend (#629)
* Add AdGuard Home snmp extend Queries the AdGuard Home REST API (/control/status, /control/stats) and outputs LibreNMS application JSON v1: query/block counters, average processing time, and running/protection state. Config (URL + web UI credentials) lives in /etc/snmp/adguard.json. * Default AdGuard extend timeout to 5 seconds Each poll makes two HTTP calls, so 10s each could overrun typical SNMP timeouts. Document that the SNMP timeout must exceed 2 * timeout. * gzip+base64 encapsulate AdGuard JSON output Some net-snmp snmpd versions mangle raw JSON in extend output. LibreNMS json_app_get already auto-detects and decodes this.
1 parent a9c9709 commit 3e13fb9

1 file changed

Lines changed: 115 additions & 0 deletions

File tree

snmp/adguard

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
#!/usr/bin/env python3
2+
"""LibreNMS snmp extend script for AdGuard Home.
3+
4+
Queries the AdGuard Home REST API (/control/status and /control/stats) and
5+
prints the results as LibreNMS application JSON (version 1),
6+
gzip+base64 encoded so snmpd does not mangle the payload.
7+
8+
Configuration is read from a JSON file (default /etc/snmp/adguard.json):
9+
10+
{
11+
"url": "http://127.0.0.1:3000",
12+
"username": "admin",
13+
"password": "secret",
14+
"timeout": 5,
15+
"insecure": false
16+
}
17+
18+
"url" is the base URL of the AdGuard Home web interface. "insecure" disables
19+
TLS certificate verification for https URLs. "timeout" applies to each of
20+
the two API calls, so the SNMP timeout must exceed 2 * timeout. The config
21+
file holds the web UI credentials, so restrict it to the user snmpd runs
22+
extend scripts as (root:Debian-snmp mode 0640 on Debian/Ubuntu, root-only
23+
0600 where snmpd runs as root).
24+
25+
snmpd.conf entry:
26+
27+
extend adguard /etc/snmp/adguard
28+
29+
Error codes:
30+
1 = config file missing or invalid
31+
2 = HTTP request failed
32+
3 = API response was not valid JSON
33+
"""
34+
35+
import base64
36+
import gzip
37+
import json
38+
import ssl
39+
import sys
40+
import urllib.error
41+
import urllib.request
42+
43+
VERSION = 1
44+
CONFIG_FILE = "/etc/snmp/adguard.json"
45+
46+
# stats keys copied into data verbatim; all are gauges over AdGuard's
47+
# configured stats window (24h by default)
48+
STATS_KEYS = [
49+
"num_dns_queries",
50+
"num_blocked_filtering",
51+
"num_replaced_safebrowsing",
52+
"num_replaced_safesearch",
53+
"num_replaced_parental",
54+
"avg_processing_time",
55+
]
56+
57+
58+
def output(data, error, error_string):
59+
text = json.dumps({
60+
"data": data,
61+
"error": error,
62+
"errorString": error_string,
63+
"version": VERSION,
64+
})
65+
print(base64.b64encode(gzip.compress(text.encode("utf-8"))).decode("ascii"))
66+
sys.exit(0 if error == 0 else 1)
67+
68+
69+
def api_get(base_url, path, auth_header, timeout, insecure):
70+
request = urllib.request.Request(base_url + path)
71+
request.add_header("Authorization", auth_header)
72+
context = None
73+
if insecure:
74+
context = ssl.create_default_context()
75+
context.check_hostname = False
76+
context.verify_mode = ssl.CERT_NONE
77+
with urllib.request.urlopen(request, timeout=timeout, context=context) as response:
78+
return json.loads(response.read().decode("utf-8"))
79+
80+
81+
def main():
82+
config_file = sys.argv[1] if len(sys.argv) > 1 else CONFIG_FILE
83+
84+
try:
85+
with open(config_file) as handle:
86+
config = json.load(handle)
87+
base_url = config["url"].rstrip("/")
88+
credentials = "{}:{}".format(config["username"], config["password"])
89+
except (OSError, ValueError, KeyError) as error:
90+
output({}, 1, "config error: {}".format(error))
91+
92+
auth_header = "Basic " + base64.b64encode(credentials.encode()).decode()
93+
timeout = config.get("timeout", 5)
94+
insecure = bool(config.get("insecure", False))
95+
96+
data = {}
97+
try:
98+
status = api_get(base_url, "/control/status", auth_header, timeout, insecure)
99+
stats = api_get(base_url, "/control/stats", auth_header, timeout, insecure)
100+
except urllib.error.URLError as error:
101+
output({}, 2, "http error: {}".format(error))
102+
except ValueError as error:
103+
output({}, 3, "bad json: {}".format(error))
104+
105+
data["version"] = status.get("version", "")
106+
data["running"] = int(bool(status.get("running", False)))
107+
data["protection_enabled"] = int(bool(status.get("protection_enabled", False)))
108+
for key in STATS_KEYS:
109+
data[key] = stats.get(key, 0)
110+
111+
output(data, 0, "")
112+
113+
114+
if __name__ == "__main__":
115+
main()

0 commit comments

Comments
 (0)