Files
Yodmon/app/zabbix.py
Christoph Gasser 5db8beb847 Replace # with - in Zabbix host names (Zabbix 7.x compatibility)
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>
2026-04-17 10:21:05 +02:00

197 lines
7.3 KiB
Python

"""
Zabbix API client for automatic host management.
Each Yodeck player becomes a Zabbix host with:
- hostname : yodeck-<yodeck_id>
- visible name: "QRS-" + the Yodeck player's display name (e.g. QRS-AMS-COF1)
- SNMP v2c interface pointing to this app (APP_HOST:161)
- Four SNMP items: online, last_seen, updating, registered
"""
import logging
import requests
from app.config import (
ZABBIX_URL, ZABBIX_API_TOKEN, ZABBIX_USER, ZABBIX_PASSWORD,
ZABBIX_HOST_GROUP, ZABBIX_SNMP_COMMUNITY,
APP_HOST, ENTERPRISE_OID,
)
log = logging.getLogger(__name__)
SNMP_PORT = 161
# OID column indices (must match snmp/pass_persist.py)
COL_HOSTNAME = 1
COL_NAME = 2
COL_ONLINE = 3
COL_LAST_SEEN = 4
COL_UPDATING = 5
COL_REGISTERED = 6
class ZabbixClient:
def __init__(self):
self._url = f"{ZABBIX_URL.rstrip('/')}/api_jsonrpc.php"
self._auth = None
self._id = 0
def _call(self, method, params):
self._id += 1
payload = {'jsonrpc': '2.0', 'method': method, 'params': params, 'id': self._id}
headers = {}
if ZABBIX_API_TOKEN:
# Zabbix 6.4+: API tokens go in the Authorization header, not the payload
headers['Authorization'] = f'Bearer {ZABBIX_API_TOKEN}'
elif self._auth:
# Zabbix < 6.4 / user+password sessions: auth goes in the JSON-RPC payload
payload['auth'] = self._auth
resp = requests.post(self._url, json=payload, headers=headers, timeout=30)
resp.raise_for_status()
body = resp.json()
if 'error' in body:
raise RuntimeError(f"Zabbix [{method}]: {body['error'].get('data', body['error'])}")
return body['result']
def login(self):
if ZABBIX_API_TOKEN:
pass # token is sent via Authorization header in every _call — no login needed
else:
self._auth = self._call('user.login', {'user': ZABBIX_USER, 'password': ZABBIX_PASSWORD})
def logout(self):
if ZABBIX_API_TOKEN:
return # API tokens have no session to invalidate
try:
self._call('user.logout', [])
except Exception:
pass
self._auth = None
# ------------------------------------------------------------------ groups
def ensure_hostgroup(self, name):
existing = self._call('hostgroup.get', {'filter': {'name': name}, 'output': ['groupid']})
if existing:
return existing[0]['groupid']
try:
return self._call('hostgroup.create', {'name': name})['groupids'][0]
except RuntimeError as exc:
if 'permission' in str(exc).lower():
raise RuntimeError(
f"Host group '{name}' not found or not visible to the API token user. "
f"Fix in Zabbix: Administration -> User groups -> [your group] -> "
f"Permissions -> add '{name}' with Read-write access. "
f"Alternatively assign Super admin role to the API token user."
) from exc
raise
# ------------------------------------------------------------------ hosts
def get_hosts_in_group(self, groupid):
return self._call('host.get', {
'groupids': groupid,
'output': ['hostid', 'host', 'name'],
})
def create_host(self, hostname, visible_name, groupid):
result = self._call('host.create', {
'host': hostname,
'name': visible_name,
'groups': [{'groupid': groupid}],
'interfaces': [{
'type': 2, # SNMP
'main': 1,
'useip': 1,
'ip': APP_HOST,
'dns': '',
'port': str(SNMP_PORT),
'details': {
'version': 2, # SNMPv2c
'community': ZABBIX_SNMP_COMMUNITY,
'bulk': 1,
},
}],
})
return result['hostids'][0]
def update_host_name(self, hostid, visible_name):
self._call('host.update', {'hostid': hostid, 'name': visible_name})
# ------------------------------------------------------------------ items
def _get_interface_id(self, hostid):
ifaces = self._call('hostinterface.get', {
'hostids': hostid, 'output': ['interfaceid'],
})
return ifaces[0]['interfaceid'] if ifaces else None
def _create_item(self, hostid, interfaceid, name, key, oid, value_type):
"""
type 20 = SNMP agent (Zabbix 6.0+)
value_type 3 = unsigned int, 4 = text
"""
try:
self._call('item.create', {
'hostid': hostid,
'interfaceid': interfaceid,
'name': name,
'key_': key,
'type': 20,
'snmp_oid': oid,
'value_type': value_type,
'delay': '1m',
'history': '90d',
})
except Exception as exc:
log.warning("Could not create item '%s' on host %s: %s", name, hostid, exc)
def add_player_items(self, hostid, yodeck_id):
ifid = self._get_interface_id(hostid)
if not ifid:
log.warning("No SNMP interface found for host %s", hostid)
return
base = f"{ENTERPRISE_OID}.1.1"
self._create_item(hostid, ifid, 'Online', 'yodeck.online', f'{base}.{COL_ONLINE}.{yodeck_id}', value_type=3)
self._create_item(hostid, ifid, 'Last Seen', 'yodeck.last_seen', f'{base}.{COL_LAST_SEEN}.{yodeck_id}', value_type=4)
self._create_item(hostid, ifid, 'Updating', 'yodeck.updating', f'{base}.{COL_UPDATING}.{yodeck_id}', value_type=3)
self._create_item(hostid, ifid, 'Registered', 'yodeck.registered', f'{base}.{COL_REGISTERED}.{yodeck_id}', value_type=3)
def sync_to_zabbix(players, add_log_fn):
"""Sync player list to Zabbix: create missing hosts, update names."""
if not ZABBIX_URL:
log.debug("ZABBIX_URL not configured — skipping Zabbix sync")
return
zbx = ZabbixClient()
try:
zbx.login()
groupid = zbx.ensure_hostgroup(ZABBIX_HOST_GROUP)
existing = {h['host']: h for h in zbx.get_hosts_in_group(groupid)}
created = updated = 0
for player in players:
yid = player['id']
hostname = f"yodeck-{yid}"
visible = f"QRS-{player['name']}"
if hostname not in existing:
hostid = zbx.create_host(hostname, visible, groupid)
zbx.add_player_items(hostid, yid)
created += 1
log.info("Created Zabbix host: %s (%s)", hostname, visible)
else:
if existing[hostname]['name'] != visible:
zbx.update_host_name(existing[hostname]['hostid'], visible)
updated += 1
msg = f"Zabbix sync complete: {created} created, {updated} updated ({len(players)} total)"
add_log_fn('zabbix_sync', msg, {'created': created, 'updated': updated, 'total': len(players)})
log.info(msg)
except Exception as exc:
msg = f"Zabbix sync failed: {exc}"
add_log_fn('error', msg)
log.error(msg)
finally:
zbx.logout()