"""Zabbix service for real-time printer supply lookups.""" import logging from typing import Dict, List, Optional import requests from flask import current_app from shopdb.extensions import cache logger = logging.getLogger(__name__) class ZabbixService: """ Zabbix API service for real-time printer supply lookups. Queries Zabbix by IP address to get current supply levels. Use getsuppliesbyip_cached() for cached lookups or getsuppliesbyip() for live data. Configuration: ZABBIX_ENABLED: Set to True to enable Zabbix integration (default: False) ZABBIX_URL: Zabbix API URL (e.g., http://zabbix.example.com:8080) ZABBIX_TOKEN: Zabbix API authentication token """ CACHE_TTL = 600 # 10 minutes REACHABLE_CHECK_TTL = 60 # Check reachability every 60 seconds def __init__(self): self._url = None self._token = None self._enabled = None @property def isenabled(self) -> bool: """Check if Zabbix integration is enabled.""" # Check database setting first, fall back to env var from shopdb.core.models import Setting db_enabled = Setting.get('zabbix_enabled') if db_enabled is not None: return bool(db_enabled) # Fall back to env var for backwards compatibility return current_app.config.get('ZABBIX_ENABLED', False) @property def isconfigured(self) -> bool: """Check if Zabbix is enabled and configured.""" if not self.isenabled: return False # Check database settings first, fall back to env vars from shopdb.core.models import Setting self._url = Setting.get('zabbix_url') or current_app.config.get('ZABBIX_URL') self._token = Setting.get('zabbix_token') or current_app.config.get('ZABBIX_TOKEN') return bool(self._url and self._token) @property def isreachable(self) -> bool: """Check if Zabbix is reachable (cached for 60 seconds).""" if not self.isenabled or not self.isconfigured: return False cache_key = 'zabbix_reachable' cached = cache.get(cache_key) if cached is not None: return cached # Quick connectivity check with 500ms timeout try: response = requests.get( f"{self._url}/api_jsonrpc.php", timeout=0.5 ) reachable = response.status_code in (200, 401, 403, 405) except requests.RequestException: reachable = False cache.set(cache_key, reachable, timeout=self.REACHABLE_CHECK_TTL) logger.debug(f"Zabbix reachability check: {reachable}") return reachable def _apicall(self, method: str, params: Dict) -> Optional[Dict]: """Make a Zabbix API call.""" if not self.isconfigured: return None payload = { 'jsonrpc': '2.0', 'method': method, 'params': params, 'auth': self._token, 'id': 1 } try: response = requests.post( f"{self._url}/api_jsonrpc.php", json=payload, headers={'Content-Type': 'application/json'}, timeout=0.5 # 500ms timeout - fail fast if Zabbix is slow/unreachable ) response.raise_for_status() data = response.json() if 'error' in data: logger.error(f"Zabbix API error: {data['error']}") return None return data.get('result') except requests.RequestException as e: logger.error(f"Zabbix API request failed: {e}") return None def gethostbyip(self, ip: str) -> Optional[Dict]: """Find a Zabbix host by IP address.""" result = self._apicall('host.get', { 'output': ['hostid', 'host', 'name'], 'filter': {'ip': ip}, 'selectInterfaces': ['ip'] }) if result: return result[0] if result else None return None def getsuppliesbyip(self, ip: str) -> Optional[List[Dict]]: """ Get printer supply levels by IP address. Returns list of supplies with name and level percentage. """ # Find host by IP host = self.gethostbyip(ip) if not host: logger.debug(f"No Zabbix host found for IP {ip}") return None hostid = host['hostid'] # Get supply-related items items = self._apicall('item.get', { 'output': ['itemid', 'name', 'lastvalue', 'key_'], 'hostids': hostid, 'search': { 'key_': 'supply' # Common key pattern for printer supplies }, 'searchWildcardsEnabled': True }) if not items: # Try alternate patterns items = self._apicall('item.get', { 'output': ['itemid', 'name', 'lastvalue', 'key_'], 'hostids': hostid, 'search': { 'name': 'toner' }, 'searchWildcardsEnabled': True }) if not items: return [] supplies = [] for item in items: try: level = int(float(item.get('lastvalue', 0))) except (ValueError, TypeError): level = 0 supplies.append({ 'name': item.get('name', 'Unknown'), 'level': level, 'itemid': item.get('itemid'), 'key': item.get('key_'), }) return supplies def gethostid(self, ip: str) -> Optional[str]: """Get Zabbix host ID for an IP address.""" host = self.gethostbyip(ip) return host['hostid'] if host else None def getsuppliesbyip_cached(self, ip: str) -> Optional[List[Dict]]: """Get printer supply levels with caching (10-minute TTL).""" cache_key = f'zabbix_supplies_{ip}' result = cache.get(cache_key) if result is not None: return result result = self.getsuppliesbyip(ip) if result is not None: cache.set(cache_key, result, timeout=self.CACHE_TTL) return result def clearcache(self, ip: str = None): """Clear cached supply data for one IP or all.""" if ip: cache.delete(f'zabbix_supplies_{ip}') cache.delete('printers_low_supplies')