- 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>
151 lines
4.5 KiB
Python
151 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
net-snmp pass_persist handler for Yodmon.
|
|
|
|
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.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>
|
|
stdout: PONG | <oid>\n<type>\n<value> | NONE
|
|
"""
|
|
import os
|
|
import sys
|
|
import sqlite3
|
|
from datetime import datetime, timezone
|
|
|
|
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']}"),
|
|
(2, 'STRING', lambda p: p.get('name') or ''),
|
|
(3, 'INTEGER', lambda p: str(p.get('online', 0))),
|
|
(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)
|
|
_last_log_ts = 0.0
|
|
|
|
|
|
def _get_players():
|
|
try:
|
|
conn = sqlite3.connect(DB_PATH)
|
|
conn.row_factory = sqlite3.Row
|
|
rows = conn.execute('SELECT * FROM players ORDER BY id').fetchall()
|
|
conn.close()
|
|
return [dict(r) for r in rows]
|
|
except Exception:
|
|
return []
|
|
|
|
|
|
def _build_tree():
|
|
"""Return a sorted list of (oid_tuple, oid_str, snmp_type, value)."""
|
|
entries = []
|
|
for player in _get_players():
|
|
pid = player['id']
|
|
for col, typ, getter in COLUMNS:
|
|
oid_str = f"{BASE_OID}.{col}.{pid}"
|
|
oid_tuple = tuple(int(x) for x in oid_str.lstrip('.').split('.'))
|
|
entries.append((oid_tuple, oid_str, typ, getter(player)))
|
|
entries.sort(key=lambda e: e[0])
|
|
return entries
|
|
|
|
|
|
def _to_tuple(oid_str):
|
|
return tuple(int(x) for x in oid_str.lstrip('.').split('.'))
|
|
|
|
|
|
def _log_transfer():
|
|
global _last_log_ts
|
|
import time
|
|
now = time.time()
|
|
if now - _last_log_ts < 60:
|
|
return
|
|
_last_log_ts = now
|
|
try:
|
|
conn = sqlite3.connect(DB_PATH)
|
|
conn.execute(
|
|
"INSERT INTO logs (timestamp, event_type, message) VALUES (?, ?, ?)",
|
|
(datetime.now(timezone.utc).isoformat(), 'snmp_transfer', 'SNMP data served to Zabbix'),
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def _reply(oid_str, typ, value):
|
|
_log_transfer()
|
|
sys.stdout.write(f"{oid_str}\n{typ}\n{value}\n")
|
|
|
|
|
|
def main():
|
|
while True:
|
|
try:
|
|
line = sys.stdin.readline()
|
|
if not line:
|
|
break
|
|
cmd = line.strip()
|
|
|
|
if cmd == 'PING':
|
|
sys.stdout.write('PONG\n')
|
|
sys.stdout.flush()
|
|
continue
|
|
|
|
if cmd in ('get', 'getnext'):
|
|
raw_oid = sys.stdin.readline().strip()
|
|
tree = _build_tree()
|
|
|
|
if cmd == 'get':
|
|
tup = _to_tuple(raw_oid)
|
|
match = next((e for e in tree if e[0] == tup), None)
|
|
if match:
|
|
_reply(match[1], match[2], match[3])
|
|
else:
|
|
sys.stdout.write('NONE\n')
|
|
|
|
else: # getnext
|
|
tup = _to_tuple(raw_oid)
|
|
nxt = next((e for e in tree if e[0] > tup), None)
|
|
if nxt:
|
|
_reply(nxt[1], nxt[2], nxt[3])
|
|
else:
|
|
sys.stdout.write('NONE\n')
|
|
|
|
sys.stdout.flush()
|
|
|
|
except (EOFError, BrokenPipeError, KeyboardInterrupt):
|
|
break
|
|
except Exception as exc:
|
|
sys.stderr.write(f"pass_persist error: {exc}\n")
|
|
sys.stderr.flush()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|