feat: add homelab topology documentation, infrastructure improvement plans, and management scripts
This commit is contained in:
205
scripts/setup_zabbix_notifications.py
Normal file
205
scripts/setup_zabbix_notifications.py
Normal file
@@ -0,0 +1,205 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Configure Zabbix 7.0.16 Notification Pipeline:
|
||||
1. Creates/Updates 'Snarfnet Gotify + Discord Failover' Webhook MediaType.
|
||||
- Primary path: Sends to Gotify (http://140.44.4.70:8090) -> pushes to devices + Gotify-Discord Bridge.
|
||||
- Failover path: If Gotify/Shipyard is unreachable, sends directly to Discord (#infra-alerts or #ups-power).
|
||||
2. Attaches MediaType to Zabbix 'Admin' user (userid=1).
|
||||
3. Enables Trigger Action 'Report problems to Zabbix administrators' (status=0).
|
||||
"""
|
||||
import json
|
||||
import ssl
|
||||
import urllib.request
|
||||
|
||||
ZABBIX_URL = "https://zabbix.snarfnet.net/api_jsonrpc.php"
|
||||
ZABBIX_USER = "Admin"
|
||||
ZABBIX_PASS = "zabbix"
|
||||
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
|
||||
def zbx_req(method: str, params: dict, auth: str = None):
|
||||
payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": method,
|
||||
"params": params,
|
||||
"id": 1,
|
||||
}
|
||||
headers = {"Content-Type": "application/json-rpc"}
|
||||
if auth:
|
||||
headers["Authorization"] = f"Bearer {auth}"
|
||||
req = urllib.request.Request(
|
||||
ZABBIX_URL,
|
||||
data=json.dumps(payload).encode("utf-8"),
|
||||
headers=headers,
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=10, context=ctx) as r:
|
||||
resp = json.loads(r.read().decode("utf-8"))
|
||||
if "error" in resp:
|
||||
raise RuntimeError(f"Zabbix API error in {method}: {resp['error']}")
|
||||
return resp["result"]
|
||||
|
||||
|
||||
WEBHOOK_JS = r"""
|
||||
try {
|
||||
var params = JSON.parse(value);
|
||||
var subject = params.Subject || 'Zabbix Alert';
|
||||
var message = params.Message || '';
|
||||
var severity = parseInt(params.Severity || '3', 10);
|
||||
var host = (params.Host || '').toLowerCase();
|
||||
|
||||
// Map Zabbix severity (0-5) to Gotify priority (2-10)
|
||||
var prioMap = {0: 2, 1: 3, 2: 5, 3: 7, 4: 9, 5: 10};
|
||||
var priority = prioMap[severity] || 6;
|
||||
|
||||
var isUps = (host.indexOf('nut') !== -1 || host.indexOf('ups') !== -1 || subject.toLowerCase().indexOf('ups') !== -1);
|
||||
var gotifyToken = isUps ? params.GotifyUpsToken : params.GotifyInfraToken;
|
||||
var discordWebhook = isUps ? params.DiscordUpsWebhook : params.DiscordInfraWebhook;
|
||||
|
||||
var req = new HttpRequest();
|
||||
req.addHeader('Content-Type: application/json');
|
||||
|
||||
var gotifyUrl = params.GotifyUrl.replace(/\/$/, '') + '/message?token=' + gotifyToken;
|
||||
var gotifyPayload = JSON.stringify({
|
||||
title: subject,
|
||||
message: message,
|
||||
priority: priority
|
||||
});
|
||||
|
||||
var resp = req.post(gotifyUrl, gotifyPayload);
|
||||
if (req.getStatus() >= 200 && req.getStatus() < 300) {
|
||||
return 'OK (Delivered via Gotify + Discord Bridge)';
|
||||
}
|
||||
|
||||
// Automatic Direct-to-Discord Failover if Gotify/Shipyard is unreachable
|
||||
Zabbix.log(3, '[Snarfnet Webhook] Gotify returned HTTP ' + req.getStatus() + ', failing over directly to Discord.');
|
||||
var dReq = new HttpRequest();
|
||||
dReq.addHeader('Content-Type: application/json');
|
||||
var color = (priority >= 8) ? 15158332 : ((priority >= 5) ? 15965202 : 3447003);
|
||||
var discordPayload = JSON.stringify({
|
||||
username: 'Snarfnet Zabbix (Direct Failover)',
|
||||
embeds: [{
|
||||
title: subject.substring(0, 256),
|
||||
description: message.substring(0, 4000),
|
||||
color: color,
|
||||
footer: {text: 'Zabbix Direct Failover • Priority ' + priority}
|
||||
}]
|
||||
});
|
||||
dReq.post(discordWebhook, discordPayload);
|
||||
if (dReq.getStatus() >= 200 && dReq.getStatus() < 300) {
|
||||
return 'OK (Delivered via Direct Discord Failover)';
|
||||
}
|
||||
throw 'Both Gotify and Direct Discord failed (Discord HTTP ' + dReq.getStatus() + ')';
|
||||
} catch (error) {
|
||||
Zabbix.log(3, '[Snarfnet Webhook] Error: ' + error);
|
||||
throw 'Sending failed: ' + error;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
token = zbx_req("user.login", {"username": ZABBIX_USER, "password": ZABBIX_PASS})
|
||||
print("Authenticated to Zabbix 7.0.16 API.")
|
||||
|
||||
mt_name = "Snarfnet Gotify + Discord Failover"
|
||||
existing = zbx_req("mediatype.get", {"filter": {"name": [mt_name]}}, auth=token)
|
||||
|
||||
mt_params = [
|
||||
{"name": "Subject", "value": "{ALERT.SUBJECT}"},
|
||||
{"name": "Message", "value": "{ALERT.MESSAGE}"},
|
||||
{"name": "Severity", "value": "{EVENT.NSEVERITY}"},
|
||||
{"name": "Host", "value": "{HOST.NAME}"},
|
||||
{"name": "GotifyUrl", "value": "http://140.44.4.70:8090"},
|
||||
{"name": "GotifyInfraToken", "value": "A7zBx9InFrAaLeRtStOk"},
|
||||
{"name": "GotifyUpsToken", "value": "A4uN4fEmErGeNcYtOkEn"},
|
||||
{
|
||||
"name": "DiscordInfraWebhook",
|
||||
"value": "https://discord.com/api/webhooks/1566076777963593772/wKN30N0ZXVJKGP-prZ1_P9cp_u7Oakmh8R4bVh0qr8RFpyeGcJZrHfPQBJxtfzQqb4dn",
|
||||
},
|
||||
{
|
||||
"name": "DiscordUpsWebhook",
|
||||
"value": "https://discord.com/api/webhooks/1566076118535114813/2e_fCJeHb-mF8VwerzOORGRa-QUDFuwN6R2Gfz-MGUiIOudA0PiXGZSyd19PNFG-vR7D",
|
||||
},
|
||||
]
|
||||
|
||||
mt_templates = [
|
||||
{
|
||||
"eventsource": 0, # Triggers
|
||||
"recovery": 0, # Problem
|
||||
"subject": "🚨 [{EVENT.SEVERITY}] {HOST.NAME}: {EVENT.NAME}",
|
||||
"message": "**Host:** `{HOST.NAME}` (`{HOST.IP}`)\n**Severity:** {EVENT.SEVERITY}\n**Trigger:** {EVENT.NAME}\n**Status:** {EVENT.STATUS}\n**Time:** {EVENT.DATE} {EVENT.TIME}\n**Event ID:** {EVENT.ID}",
|
||||
},
|
||||
{
|
||||
"eventsource": 0, # Triggers
|
||||
"recovery": 1, # Recovery
|
||||
"subject": "✅ [RESOLVED] {HOST.NAME}: {EVENT.NAME}",
|
||||
"message": "**Host:** `{HOST.NAME}` (`{HOST.IP}`)\n**Resolved After:** {EVENT.DURATION}\n**Trigger:** {EVENT.NAME}\n**Time:** {EVENT.RECOVERY.DATE} {EVENT.RECOVERY.TIME}\n**Event ID:** {EVENT.ID}",
|
||||
},
|
||||
]
|
||||
|
||||
if existing:
|
||||
mt_id = existing[0]["mediatypeid"]
|
||||
zbx_req(
|
||||
"mediatype.update",
|
||||
{
|
||||
"mediatypeid": mt_id,
|
||||
"type": 4, # Webhook
|
||||
"status": 0, # Enabled
|
||||
"script": WEBHOOK_JS.strip(),
|
||||
"timeout": "15s",
|
||||
"parameters": mt_params,
|
||||
"message_templates": mt_templates,
|
||||
},
|
||||
auth=token,
|
||||
)
|
||||
print(f"Updated MediaType '{mt_name}' (mediatypeid={mt_id})")
|
||||
else:
|
||||
res = zbx_req(
|
||||
"mediatype.create",
|
||||
{
|
||||
"name": mt_name,
|
||||
"type": 4, # Webhook
|
||||
"status": 0, # Enabled
|
||||
"script": WEBHOOK_JS.strip(),
|
||||
"timeout": "15s",
|
||||
"parameters": mt_params,
|
||||
"message_templates": mt_templates,
|
||||
},
|
||||
auth=token,
|
||||
)
|
||||
mt_id = res["mediatypeids"][0]
|
||||
print(f"Created MediaType '{mt_name}' (mediatypeid={mt_id})")
|
||||
|
||||
# Attach to Admin user (userid=1) for Warning, Average, High, Disaster (bitmask 60 = 4+8+16+32)
|
||||
zbx_req(
|
||||
"user.update",
|
||||
{
|
||||
"userid": "1",
|
||||
"medias": [
|
||||
{
|
||||
"mediatypeid": str(mt_id),
|
||||
"sendto": "snarf@snarfnet.net",
|
||||
"active": 0,
|
||||
"severity": 60, # Warning (2), Average (3), High (4), Disaster (5)
|
||||
"period": "1-7,00:00-24:00",
|
||||
}
|
||||
],
|
||||
},
|
||||
auth=token,
|
||||
)
|
||||
print("Attached MediaType to Zabbix Admin user (userid=1).")
|
||||
|
||||
# Enable Trigger Action 'Report problems to Zabbix administrators'
|
||||
actions = zbx_req("action.get", {"filter": {"eventsource": 0}}, auth=token)
|
||||
for act in actions:
|
||||
if "Report problems" in act["name"] or act["actionid"] == "3":
|
||||
zbx_req("action.update", {"actionid": act["actionid"], "status": 0}, auth=token)
|
||||
print(f"Enabled Trigger Action '{act['name']}' (actionid={act['actionid']}).")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user