206 lines
7.7 KiB
Python
206 lines
7.7 KiB
Python
#!/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()
|
|
|