Files
shopdb-flask/tools/mock_zabbix.py
cproudlock f1b3b65532 Zabbix supply backend rebuild + data-driven model supplies
Rewrite the printer Zabbix integration (Bearer auth, host-by-IP, tag-based
supply lookup, ping) and replace the hardcoded toner table with a
modelsupplies table + CRUD + seed. Add mock Zabbix server, live test
harness, and the Playwright screenshot tooling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 08:35:02 -04:00

145 lines
5.2 KiB
Python

"""Mock Zabbix 7.0 JSON-RPC server for testing the printer supply integration.
Speaks just enough of the Zabbix API (api_jsonrpc.php) to exercise our
ZabbixService end to end over real HTTP: Bearer token auth, host.get by IP,
and item.get returning printer supply items tagged the way the real Zabbix
templates tag them (component=supplies, type=level, color=<color>).
Run standalone:
python tools/mock_zabbix.py --port 18080 --token testtoken
Then point the app at it:
ZABBIX_ENABLED=true
ZABBIX_URL=http://localhost:18080
ZABBIX_TOKEN=testtoken
It is also imported by tests/test_plugins/test_zabbix_live.py, which boots it
in a background thread.
"""
import argparse
import json
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
# one fake host, named by its IP (matches how the real shop Zabbix names hosts)
HOSTS = {
'10.20.30.40': '10501',
}
# supply level items for host 10501, with Zabbix-style tags
SUPPLY_ITEMS = {
'10501': [
{'itemid': '1', 'name': 'Black Toner Level', 'lastvalue': '4',
'status': '0', 'state': '0',
'tags': [{'tag': 'component', 'value': 'supplies'},
{'tag': 'type', 'value': 'level'},
{'tag': 'color', 'value': 'black'}]},
{'itemid': '2', 'name': 'Cyan Toner Level', 'lastvalue': '60',
'status': '0', 'state': '0',
'tags': [{'tag': 'component', 'value': 'supplies'},
{'tag': 'type', 'value': 'level'},
{'tag': 'color', 'value': 'cyan'}]},
{'itemid': '3', 'name': 'Waste Cartridge Level', 'lastvalue': '97',
'status': '0', 'state': '0',
'tags': [{'tag': 'component', 'value': 'supplies'},
{'tag': 'type', 'value': 'level'}]},
{'itemid': '4', 'name': 'Disabled Drum Level', 'lastvalue': '0',
'status': '1', 'state': '0',
'tags': [{'tag': 'component', 'value': 'supplies'},
{'tag': 'type', 'value': 'level'}]},
],
}
PING_ITEMS = {
'10501': '1',
}
def build_handler(token):
class ZabbixHandler(BaseHTTPRequestHandler):
def log_message(self, *args):
pass # quiet
def _send(self, payload, code=200):
body = json.dumps(payload).encode()
self.send_response(code)
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
# reachability probe hits the endpoint with GET
self._send({'jsonrpc': '2.0', 'error': {'code': -32600}, 'id': None})
def do_POST(self):
length = int(self.headers.get('Content-Length', 0))
request = json.loads(self.rfile.read(length) or b'{}')
method = request.get('method')
params = request.get('params', {})
reqid = request.get('id', 1)
auth = self.headers.get('Authorization', '')
if auth != f'Bearer {token}':
self._send({'jsonrpc': '2.0',
'error': {'code': -32602, 'message': 'Not authorised'},
'id': reqid})
return
result = self._dispatch(method, params)
self._send({'jsonrpc': '2.0', 'result': result, 'id': reqid})
def _dispatch(self, method, params):
if method in ('apiinfo.version',):
return '7.0.0'
if method == 'hostgroup.get':
return [{'groupid': '1'}]
if method == 'host.get':
ips = (params.get('filter') or {}).get('host', [])
out = []
for ip in ips:
if ip in HOSTS:
out.append({'hostid': HOSTS[ip], 'host': ip, 'name': ip})
return out
if method == 'item.get':
hostids = params.get('hostids')
hostid = hostids[0] if isinstance(hostids, list) else hostids
search = params.get('search') or {}
if 'icmpping' in (search.get('key_') or ''):
value = PING_ITEMS.get(hostid)
return [{'lastvalue': value}] if value is not None else []
return SUPPLY_ITEMS.get(hostid, [])
return []
return ZabbixHandler
def serve(port, token):
server = ThreadingHTTPServer(('127.0.0.1', port), build_handler(token))
return server
def serve_in_thread(port, token):
"""Start the mock in a daemon thread. Returns the server (call shutdown())."""
server = serve(port, token)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
return server
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Mock Zabbix JSON-RPC server')
parser.add_argument('--port', type=int, default=18080)
parser.add_argument('--token', default='testtoken')
args = parser.parse_args()
httpd = serve(args.port, args.token)
print(f"Mock Zabbix on http://127.0.0.1:{args.port}/api_jsonrpc.php "
f"(token: {args.token})")
print(f"Known host: {list(HOSTS)[0]} -> supplies + icmpping")
try:
httpd.serve_forever()
except KeyboardInterrupt:
httpd.shutdown()