Zabbix 7.x rejects '#' in host names. Changed hostname format from 'yodeck#<id>' to 'yodeck-<id>' (e.g. yodeck-54239). Updated SNMP col 1 value and docs for consistency. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
138 lines
4.1 KiB
Python
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()
|