#!/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. STRING "yodeck#" base.2. STRING player display name base.3. INTEGER online (1 = online, 0 = offline) base.4. STRING last_seen (ISO-8601 timestamp) base.5. INTEGER updating (1 = yes, 0 = no) base.6. INTEGER registered (1 = yes, 0 = no) pass_persist protocol (net-snmp): stdin: PING | get\n | getnext\n stdout: PONG | \n\n | 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()