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>
This commit is contained in:
137
snmp/pass_persist.py
Normal file
137
snmp/pass_persist.py
Normal file
@@ -0,0 +1,137 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user