home-assistant / core

:house_with_garden: Open source home automation that puts local control and privacy first.
https://www.home-assistant.io
Apache License 2.0
69.81k stars 28.94k forks source link

Zabbix integration unable to login. #86879

Open incama opened 1 year ago

incama commented 1 year ago

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

zabbix:
  host: xxx.xxx.xxx.xxx
  path: /
  ssl: false
  username: Admin
  password: ***
  publish_states_host: hostname-of-has
  exclude:
    domains:
      - device_tracker
    entities:
      - sun.sun
      - sensor.time

Anything in the logs that might be useful for us?

2023-01-29 07:22:17.741 ERROR (SyncWorker_5) [homeassistant.components.zabbix] Unable to login to the Zabbix API: {'code': -32602, 'message': 'Invalid params.', 'data': 'Invalid parameter "/": unexpected parameter "user".', 'json': "{'jsonrpc': '2.0', 'method': 'user.login', 'params': {'user': 'Admin', 'password': '********'}, 'id': '1'}"}
2023-01-29 07:22:17.830 ERROR (MainThread) [homeassistant.setup] Setup failed for zabbix: Integration failed to initialize.

Additional information

No response

home-assistant[bot] commented 1 year ago

zabbix documentation zabbix source

rwoelk commented 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.

incama commented 1 year ago

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?

incama commented 1 year ago

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 :)

christophermichaelshaw commented 1 year ago

@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.

Folmaland commented 1 year ago

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: image

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 commented 1 year ago

@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.

incama commented 1 year ago

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.

incama commented 1 year ago

For now, I use the HAS Api with the Zabbix http agent which works really well. Make sure to generate a lifetime bearer apikey.

christophermichaelshaw commented 1 year ago

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

incama commented 1 year ago

Going to write a blogpost about it, for now you can have a look at: Github Incama This basicly is the way to go.

ACiDGRiM commented 1 year ago

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},
        )
Dante4 commented 1 year ago

Yep. I have the same problem with HassOS from ova file

Why HassOS even using py-zabbix instead of pyzabbix?

WebSpider commented 1 year ago

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

ACiDGRiM commented 1 year ago

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

This is a misunderstanding of the issue: the namespace "pyzabbix" is shared (in conflict) between the two distinct pyzabbix and py-zabbix libraries.

dsteinkopf commented 1 year ago

Is there a current solution or workaround for not being able to connect to zabbix after upgrading to zabbix 6.4 ?

WebSpider commented 1 year ago

The workround is to downgrade :) Haven't had the time to dive into this any deeper.

incama commented 1 year ago

It is easier to use the HAS api then downgrading your zabbix instance.

dsteinkopf commented 1 year ago

@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"?

dsteinkopf commented 1 year ago

...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.

dsteinkopf commented 1 year ago

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 "-"):

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...

dsteinkopf commented 1 year ago

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.

WebSpider commented 1 year ago

Thanks. I don't run zabbix myself atm, but I can write some code within HA that should help solving the login problem

incama commented 1 year ago

...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.

Blog post. Git Link.

This way you are certain to get just the data that you want. Secondly, you don't depend on third party python modules anymore.

Bagunda commented 1 year ago

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. zabbix_api

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
dsteinkopf commented 1 year ago

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.)

dsteinkopf commented 1 year ago

Sorry - cut-n-paste-error. Correct is docker exec -ti homeassistant bash (updated my comment above, now).

Bagunda commented 1 year ago

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

Bagunda commented 1 year ago

Sorry - cut-n-paste-error. Correct is docker exec -ti homeassistant bash (updated my comment above, now).

And what to do next? image

dsteinkopf commented 1 year ago

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.

ol3k commented 1 year ago

waiting for fix, too.

Any chance of implementing the workaround on HAOS?

dsteinkopf commented 1 year ago

@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.

WebSpider commented 1 year ago

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?

incama commented 1 year ago

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?

WebSpider commented 1 year ago

Is there someone that can test this branch ?

dsteinkopf commented 1 year ago

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)

WebSpider commented 1 year ago

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

dsteinkopf commented 1 year ago

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.

dsteinkopf commented 1 year ago

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'}"}
dsteinkopf commented 1 year ago

Seems, the __init__ method in pyzabbix does an unconditional login call :-(

WebSpider commented 1 year ago

Missed that one. That's not great, since the py-zabbix we use is pretty much unmaintained.

incama commented 1 year ago

Maybe we could use pyzabbix instead of py-zabbix. Their releases are recent and well maintained. https://github.com/lukecyca/pyzabbix

kornkuu commented 1 year ago

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.

dsteinkopf commented 1 year ago

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...)

WebSpider commented 1 year ago

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.

dsteinkopf commented 1 year ago

@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 commented 1 year ago

@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.

dsteinkopf commented 1 year ago

Any progress here? Any help needed?

DomiiBunn commented 1 year ago

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.

HA-TB303 commented 11 months ago

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)