Files
Yodmon/snmp/pass_persist.py
Christoph Gasser 9fc3e97546 Initial commit: Yodmon Yodeck→Zabbix bridge
- Yodeck API poller (every 10 min, paginated, 310 players)
- SQLite persistence (players + activity logs)
- SNMP v2c agent via net-snmp pass_persist
- Zabbix API auto host creation/update (6.0+)
- Flask web dashboard with live player status and log
- Docker deployment with persistent volume
- dev_server.py for local testing without Docker

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 09:31:00 +02:00

138 lines
4.1 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)
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"
# (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))),
]
# 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()