System Settings: - Add SystemSettings.vue with Zabbix integration, SMTP/email config, SAML SSO settings - Add Setting model with key-value storage and typed values - Add settings API with caching Audit Logging: - Add AuditLog model tracking user, IP, action, entity changes - Add comprehensive audit logging to all CRUD operations: - Machines, Computers, Equipment, Network devices, VLANs, Subnets - Printers, USB devices (including checkout/checkin) - Applications, Settings, Users/Roles - Track old/new values for all field changes - Mask sensitive values (passwords, tokens) in logs User Management: - Add UsersList.vue with full user CRUD - Add Role management with granular permissions - Add 41 predefined permissions across 10 categories - Add users API with roles and permissions endpoints Reports: - Add TonerReport.vue for printer supply monitoring Dark Mode Fixes: - Fix map position section in PCForm, PrinterForm - Fix alert-warning in KnowledgeBaseDetail - All components now use CSS variables for theming CLI Commands: - Add flask seed permissions - Add flask seed settings Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
204 lines
6.4 KiB
Python
204 lines
6.4 KiB
Python
"""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')
|