Open incama opened 1 year ago
It looks like Zabbix API authorization has changed between 6.2 and 6.4. User/password will no longer work, it requires an authorization token. https://www.zabbix.com/documentation/current/en/manual/api https://www.zabbix.com/documentation/devel/en/manual/api There is a section in the Zabbix UI to create API tokens under user settings, in both versions.
Yes it will still work, Using user is deprecated in version 6.4 and replaced by username. https://www.zabbix.com/documentation/current/en/manual/api/reference/user/login
Is there a way I can test this?
Found this: https://github.com/home-assistant/core/blob/dev/homeassistant/components/zabbix/init.py
Line 84:
zapi = ZabbixAPI(url=url, user=username, password=password)
Maybe we can change the definition so we can use Zabbix < 6.4 and > 6.4 based on an additional configuration option in the configuration.yaml. And to be more future proof, intergrating a third option when using the new API call method.
Just a thought :)
@incama Were you able to get login to work?
I don't see a config option for api key in the documentation for the Zabbix Agent add-on -- I'm running Zabbix 6.4.0 beta 5 if that helps.
I have the same problem. I installed Zabbix 5.0 LTS and rolled back HA core to 2022.11.4. Also installed Python 3.10
This is my config:
I'm not sure what to do with path, I tried all possible options, but nothing works.
This is my error: Logger: homeassistant.setup Source: components/zabbix/init.py:84 First occurred: 6:47:18 PM (1 occurrences) Last logged: 6:47:18 PM
Error during setup of component zabbix Traceback (most recent call last): File "/usr/local/lib/python3.10/urllib/request.py", line 1348, in do_open h.request(req.get_method(), req.selector, req.data, headers, File "/usr/local/lib/python3.10/http/client.py", line 1282, in request self._send_request(method, url, body, headers, encode_chunked) File "/usr/local/lib/python3.10/http/client.py", line 1328, in _send_request self.endheaders(body, encode_chunked=encode_chunked) File "/usr/local/lib/python3.10/http/client.py", line 1277, in endheaders self._send_output(message_body, encode_chunked=encode_chunked) File "/usr/local/lib/python3.10/http/client.py", line 1037, in _send_output self.send(msg) File "/usr/local/lib/python3.10/http/client.py", line 975, in send self.connect() File "/usr/local/lib/python3.10/http/client.py", line 941, in connect self.sock = self._create_connection( File "/usr/local/lib/python3.10/socket.py", line 824, in create_connection for res in getaddrinfo(host, port, 0, SOCK_STREAM): File "/usr/local/lib/python3.10/socket.py", line 955, in getaddrinfo for res in _socket.getaddrinfo(host, port, family, type, proto, flags): socket.gaierror: [Errno -2] Name does not resolve
During handling of the above exception, another exception occurred:
Traceback (most recent call last): File "/usr/src/homeassistant/homeassistant/setup.py", line 235, in _async_setup_component result = await task File "/usr/local/lib/python3.10/concurrent/futures/thread.py", line 58, in run result = self.fn(*self.args, *self.kwargs) File "/usr/src/homeassistant/homeassistant/components/zabbix/init.py", line 84, in setup zapi = ZabbixAPI(url=url, user=username, password=password) File "/usr/local/lib/python3.10/site-packages/pyzabbix/api.py", line 180, in init self._login(user, password) File "/usr/local/lib/python3.10/site-packages/pyzabbix/api.py", line 210, in _login self.auth = self.user.login(user=user, password=password) File "/usr/local/lib/python3.10/site-packages/pyzabbix/api.py", line 92, in fn return self.parent.do_request( File "/usr/local/lib/python3.10/site-packages/pyzabbix/api.py", line 292, in do_request res = urlopen(req) File "/usr/local/lib/python3.10/site-packages/pyzabbix/api.py", line 119, in inner res = func(req, context=ctx) File "/usr/local/lib/python3.10/site-packages/pyzabbix/api.py", line 130, in urlopen return urllib2.urlopen(args, *kwargs) File "/usr/local/lib/python3.10/urllib/request.py", line 216, in urlopen return opener.open(url, data, timeout) File "/usr/local/lib/python3.10/urllib/request.py", line 519, in open response = self._open(req, data) File "/usr/local/lib/python3.10/urllib/request.py", line 536, in _open result = self._call_chain(self.handle_open, protocol, protocol + File "/usr/local/lib/python3.10/urllib/request.py", line 496, in _call_chain result = func(args) File "/usr/local/lib/python3.10/urllib/request.py", line 1377, in http_open return self.do_open(http.client.HTTPConnection, req) File "/usr/local/lib/python3.10/urllib/request.py", line 1351, in do_open raise URLError(err) urllib.error.URLError: <urlopen error [Errno -2] Name does not resolve>
@incama Were you able to get login to work?
I don't see a config option for api key in the documentation for the Zabbix Agent add-on -- I'm running Zabbix 6.4.0 beta 5 if that helps.
No, I'm sorry. I just don't know how to get to the init.py file. I'm running on HAS OS so I don't know how to get to the file. A quick fix would be to implement an api version check before parsing the variables, maybe a developer could guide me somehow so I can create a pull request.
I have the same problem. I installed Zabbix 5.0 LTS and rolled back HA core to 2022.11.4. Also installed Python 3.10
I believe this is not related. The path variable should not be the path to the component, but the path to the Zabbix url, eg http://my-zabbixserver.com/zabbix. Your path in this example would be /zabbix.
For now, I use the HAS Api with the Zabbix http agent which works really well. Make sure to generate a lifetime bearer apikey.
Would you mind posting a link to documentation to set this up? I was not aware this was an option, even after reading through all available options.
On Mon, Mar 13, 2023 at 23:03 incama @.***> wrote:
For now, I use the HAS Api with the Zabbix http agent which works really well. Make sure to generate a lifetime bearer apikey.
— Reply to this email directly, view it on GitHub https://github.com/home-assistant/core/issues/86879#issuecomment-1467417447, or unsubscribe https://github.com/notifications/unsubscribe-auth/APSZUKGD3KBC5VHVQTWAD4LW4AC3JANCNFSM6AAAAAAUKDENTU . You are receiving this because you commented.Message ID: @.***>
--
Christopher Shaw @.*** 425.435.8440
Going to write a blogpost about it, for now you can have a look at: Github Incama This basicly is the way to go.
Do I am not a python dev, this is the first time I've written more than 1 line of Python. I found that py-zabbix 1.1.7 doesn't have support for the api_token. However another conflicting library pyzabbix 1.2.1 does, however it doesn't seem that the classes continue to work when mashed together.
I created a custom zabbix component with the below settings, and I had to manually include the logger and sender from py-zabbix
This loads without an error as modified, I don't yet know if it's sending states to zabbix, but it does create the sensors in Homeassistant if specified in sensors config.
It would be nice if this created a sensor for each trigger, rather than for all triggers on each host. Also, it doesn't support ipv6 due to the ZabbixMetric class being from py-zabbix 1.1.7 which is years old without update
zabbix.yaml
host: x.x.x.x
path: "/"
ssl: false
api_version: "6.4"
api_token: !secret zabbix
publish_states_host: homeassistant
include:
entity_globs:
- sensor.*filter_life*
manifest.json
{
"domain": "zabbix",
"name": "Zabbix",
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/zabbix",
"iot_class": "local_polling",
"loggers": ["pyzabbix"],
"requirements": ["py-zabbix==1.1.7","pyzabbix==1.2.1"],
"version": "0.0.0"
}
__init__.py
"""Support for Zabbix."""
from contextlib import suppress
import json
import logging
import math
import queue
import threading
import time
from urllib.error import HTTPError
from urllib.parse import urljoin
from pyzabbix import ZabbixAPI, ZabbixAPIException
from .sender import ZabbixMetric, ZabbixSender
import voluptuous as vol
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_PATH,
CONF_API_TOKEN,
CONF_API_VERSION,
CONF_SSL,
CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP,
EVENT_STATE_CHANGED,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import event as event_helper, state as state_helper
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entityfilter import (
INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA,
convert_include_exclude_filter,
)
from homeassistant.helpers.typing import ConfigType
_LOGGER = logging.getLogger(__name__)
CONF_PUBLISH_STATES_HOST = "publish_states_host"
DEFAULT_SSL = False
DEFAULT_PATH = "zabbix"
DEFAULT_API = "0.0"
DOMAIN = "zabbix"
TIMEOUT = 5
RETRY_DELAY = 20
QUEUE_BACKLOG_SECONDS = 30
RETRY_INTERVAL = 60 # seconds
RETRY_MESSAGE = f"%s Retrying in {RETRY_INTERVAL} seconds."
BATCH_TIMEOUT = 1
BATCH_BUFFER_SIZE = 100
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_API_VERSION, default=DEFAULT_API): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_API_TOKEN): cv.string,
vol.Optional(CONF_PUBLISH_STATES_HOST): cv.string,
}
)
},
extra=vol.ALLOW_EXTRA,
)
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Zabbix component."""
conf = config[DOMAIN]
protocol = "https" if conf[CONF_SSL] else "http"
url = urljoin(f"{protocol}://{conf[CONF_HOST]}", conf[CONF_PATH])
apiversion = conf.get(CONF_API_VERSION)
username = conf.get(CONF_USERNAME)
password = conf.get(CONF_PASSWORD)
apitoken = conf.get(CONF_API_TOKEN)
publish_states_host = conf.get(CONF_PUBLISH_STATES_HOST)
entities_filter = convert_include_exclude_filter(conf)
try:
if apiversion == "6.4":
zapi = ZabbixAPI(url)
zapi.login(api_token=apitoken)
else:
zapi = ZabbixAPI(url)
zapi.login(username, password)
_LOGGER.info("Connected to Zabbix API Version %s", zapi.api_version())
except ZabbixAPIException as login_exception:
_LOGGER.error("Unable to login to the Zabbix API: %s", login_exception)
return False
except HTTPError as http_error:
_LOGGER.error("HTTPError when connecting to Zabbix API: %s", http_error)
zapi = None
_LOGGER.error(RETRY_MESSAGE, http_error)
event_helper.call_later(
hass,
RETRY_INTERVAL,
lambda _: setup(hass, config), # type: ignore[arg-type,return-value]
)
return True
hass.data[DOMAIN] = zapi
def event_to_metrics(event, float_keys, string_keys):
"""Add an event to the outgoing Zabbix list."""
state = event.data.get("new_state")
if state is None or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE):
return
entity_id = state.entity_id
if not entities_filter(entity_id):
return
floats = {}
strings = {}
try:
_state_as_value = float(state.state)
floats[entity_id] = _state_as_value
except ValueError:
try:
_state_as_value = float(state_helper.state_as_number(state))
floats[entity_id] = _state_as_value
except ValueError:
strings[entity_id] = state.state
for key, value in state.attributes.items():
# For each value we try to cast it as float
# But if we cannot do it we store the value
# as string
attribute_id = f"{entity_id}/{key}"
try:
float_value = float(value)
except (ValueError, TypeError):
float_value = None
if float_value is None or not math.isfinite(float_value):
strings[attribute_id] = str(value)
else:
floats[attribute_id] = float_value
metrics = []
float_keys_count = len(float_keys)
float_keys.update(floats)
if len(float_keys) != float_keys_count:
floats_discovery = []
for float_key in float_keys:
floats_discovery.append({"{#KEY}": float_key})
metric = ZabbixMetric(
publish_states_host,
"homeassistant.floats_discovery",
json.dumps(floats_discovery),
)
metrics.append(metric)
for key, value in floats.items():
metric = ZabbixMetric(
publish_states_host, f"homeassistant.float[{key}]", value
)
metrics.append(metric)
string_keys.update(strings)
return metrics
if publish_states_host:
zabbix_sender = ZabbixSender(zabbix_server=conf[CONF_HOST])
instance = ZabbixThread(hass, zabbix_sender, event_to_metrics)
instance.setup(hass)
return True
class ZabbixThread(threading.Thread):
"""A threaded event handler class."""
MAX_TRIES = 3
def __init__(self, hass, zabbix_sender, event_to_metrics):
"""Initialize the listener."""
threading.Thread.__init__(self, name="Zabbix")
self.queue = queue.Queue()
self.zabbix_sender = zabbix_sender
self.event_to_metrics = event_to_metrics
self.write_errors = 0
self.shutdown = False
self.float_keys = set()
self.string_keys = set()
def setup(self, hass):
"""Set up the thread and start it."""
hass.bus.listen(EVENT_STATE_CHANGED, self._event_listener)
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self._shutdown)
self.start()
_LOGGER.debug("Started publishing state changes to Zabbix")
def _shutdown(self, event):
"""Shut down the thread."""
self.queue.put(None)
self.join()
@callback
def _event_listener(self, event):
"""Listen for new messages on the bus and queue them for Zabbix."""
item = (time.monotonic(), event)
self.queue.put(item)
def get_metrics(self):
"""Return a batch of events formatted for writing."""
queue_seconds = QUEUE_BACKLOG_SECONDS + self.MAX_TRIES * RETRY_DELAY
count = 0
metrics = []
dropped = 0
with suppress(queue.Empty):
while len(metrics) < BATCH_BUFFER_SIZE and not self.shutdown:
timeout = None if count == 0 else BATCH_TIMEOUT
item = self.queue.get(timeout=timeout)
count += 1
if item is None:
self.shutdown = True
else:
timestamp, event = item
age = time.monotonic() - timestamp
if age < queue_seconds:
event_metrics = self.event_to_metrics(
event, self.float_keys, self.string_keys
)
if event_metrics:
metrics += event_metrics
else:
dropped += 1
if dropped:
_LOGGER.warning("Catching up, dropped %d old events", dropped)
return count, metrics
def write_to_zabbix(self, metrics):
"""Write preprocessed events to zabbix, with retry."""
for retry in range(self.MAX_TRIES + 1):
try:
self.zabbix_sender.send(metrics)
if self.write_errors:
_LOGGER.error("Resumed, lost %d events", self.write_errors)
self.write_errors = 0
_LOGGER.debug("Wrote %d metrics", len(metrics))
break
except OSError as err:
if retry < self.MAX_TRIES:
time.sleep(RETRY_DELAY)
else:
if not self.write_errors:
_LOGGER.error("Write error: %s", err)
self.write_errors += len(metrics)
def run(self):
"""Process incoming events."""
while not self.shutdown:
count, metrics = self.get_metrics()
if metrics:
self.write_to_zabbix(metrics)
for _ in range(count):
self.queue.task_done()
sender.py
# -*- encoding: utf-8 -*-
#
# Copyright © 2014 Alexey Dubkov
#
# This file is part of py-zabbix.
#
# Py-zabbix is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Py-zabbix is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with py-zabbix. If not, see <http://www.gnu.org/licenses/>.
from decimal import Decimal
import inspect
import json
import logging
import socket
import struct
import re
# For python 2 and 3 compatibility
try:
from StringIO import StringIO
import ConfigParser as configparser
except ImportError:
from io import StringIO
import configparser
from .logger import NullHandler
null_handler = NullHandler()
logger = logging.getLogger(__name__)
logger.addHandler(null_handler)
class ZabbixResponse(object):
"""The :class:`ZabbixResponse` contains the parsed response from Zabbix.
"""
def __init__(self):
self._processed = 0
self._failed = 0
self._total = 0
self._time = 0
self._chunk = 0
pattern = (r'[Pp]rocessed:? (\d*);? [Ff]ailed:? (\d*);? '
r'[Tt]otal:? (\d*);? [Ss]econds spent:? (\d*\.\d*)')
self._regex = re.compile(pattern)
def __repr__(self):
"""Represent detailed ZabbixResponse view."""
result = json.dumps({'processed': self._processed,
'failed': self._failed,
'total': self._total,
'time': str(self._time),
'chunk': self._chunk})
return result
def parse(self, response):
"""Parse zabbix response."""
info = response.get('info')
res = self._regex.search(info)
self._processed += int(res.group(1))
self._failed += int(res.group(2))
self._total += int(res.group(3))
self._time += Decimal(res.group(4))
self._chunk += 1
@property
def processed(self):
return self._processed
@property
def failed(self):
return self._failed
@property
def total(self):
return self._total
@property
def time(self):
return self._time
@property
def chunk(self):
return self._chunk
class ZabbixMetric(object):
"""The :class:`ZabbixMetric` contain one metric for zabbix server.
:type host: str
:param host: Hostname as it displayed in Zabbix.
:type key: str
:param key: Key by which you will identify this metric.
:type value: str
:param value: Metric value.
:type clock: int
:param clock: Unix timestamp. Current time will used if not specified.
>>> from pyzabbix import ZabbixMetric
>>> ZabbixMetric('localhost', 'cpu[usage]', 20)
"""
def __init__(self, host, key, value, clock=None):
self.host = str(host)
self.key = str(key)
self.value = str(value)
if clock:
if isinstance(clock, (float, int)):
self.clock = int(clock)
else:
raise ValueError('Clock must be time in unixtime format')
def __repr__(self):
"""Represent detailed ZabbixMetric view."""
result = json.dumps(self.__dict__, ensure_ascii=False)
logger.debug('%s: %s', self.__class__.__name__, result)
return result
class ZabbixSender(object):
"""The :class:`ZabbixSender` send metrics to Zabbix server.
Implementation of
`zabbix protocol <https://www.zabbix.com/documentation/1.8/protocols>`_.
:type zabbix_server: str
:param zabbix_server: Zabbix server ip address. Default: `127.0.0.1`
:type zabbix_port: int
:param zabbix_port: Zabbix server port. Default: `10051`
:type use_config: str
:param use_config: Path to zabbix_agentd.conf file to load settings from.
If value is `True` then default config path will used:
/etc/zabbix/zabbix_agentd.conf
:type chunk_size: int
:param chunk_size: Number of metrics send to the server at one time
:type socket_wrapper: function
:param socket_wrapper: to provide a socket wrapper function to be used to
wrap the socket connection to zabbix.
Example:
from pyzabbix import ZabbixSender
import ssl
secure_connection_option = dict(..)
zs = ZabbixSender(
zabbix_server=zabbix_server,
zabbix_port=zabbix_port,
socket_wrapper=lambda sock:ssl.wrap_socket(sock,**secure_connection_option)
)
:type timeout: int
:param timeout: Number of seconds before call to Zabbix server times out
Default: 10
>>> from pyzabbix import ZabbixMetric, ZabbixSender
>>> metrics = []
>>> m = ZabbixMetric('localhost', 'cpu[usage]', 20)
>>> metrics.append(m)
>>> zbx = ZabbixSender('127.0.0.1')
>>> zbx.send(metrics)
"""
def __init__(self,
zabbix_server='127.0.0.1',
zabbix_port=10051,
use_config=None,
chunk_size=250,
socket_wrapper=None,
timeout=10):
self.chunk_size = chunk_size
self.timeout = timeout
self.socket_wrapper = socket_wrapper
if use_config:
self.zabbix_uri = self._load_from_config(use_config)
else:
self.zabbix_uri = [(zabbix_server, zabbix_port)]
def __repr__(self):
"""Represent detailed ZabbixSender view."""
result = json.dumps(self.__dict__, ensure_ascii=False)
logger.debug('%s: %s', self.__class__.__name__, result)
return result
def _load_from_config(self, config_file):
"""Load zabbix server IP address and port from zabbix agent config
file.
If ServerActive variable is not found in the file, it will
use the default: 127.0.0.1:10051
:type config_file: str
:param use_config: Path to zabbix_agentd.conf file to load settings
from. If value is `True` then default config path will used:
/etc/zabbix/zabbix_agentd.conf
"""
if config_file and isinstance(config_file, bool):
config_file = '/etc/zabbix/zabbix_agentd.conf'
logger.debug("Used config: %s", config_file)
# This is workaround for config wile without sections
with open(config_file, 'r') as f:
config_file_data = "[root]\n" + f.read()
params = {}
try:
# python2
args = inspect.getargspec(
configparser.RawConfigParser.__init__).args
except ValueError:
# python3
args = inspect.getfullargspec(
configparser.RawConfigParser.__init__).kwonlyargs
if 'strict' in args:
params['strict'] = False
config_file_fp = StringIO(config_file_data)
config = configparser.RawConfigParser(**params)
config.readfp(config_file_fp)
# Prefer ServerActive, then try Server and fallback to defaults
if config.has_option('root', 'ServerActive'):
zabbix_serveractives = config.get('root', 'ServerActive')
elif config.has_option('root', 'Server'):
zabbix_serveractives = config.get('root', 'Server')
else:
zabbix_serveractives = '127.0.0.1:10051'
result = []
for serverport in zabbix_serveractives.split(','):
if ':' not in serverport:
serverport = "%s:%s" % (serverport.strip(), 10051)
server, port = serverport.split(':')
serverport = (server, int(port))
result.append(serverport)
logger.debug("Loaded params: %s", result)
return result
def _receive(self, sock, count):
"""Reads socket to receive data from zabbix server.
:type socket: :class:`socket._socketobject`
:param socket: Socket to read.
:type count: int
:param count: Number of bytes to read from socket.
"""
buf = b''
while len(buf) < count:
chunk = sock.recv(count - len(buf))
if not chunk:
break
buf += chunk
return buf
def _create_messages(self, metrics):
"""Create a list of zabbix messages from a list of ZabbixMetrics.
:type metrics_array: list
:param metrics_array: List of :class:`zabbix.sender.ZabbixMetric`.
:rtype: list
:return: List of zabbix messages.
"""
messages = []
# Fill the list of messages
for m in metrics:
messages.append(str(m))
logger.debug('Messages: %s', messages)
return messages
def _create_request(self, messages):
"""Create a formatted request to zabbix from a list of messages.
:type messages: list
:param messages: List of zabbix messages
:rtype: list
:return: Formatted zabbix request
"""
msg = ','.join(messages)
request = '{{"request":"sender data","data":[{msg}]}}'.format(msg=msg)
request = request.encode("utf-8")
logger.debug('Request: %s', request)
return request
def _create_packet(self, request):
"""Create a formatted packet from a request.
:type request: str
:param request: Formatted zabbix request
:rtype: str
:return: Data packet for zabbix
"""
data_len = struct.pack('<Q', len(request))
packet = b'ZBXD\x01' + data_len + request
def ord23(x):
if not isinstance(x, int):
return ord(x)
else:
return x
logger.debug('Packet [str]: %s', packet)
logger.debug('Packet [hex]: %s',
':'.join(hex(ord23(x))[2:] for x in packet))
return packet
def _get_response(self, connection):
"""Get response from zabbix server, reads from self.socket.
:type connection: :class:`socket._socketobject`
:param connection: Socket to read.
:rtype: dict
:return: Response from zabbix server or False in case of error.
"""
response_header = self._receive(connection, 13)
logger.debug('Response header: %s', response_header)
if (not response_header.startswith(b'ZBXD\x01') or
len(response_header) != 13):
logger.debug('Zabbix return not valid response.')
result = False
else:
response_len = struct.unpack('<Q', response_header[5:])[0]
response_body = connection.recv(response_len)
result = json.loads(response_body.decode("utf-8"))
logger.debug('Data received: %s', result)
try:
connection.close()
except socket.error:
pass
return result
def _chunk_send(self, metrics):
"""Send the one chunk metrics to zabbix server.
:type metrics: list
:param metrics: List of :class:`zabbix.sender.ZabbixMetric` to send
to Zabbix
:rtype: str
:return: Response from Zabbix Server
"""
messages = self._create_messages(metrics)
request = self._create_request(messages)
packet = self._create_packet(request)
for host_addr in self.zabbix_uri:
logger.debug('Sending data to %s', host_addr)
try:
# IPv4
connection_ = socket.socket(socket.AF_INET)
except socket.error:
# IPv6
try:
connection_ = socket.socket(socket.AF_INET6)
except socket.error:
raise Exception("Error creating socket for {host_addr}".format(host_addr=host_addr))
if self.socket_wrapper:
connection = self.socket_wrapper(connection_)
else:
connection = connection_
connection.settimeout(self.timeout)
try:
# server and port must be tuple
connection.connect(host_addr)
connection.sendall(packet)
except socket.timeout:
logger.error('Sending failed: Connection to %s timed out after'
'%d seconds', host_addr, self.timeout)
connection.close()
raise socket.timeout
except socket.error as err:
# In case of error we should close connection, otherwise
# we will close it after data will be received.
logger.warning('Sending failed: %s', getattr(err, 'msg', str(err)))
connection.close()
raise err
response = self._get_response(connection)
logger.debug('%s response: %s', host_addr, response)
if response and response.get('response') != 'success':
logger.debug('Response error: %s}', response)
raise socket.error(response)
return response
def send(self, metrics):
"""Send the metrics to zabbix server.
:type metrics: list
:param metrics: List of :class:`zabbix.sender.ZabbixMetric` to send
to Zabbix
:rtype: :class:`pyzabbix.sender.ZabbixResponse`
:return: Parsed response from Zabbix Server
"""
result = ZabbixResponse()
for m in range(0, len(metrics), self.chunk_size):
result.parse(self._chunk_send(metrics[m:m + self.chunk_size]))
return result
logger.py
# -*- encoding: utf-8 -*-
#
# Copyright © 2014 Alexey Dubkov
#
# This file is part of py-zabbix.
#
# Py-zabbix is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Py-zabbix is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with py-zabbix. If not, see <http://www.gnu.org/licenses/>.
import logging
import re
class NullHandler(logging.Handler):
"""Null logger handler.
:class:`NullHandler` will be used if there are no other logger handlers.
"""
def emit(self, record):
pass
class HideSensitiveFilter(logging.Filter):
"""Filter to hide sensitive Zabbix info (password, auth) in logs"""
def __init__(self, *args, **kwargs):
super(logging.Filter, self).__init__(*args, **kwargs)
self.hide_sensitive = HideSensitiveService.hide_sensitive
def filter(self, record):
record.msg = self.hide_sensitive(record.msg)
if record.args:
newargs = [self.hide_sensitive(arg) if isinstance(arg, str)
else arg for arg in record.args]
record.args = tuple(newargs)
return 1
class HideSensitiveService(object):
"""
Service to hide sensitive Zabbix info (password, auth tokens)
Call classmethod hide_sensitive(message: str)
"""
HIDEMASK = "********"
_pattern = re.compile(
r'(?P<key>password)["\']\s*:\s*u?["\'](?P<password>.+?)["\']'
r'|'
r'\W(?P<token>[a-z0-9]{32})')
@classmethod
def hide_sensitive(cls, message):
def hide(m):
if m.group('key') == 'password':
return m.string[m.start():m.end()].replace(
m.group('password'), cls.HIDEMASK)
else:
return m.string[m.start():m.end()].replace(
m.group('token'), cls.HIDEMASK)
message = re.sub(cls._pattern, hide, message)
return message
sensor.py
"""Support for Zabbix sensors."""
from __future__ import annotations
import logging
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .. import zabbix
_LOGGER = logging.getLogger(__name__)
_CONF_TRIGGERS = "triggers"
_CONF_HOSTIDS = "hostids"
_CONF_INDIVIDUAL = "individual"
_ZABBIX_ID_LIST_SCHEMA = vol.Schema([int])
_ZABBIX_TRIGGER_SCHEMA = vol.Schema(
{
vol.Optional(_CONF_HOSTIDS, default=[]): _ZABBIX_ID_LIST_SCHEMA,
vol.Optional(_CONF_INDIVIDUAL, default=False): cv.boolean,
vol.Optional(CONF_NAME): cv.string,
}
)
# SCAN_INTERVAL = 30
#
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{vol.Required(_CONF_TRIGGERS): vol.Any(_ZABBIX_TRIGGER_SCHEMA, None)}
)
def setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Zabbix sensor platform."""
sensors: list[ZabbixTriggerCountSensor] = []
if not (zapi := hass.data[zabbix.DOMAIN]):
_LOGGER.error("Zabbix integration hasn't been loaded? zapi is None")
return
_LOGGER.info("Connected to Zabbix API Version %s", zapi.api_version())
# The following code seems overly complex. Need to think about this...
if trigger_conf := config.get(_CONF_TRIGGERS):
hostids = trigger_conf.get(_CONF_HOSTIDS)
individual = trigger_conf.get(_CONF_INDIVIDUAL)
name = trigger_conf.get(CONF_NAME)
if individual:
# Individual sensor per host
if not hostids:
# We need hostids
_LOGGER.error("If using 'individual', must specify hostids")
return
for hostid in hostids:
_LOGGER.debug("Creating Zabbix Sensor: %s", str(hostid))
sensors.append(ZabbixSingleHostTriggerCountSensor(zapi, [hostid], name))
else:
if not hostids:
# Single sensor that provides the total count of triggers.
_LOGGER.debug("Creating Zabbix Sensor")
sensors.append(ZabbixTriggerCountSensor(zapi, name))
else:
# Single sensor that sums total issues for all hosts
_LOGGER.debug("Creating Zabbix Sensor group: %s", str(hostids))
sensors.append(
ZabbixMultipleHostTriggerCountSensor(zapi, hostids, name)
)
else:
# Single sensor that provides the total count of triggers.
_LOGGER.debug("Creating Zabbix Sensor")
sensors.append(ZabbixTriggerCountSensor(zapi))
add_entities(sensors)
class ZabbixTriggerCountSensor(SensorEntity):
"""Get the active trigger count for all Zabbix monitored hosts."""
def __init__(self, zapi, name="Zabbix"):
"""Initialize Zabbix sensor."""
self._name = name
self._zapi = zapi
self._state = None
self._attributes = {}
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def native_value(self):
"""Return the state of the sensor."""
return self._state
@property
def native_unit_of_measurement(self):
"""Return the units of measurement."""
return "issues"
def _call_zabbix_api(self):
return self._zapi.trigger.get(
output="extend", only_true=1, monitored=1, filter={"value": 1}
)
def update(self) -> None:
"""Update the sensor."""
_LOGGER.debug("Updating ZabbixTriggerCountSensor: %s", str(self._name))
triggers = self._call_zabbix_api()
self._state = len(triggers)
@property
def extra_state_attributes(self):
"""Return the state attributes of the device."""
return self._attributes
class ZabbixSingleHostTriggerCountSensor(ZabbixTriggerCountSensor):
"""Get the active trigger count for a single Zabbix monitored host."""
def __init__(self, zapi, hostid, name=None):
"""Initialize Zabbix sensor."""
super().__init__(zapi, name)
self._hostid = hostid
if not name:
self._name = self._zapi.host.get(hostids=self._hostid, output="extend")[0][
"name"
]
self._attributes["Host ID"] = self._hostid
def _call_zabbix_api(self):
return self._zapi.trigger.get(
hostids=self._hostid,
output="extend",
only_true=1,
monitored=1,
filter={"value": 1},
)
class ZabbixMultipleHostTriggerCountSensor(ZabbixTriggerCountSensor):
"""Get the active trigger count for specified Zabbix monitored hosts."""
def __init__(self, zapi, hostids, name=None):
"""Initialize Zabbix sensor."""
super().__init__(zapi, name)
self._hostids = hostids
if not name:
host_names = self._zapi.host.get(hostids=self._hostids, output="extend")
self._name = " ".join(name["name"] for name in host_names)
self._attributes["Host IDs"] = self._hostids
def _call_zabbix_api(self):
return self._zapi.trigger.get(
hostids=self._hostids,
output="extend",
only_true=1,
monitored=1,
filter={"value": 1},
)
Yep. I have the same problem with HassOS from ova file
Why HassOS even using py-zabbix instead of pyzabbix?
Why HassOS even using py-zabbix instead of pyzabbix?
HASS is using pyzabbix: https://github.com/home-assistant/core/blob/d427c35c871ff3e4be4adbc803d7bfc9677b624c/homeassistant/components/zabbix/__init__.py#L12
Why HassOS even using py-zabbix instead of pyzabbix?
HASS is using pyzabbix:
This is a misunderstanding of the issue: the namespace "pyzabbix" is shared (in conflict) between the two distinct pyzabbix and py-zabbix libraries.
Is there a current solution or workaround for not being able to connect to zabbix after upgrading to zabbix 6.4 ?
The workround is to downgrade :) Haven't had the time to dive into this any deeper.
It is easier to use the HAS api then downgrading your zabbix instance.
@WebSpider In fact, I don't think, it's an option to downgrade my hole zabbix system (DB downgrade via Backup?, proxies etc.) Thank you, anyway. Things happen...
@incama What is your suggestion? What is the "HAS api"?
...I assume you refer to your post above
For now, I use the HAS Api with the Zabbix http agent which works really well. Make sure to generate a lifetime bearer apikey.
and you are using the Zabbix agent to access to Homeassistant API. Correct? Do you have some helpful hints or links to start with? That would be great.
BTW. Login with username and password (as mentioned by someone above) still IS supported: see zabbix api docu 6.4.
But the parameter changed from user
to username
, which was deprecated in 6.2. See zabbix api docu 6.2.
So I tried a quick hack in some local test client installation of py-zabbix (the one with "-"):
/usr/local/lib/python3.8/dist-packages/pyzabbix/api.py
and changed line 210 from self.user.login(user=user...
to self.user.login(username=user...
After that, my test client was able to authenticate and get some info from zabbix :-)
Maybe this would also be a quick hack as workaround for Homeassistant...
Success:
In my homeassistant, I patched /usr/local/lib/python3.10/site-packages/pyzabbix/api.py
and also changed line 210.
This made my zabbix integration authenticate again :-)
Yes, this is really a hack and I'd appreciate a good solution very much.
Thanks. I don't run zabbix myself atm, but I can write some code within HA that should help solving the login problem
...I assume you refer to your post above
For now, I use the HAS Api with the Zabbix http agent which works really well. Make sure to generate a lifetime bearer apikey.
and you are using the Zabbix agent to access to Homeassistant API. Correct? Do you have some helpful hints or links to start with? That would be great.
Yes, you can use the http agent funtion within Zabbix to connect to the home assistant api. I think it is really a preferred way to get your data as home assistant otherwise spits a lot of useless data into your zabbix instance. I have started the creation of a template set you can use within Zabbix.
This way you are certain to get just the data that you want. Secondly, you don't depend on third party python modules anymore.
I patched
/usr/local/lib/python3.10/site-packages/pyzabbix/api.py
and also changed line 210.
I confirm!
Changing line 210 from self.user.login(user=user...
to self.user.login(username=user...
works!
But I don't have pyzabbix
folder in /usr/local/lib/python3.9/dist-packages/
I searched the file in system:
find / -name pyzabbix
Found the folder like this:
/var/lib/docker/overlay2/cd673b94391f70edf489bcb04159e46bfff131e180cf089553da0a325a78ab8a/merged/usr/local/lib/python3.10/site-packages/pyzabbix
and there is a file api.py
in which I changed line 210.
configuration.yaml
:
zabbix:
host: 91.235.ip.ip
ssl: false
username: HomasTitus
password: KCbO1XZx
publish_states_host: HomasTitus
exclude:
domains:
- device_tracker
entities:
- sun.sun
- sensor.time
Found the folder like this: /var/lib/docker/overlay2/cd673b94391f70edf489bcb04159e46bfff131e180cf089553da0a325a78ab8a/merged/usr/local/lib/python3.10/site-packages/pyzabbix
Yes, that's the path inside the docker container as seen from the docker host. By docker exec -ti homeassistant bash
you get a shell within the container and you'll see "normal" linux paths. (Just FYI, the way you access and patch this file doesn't really matter in our current case.)
Sorry - cut-n-paste-error. Correct is docker exec -ti homeassistant bash
(updated my comment above, now).
the way you access and patch this file doesn't really matter in our current case
Why? This works for me! I have restarted home assistant many times. The fix works for me
Sorry - cut-n-paste-error. Correct is
docker exec -ti homeassistant bash
(updated my comment above, now).
And what to do next?
This is your homeassistant docker container, and you could access files there directly. e.g. vi /usr/local/lib/python3.10/site-packages/pyzabbix/api.py
.
waiting for fix, too.
Any chance of implementing the workaround on HAOS?
@WebSpider wrote:
Thanks. I don't run zabbix myself atm, but I can write some code within HA that should help solving the login problem
Let’s wait for this better solution.
So. If we change the user
to username
, it will work on 6.2+, however, not on lower versions.
Does anyone know how we can detect what version we're talking to, before doing authentication?
I don't believe you can. You can query the zabbix api but only with a username. I'm not a programmer, but isn't it possible to use some kind of "try -> failed -> then" stuff?
Is there someone that can test this branch ?
Does anyone know how we can detect what version we're talking to, before doing authentication?
There is a Zabbix unauthenticated API call to get the version: https://www.zabbix.com/documentation/current/en/manual/api/reference/apiinfo/version
Is there someone that can test this branch ?
Is there an easy "official way" to test it without installing some complete dev env? Or else, I could just patch (hack as above) my system with the changes from this branch. Any hints?
BTW. Thank you for doing this (even though not using Zabbix atm)
Unfortunately, you would need a dev env, or patch your running environment (if you're able to). I'm only asking this, since I dont have zabbix running to test the integration.
You can apply the following diff:
https://patch-diff.githubusercontent.com/raw/WebSpider/home-assistant/pull/3.diff
I have VSCode etc. But I think I need much more (project/HA specific) to "cleanly" do dev tests.
Yes, applying this diff was exactly what I had in mind. I'll try it.
Result after patching and rebooting:
Logger: homeassistant.setup
Source: components/zabbix/__init__.py:88
First occurred: 16:28:20 (1 occurrences)
Last logged: 16:28:20
Error during setup of component zabbix
Traceback (most recent call last):
File "/usr/src/homeassistant/homeassistant/setup.py", line 257, in _async_setup_component
result = await task
File "/usr/local/lib/python3.10/concurrent/futures/thread.py", line 58, in run
result = self.fn(*self.args, **self.kwargs)
File "/usr/src/homeassistant/homeassistant/components/zabbix/__init__.py", line 88, in setup
zapi_ver_detect = ZabbixAPI(url=url)
File "/usr/local/lib/python3.10/site-packages/pyzabbix/api.py", line 180, in __init__
self._login(user, password)
File "/usr/local/lib/python3.10/site-packages/pyzabbix/api.py", line 210, in _login
self.auth = self.user.login(user=user, password=password)
File "/usr/local/lib/python3.10/site-packages/pyzabbix/api.py", line 92, in fn
return self.parent.do_request(
File "/usr/local/lib/python3.10/site-packages/pyzabbix/api.py", line 304, in do_request
raise ZabbixAPIException(err)
pyzabbix.api.ZabbixAPIException: {'code': -32602, 'message': 'Invalid params.', 'data': 'Invalid parameter "/": unexpected parameter "user".', 'json': "{'jsonrpc': '2.0', 'method': 'user.login', 'params': {'user': 'Admin', 'password': '********'}, 'id': '1'}"}
Seems, the __init__
method in pyzabbix does an unconditional login call :-(
Missed that one. That's not great, since the py-zabbix we use is pretty much unmaintained.
Maybe we could use pyzabbix instead of py-zabbix. Their releases are recent and well maintained. https://github.com/lukecyca/pyzabbix
I got this working temporarily while I work to rewrite the code to use a maintained module. Here are the steps I followed. I strongly recommend you complete a backup before proceeding.
You will need to have the Advanced SSH and Web Terminal installed, configured, and Protection mode turned off.
SSH into your home assistance instance and run the following commands. docker cp homeassistant:/usr/local/lib/python3.10/site-packages/pyzabbix/api.py nano api.py Under def _login change the user= to username= for both login methods. then save the file and exit nano. Next, run docker cp api.py homeassistant:/usr/local/lib/python3.10/site-packages/pyzabbix/api.py to overwrite the existing api.py file. Run rm api.py to remove the artifact you created when copying the file out of docker container. Restart home assistant.
That's exactly what I am doing successfully. As this has to be done after every HA upgrade, I wrote a script which does that automatically... (see here - I don't recommend that to anyone...)
Maybe we could use pyzabbix instead of py-zabbix. Their releases are recent and well maintained. https://github.com/lukecyca/pyzabbix
For me as a non-user of this integration, that's a bit too much work, but yes, that's the way it should become.
@kornkuu
I got this working temporarily while I work to rewrite the code to use a maintained module.
Are you actually working on a "good" fix? Using pyzabbix?
@kornkuu
I got this working temporarily while I work to rewrite the code to use a maintained module.
Are you actually working on a "good" fix? Using pyzabbix?
Yes, I am working to rewrite the integration to work with that module.
Any progress here? Any help needed?
Douing a deeper dive into this
https://github.com/adubkov/py-zabbix/pull/155
In all fairnerss would be a fix, The maintainer seems to be gone offline on GH tho.
Anyone got this working on Home Assistant OS? I have tried the above instructions, but on Home Assistant OS I am unable to find the api.py flle (/usr/local/lib/python3.10/site-packages/pyzabbix/api.py)
The problem
I ran the zabbix integration for over a year now, and since a few months I get the error "Unable to login to the zabbix api". I ran Zabbix 6.4 beta 3 with succes, but since beta 4 the message appears so I guess they made changes to the api.
What version of Home Assistant Core has the issue?
2023.1.7
What was the last working version of Home Assistant Core?
No response
What type of installation are you running?
Home Assistant OS
Integration causing the issue
Zabbix
Link to integration documentation on our website
https://www.home-assistant.io/integrations/zabbix/
Diagnostics information
No response
Example YAML snippet
Anything in the logs that might be useful for us?
Additional information
No response