#!/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()