Add Zabbix template with offline and last-seen triggers
- zabbix_template.yaml: importable template "Yodmon Yodeck Player"
- 5 SNMP items using {$YODECK_ID} host macro for per-host OIDs
- Trigger: offline warning after 30 minutes (yodeck.online < 1)
- Trigger: last seen > 4 hours (now() - yodeck.last_seen_ts > 14400)
- snmp/pass_persist.py: add col 7 — last_seen as Unix timestamp
- app/zabbix.py: link hosts to template, upsert {$YODECK_ID} macro;
existing hosts get template linked on next sync automatically
- app/config.py: add ZABBIX_TEMPLATE setting
Import zabbix_template.yaml once via Zabbix UI, then redeploy.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ ZABBIX_API_TOKEN = os.environ.get('ZABBIX_API_TOKEN', '')
|
||||
ZABBIX_USER = os.environ.get('ZABBIX_USER', 'Admin')
|
||||
ZABBIX_PASSWORD = os.environ.get('ZABBIX_PASSWORD', '')
|
||||
ZABBIX_HOST_GROUP = os.environ.get('ZABBIX_HOST_GROUP', 'Yodeck Players')
|
||||
ZABBIX_TEMPLATE = os.environ.get('ZABBIX_TEMPLATE', 'Yodmon Yodeck Player')
|
||||
ZABBIX_SNMP_COMMUNITY = os.environ.get('ZABBIX_SNMP_COMMUNITY', 'public')
|
||||
# IP/hostname of this app reachable by the Zabbix server for SNMP polling
|
||||
APP_HOST = os.environ.get('APP_HOST', '127.0.0.1')
|
||||
|
||||
@@ -11,7 +11,7 @@ import logging
|
||||
import requests
|
||||
from app.config import (
|
||||
ZABBIX_URL, ZABBIX_API_TOKEN, ZABBIX_USER, ZABBIX_PASSWORD,
|
||||
ZABBIX_HOST_GROUP, ZABBIX_SNMP_COMMUNITY,
|
||||
ZABBIX_HOST_GROUP, ZABBIX_TEMPLATE, ZABBIX_SNMP_COMMUNITY,
|
||||
APP_HOST, ENTERPRISE_OID,
|
||||
)
|
||||
|
||||
@@ -90,6 +90,7 @@ class ZabbixClient:
|
||||
return self._call('host.get', {
|
||||
'groupids': groupid,
|
||||
'output': ['hostid', 'host', 'name'],
|
||||
'selectParentTemplates': ['templateid'],
|
||||
})
|
||||
|
||||
def create_host(self, hostname, visible_name, groupid):
|
||||
@@ -116,6 +117,34 @@ class ZabbixClient:
|
||||
def update_host_name(self, hostid, visible_name):
|
||||
self._call('host.update', {'hostid': hostid, 'name': visible_name})
|
||||
|
||||
# ------------------------------------------------------------------ templates
|
||||
|
||||
def get_template_id(self, name):
|
||||
result = self._call('template.get', {'filter': {'host': name}, 'output': ['templateid']})
|
||||
return result[0]['templateid'] if result else None
|
||||
|
||||
def link_template(self, hostid, templateid):
|
||||
# host.update with templates replaces the list — fetch existing first to avoid unlinking others
|
||||
existing = self._call('host.get', {
|
||||
'hostids': hostid,
|
||||
'selectParentTemplates': ['templateid'],
|
||||
'output': [],
|
||||
})
|
||||
current = [{'templateid': t['templateid']} for t in existing[0].get('parentTemplates', [])]
|
||||
if not any(t['templateid'] == templateid for t in current):
|
||||
self._call('host.update', {'hostid': hostid, 'templates': current + [{'templateid': templateid}]})
|
||||
|
||||
def upsert_host_macro(self, hostid, macro, value):
|
||||
existing = self._call('usermacro.get', {
|
||||
'hostids': hostid,
|
||||
'filter': {'macro': macro},
|
||||
'output': ['hostmacroid'],
|
||||
})
|
||||
if existing:
|
||||
self._call('usermacro.update', {'hostmacroid': existing[0]['hostmacroid'], 'value': value})
|
||||
else:
|
||||
self._call('usermacro.create', {'hostid': hostid, 'macro': macro, 'value': value})
|
||||
|
||||
# ------------------------------------------------------------------ items
|
||||
|
||||
def _get_interface_id(self, hostid):
|
||||
@@ -157,7 +186,7 @@ class ZabbixClient:
|
||||
|
||||
|
||||
def sync_to_zabbix(players, add_log_fn):
|
||||
"""Sync player list to Zabbix: create missing hosts, update names."""
|
||||
"""Sync player list to Zabbix: create missing hosts, update names, link template."""
|
||||
if not ZABBIX_URL:
|
||||
log.debug("ZABBIX_URL not configured — skipping Zabbix sync")
|
||||
return
|
||||
@@ -165,7 +194,10 @@ def sync_to_zabbix(players, add_log_fn):
|
||||
zbx = ZabbixClient()
|
||||
try:
|
||||
zbx.login()
|
||||
groupid = zbx.ensure_hostgroup(ZABBIX_HOST_GROUP)
|
||||
groupid = zbx.ensure_hostgroup(ZABBIX_HOST_GROUP)
|
||||
template_id = zbx.get_template_id(ZABBIX_TEMPLATE) if ZABBIX_TEMPLATE else None
|
||||
if ZABBIX_TEMPLATE and not template_id:
|
||||
log.warning("Template '%s' not found in Zabbix — import zabbix_template.yaml first", ZABBIX_TEMPLATE)
|
||||
existing = {h['host']: h for h in zbx.get_hosts_in_group(groupid)}
|
||||
|
||||
created = updated = 0
|
||||
@@ -176,13 +208,25 @@ def sync_to_zabbix(players, add_log_fn):
|
||||
|
||||
if hostname not in existing:
|
||||
hostid = zbx.create_host(hostname, visible, groupid)
|
||||
zbx.add_player_items(hostid, yid)
|
||||
zbx.upsert_host_macro(hostid, '{$YODECK_ID}', str(yid))
|
||||
if template_id:
|
||||
zbx.link_template(hostid, template_id)
|
||||
else:
|
||||
zbx.add_player_items(hostid, yid)
|
||||
created += 1
|
||||
log.info("Created Zabbix host: %s (%s)", hostname, visible)
|
||||
else:
|
||||
if existing[hostname]['name'] != visible:
|
||||
zbx.update_host_name(existing[hostname]['hostid'], visible)
|
||||
h = existing[hostname]
|
||||
if h['name'] != visible:
|
||||
zbx.update_host_name(h['hostid'], visible)
|
||||
updated += 1
|
||||
# Link template and set macro on existing hosts that don't have it yet
|
||||
if template_id:
|
||||
linked = {t['templateid'] for t in h.get('parentTemplates', [])}
|
||||
if template_id not in linked:
|
||||
zbx.upsert_host_macro(h['hostid'], '{$YODECK_ID}', str(yid))
|
||||
zbx.link_template(h['hostid'], template_id)
|
||||
updated += 1
|
||||
|
||||
msg = f"Zabbix sync complete: {created} created, {updated} updated ({len(players)} total)"
|
||||
add_log_fn('zabbix_sync', msg, {'created': created, 'updated': updated, 'total': len(players)})
|
||||
|
||||
@@ -36,6 +36,7 @@ services:
|
||||
ZABBIX_USER: ""
|
||||
ZABBIX_PASSWORD: ""
|
||||
ZABBIX_HOST_GROUP: "Yodeck Players"
|
||||
ZABBIX_TEMPLATE: "Yodmon Yodeck Player"
|
||||
ZABBIX_SNMP_COMMUNITY: "public"
|
||||
|
||||
volumes:
|
||||
|
||||
@@ -7,10 +7,11 @@ Reads player data from the SQLite database and serves it over SNMP v2c.
|
||||
OID layout (base = ENTERPRISE_OID.1.1):
|
||||
base.1.<yodeck_id> STRING "yodeck-<id>"
|
||||
base.2.<yodeck_id> STRING player display name
|
||||
base.3.<yodeck_id> INTEGER online (1 = online, 0 = offline)
|
||||
base.4.<yodeck_id> STRING last_seen (ISO-8601 timestamp)
|
||||
base.5.<yodeck_id> INTEGER updating (1 = yes, 0 = no)
|
||||
base.6.<yodeck_id> INTEGER registered (1 = yes, 0 = no)
|
||||
base.3.<yodeck_id> INTEGER online (1 = online, 0 = offline)
|
||||
base.4.<yodeck_id> STRING last_seen (ISO-8601 timestamp)
|
||||
base.5.<yodeck_id> INTEGER updating (1 = yes, 0 = no)
|
||||
base.6.<yodeck_id> INTEGER registered (1 = yes, 0 = no)
|
||||
base.7.<yodeck_id> INTEGER last_seen_unix (Unix timestamp, 0 if unknown)
|
||||
|
||||
pass_persist protocol (net-snmp):
|
||||
stdin: PING | get\n<oid> | getnext\n<oid>
|
||||
@@ -25,6 +26,17 @@ DB_PATH = os.environ.get('DB_PATH', '/data/yodmon.db')
|
||||
ENTERPRISE_OID = os.environ.get('ENTERPRISE_OID', '.1.3.6.1.4.1.99999')
|
||||
BASE_OID = f"{ENTERPRISE_OID}.1.1"
|
||||
|
||||
def _iso_to_unix(iso_str):
|
||||
"""Convert ISO-8601 timestamp string to Unix epoch integer, 0 if missing."""
|
||||
if not iso_str:
|
||||
return 0
|
||||
try:
|
||||
dt = datetime.fromisoformat(iso_str.replace('Z', '+00:00'))
|
||||
return int(dt.timestamp())
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
# (column_index, snmp_type, value_extractor)
|
||||
COLUMNS = [
|
||||
(1, 'STRING', lambda p: f"yodeck-{p['id']}"),
|
||||
@@ -33,6 +45,7 @@ COLUMNS = [
|
||||
(4, 'STRING', lambda p: p.get('last_seen') or ''),
|
||||
(5, 'INTEGER', lambda p: str(p.get('updating', 0))),
|
||||
(6, 'INTEGER', lambda p: str(p.get('registered', 0))),
|
||||
(7, 'INTEGER', lambda p: str(_iso_to_unix(p.get('last_seen')))),
|
||||
]
|
||||
|
||||
# Rate-limit SNMP transfer log entries (at most one per minute)
|
||||
|
||||
106
zabbix_template.yaml
Normal file
106
zabbix_template.yaml
Normal file
@@ -0,0 +1,106 @@
|
||||
zabbix_export:
|
||||
version: '7.4'
|
||||
template_groups:
|
||||
- uuid: 7df96b18c230490a9a0a9e2307226338
|
||||
name: Templates
|
||||
templates:
|
||||
- uuid: a1b2c3d4e5f6789012345678deadbeef
|
||||
template: Yodmon Yodeck Player
|
||||
name: Yodmon Yodeck Player
|
||||
description: |
|
||||
Yodmon: monitors a Yodeck digital signage player via SNMP.
|
||||
|
||||
Required host macro: {$YODECK_ID} — set automatically by Yodmon.
|
||||
The SNMP interface must point to the Yodmon host on port 161 with
|
||||
the configured community string.
|
||||
|
||||
OID base: .1.3.6.1.4.1.99999.1.1.<column>.{$YODECK_ID}
|
||||
groups:
|
||||
- name: Templates
|
||||
items:
|
||||
- uuid: 11111111222233334444555566667701
|
||||
name: Online
|
||||
type: SNMP_AGENT
|
||||
snmp_oid: '.1.3.6.1.4.1.99999.1.1.3.{$YODECK_ID}'
|
||||
key: yodeck.online
|
||||
delay: 1m
|
||||
history: 90d
|
||||
value_type: UNSIGNED
|
||||
description: '1 = online, 0 = offline'
|
||||
tags:
|
||||
- tag: yodmon
|
||||
|
||||
- uuid: 11111111222233334444555566667702
|
||||
name: Last Seen Timestamp
|
||||
type: SNMP_AGENT
|
||||
snmp_oid: '.1.3.6.1.4.1.99999.1.1.7.{$YODECK_ID}'
|
||||
key: yodeck.last_seen_ts
|
||||
delay: 1m
|
||||
history: 90d
|
||||
value_type: UNSIGNED
|
||||
description: 'Unix timestamp of last contact with Yodeck cloud (used for age-based trigger)'
|
||||
tags:
|
||||
- tag: yodmon
|
||||
|
||||
- uuid: 11111111222233334444555566667703
|
||||
name: Last Seen
|
||||
type: SNMP_AGENT
|
||||
snmp_oid: '.1.3.6.1.4.1.99999.1.1.4.{$YODECK_ID}'
|
||||
key: yodeck.last_seen
|
||||
delay: 1m
|
||||
history: 90d
|
||||
value_type: TEXT
|
||||
description: 'ISO-8601 timestamp of last contact with Yodeck cloud'
|
||||
tags:
|
||||
- tag: yodmon
|
||||
|
||||
- uuid: 11111111222233334444555566667704
|
||||
name: Updating
|
||||
type: SNMP_AGENT
|
||||
snmp_oid: '.1.3.6.1.4.1.99999.1.1.5.{$YODECK_ID}'
|
||||
key: yodeck.updating
|
||||
delay: 1m
|
||||
history: 90d
|
||||
value_type: UNSIGNED
|
||||
description: '1 = firmware update in progress, 0 = idle'
|
||||
tags:
|
||||
- tag: yodmon
|
||||
|
||||
- uuid: 11111111222233334444555566667705
|
||||
name: Registered
|
||||
type: SNMP_AGENT
|
||||
snmp_oid: '.1.3.6.1.4.1.99999.1.1.6.{$YODECK_ID}'
|
||||
key: yodeck.registered
|
||||
delay: 1m
|
||||
history: 90d
|
||||
value_type: UNSIGNED
|
||||
description: '1 = registered with Yodeck cloud, 0 = not registered'
|
||||
tags:
|
||||
- tag: yodmon
|
||||
|
||||
triggers:
|
||||
- uuid: 22222222333344445555666677778801
|
||||
expression: 'max(/Yodmon Yodeck Player/yodeck.online,30m)<1'
|
||||
name: '{HOST.NAME} is offline'
|
||||
priority: WARNING
|
||||
description: |
|
||||
The player has reported offline status for 30 consecutive minutes.
|
||||
Check the device, network, and Yodeck dashboard.
|
||||
tags:
|
||||
- tag: yodmon
|
||||
|
||||
- uuid: 22222222333344445555666677778802
|
||||
expression: 'now()-last(/Yodmon Yodeck Player/yodeck.last_seen_ts)>14400 and last(/Yodmon Yodeck Player/yodeck.last_seen_ts)>0'
|
||||
name: '{HOST.NAME} not seen for 4+ hours'
|
||||
priority: WARNING
|
||||
description: |
|
||||
The last contact timestamp from Yodeck is more than 4 hours old.
|
||||
The player may be disconnected or the Yodeck cloud may not be
|
||||
receiving heartbeats from the device.
|
||||
tags:
|
||||
- tag: yodmon
|
||||
|
||||
macros:
|
||||
- macro: '{$YODECK_ID}'
|
||||
value: ''
|
||||
description: 'Yodeck player ID — set automatically by Yodmon on host creation'
|
||||
Reference in New Issue
Block a user