/g" /etc/apache2/sites-enabled/000-default.conf
+ sudo locale-gen en_US.UTF-8
+ sudo update-locale
+ - name: Prepare environment
+ run: |
+ sudo addgroup --system --quiet zabbix
+ sudo adduser --quiet --system --disabled-login --ingroup zabbix --home /var/lib/zabbix --no-create-home zabbix
+ sudo mkdir -p /var/run/postgresql/14-main.pg_stat_tmp
+ sudo touch /var/run/postgresql/14-main.pg_stat_tmp/global.tmp
+ sudo chmod 0777 /var/run/postgresql/14-main.pg_stat_tmp/global.tmp
+ (sudo -u postgres /usr/lib/postgresql/14/bin/postgres -D /var/lib/postgresql/14/main -c config_file=/etc/postgresql/14/main/postgresql.conf)&
+ sleep 5
+ cd /tmp/zabbix-branch/database/postgresql
+ sudo -u postgres createuser zabbix
+ sudo -u postgres createdb -O zabbix -E Unicode -T template0 zabbix
+ cat schema.sql | sudo -u zabbix psql zabbix
+ cat images.sql | sudo -u zabbix psql zabbix
+ cat data.sql | sudo -u zabbix psql zabbix
+ - name: Start Apache & Nginx
+ run: |
+ sudo apache2ctl start
+ sudo nginx -g "daemon on; master_process on;"
+ - name: Install python3
+ run: |
+ sudo apt-get install -y python3 python3-pip python-is-python3
+ pip install -r ./requirements.txt
+ - name: Run tests
+ continue-on-error: true
+ run: |
+ sleep 5
+ python ./.github/scripts/$TEST_FILE 2>/tmp/depricated.log >/dev/null
+ - name: Send report
+ env:
+ TBOT_TOKEN: ${{ secrets.TBOT_TOKEN }}
+ TBOT_CHAT: ${{ vars.TBOT_CHAT }}
+ SUBJECT: Zabbix API depricated tests FAIL
+ run: |
+ tail -n1 /tmp/depricated.log | grep "OK" 1>/dev/null || tail /tmp/depricated.log | python ./.github/scripts/telegram_msg.py | exit 1
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 2a26376..72e1b88 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -68,3 +68,15 @@ jobs:
tag: "v${{ env.LIBRARY_VERSION }}"
bodyFile: RELEASE_NOTES.md
artifacts: dist/*
+ - name: Send notification
+ run: |
+ python ./.github/scripts/release_notification.py
+ working-directory: ./
+ env:
+ MAIL_SERVER: ${{ secrets.MAIL_SERVER }}
+ MAIL_PORT: ${{ secrets.MAIL_PORT }}
+ MAIL_USER: ${{ secrets.MAIL_USER }}
+ MAIL_PASS: ${{ secrets.MAIL_PASS }}
+ RELEASE_RECIPIENT_LIST: ${{ secrets.RELEASE_RECIPIENT_LIST }}
+ LIBRARY_VERSION: ${{ env.LIBRARY_VERSION }}
+ REPOSITORY: ${{ github.repository }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a068122..6fa06b8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,24 @@
+## [2.0.2](https://github.com/zabbix/python-zabbix-utils/compare/v2.0.1...v2.0.2) (2024-12-12)
+
+### Features:
+
+- added support for Zabbix 7.2
+- added support of proxy groups for Sender and AsyncSender
+
+### Changes:
+
+- discontinued support for HTTP authentication for Zabbix 7.2 and newer
+- discontinued support for Zabbix 6.4
+- added examples of deleting items
+- added examples of how to clear item history
+- added examples of how to pass get request parameters
+
+### Bug fixes:
+
+- fixed issue [#21](https://github.com/zabbix/python-zabbix-utils/issues/21) with non-obvious format of ID array passing
+- fixed issue [#26](https://github.com/zabbix/python-zabbix-utils/issues/26) with Sender and AsyncSender working with proxy groups
+- fixed small bugs and flaws
+
## [2.0.1](https://github.com/zabbix/python-zabbix-utils/compare/v2.0.0...v2.0.1) (2024-09-18)
### Features:
diff --git a/README.md b/README.md
index d891ad0..58a61c1 100644
--- a/README.md
+++ b/README.md
@@ -7,8 +7,8 @@
[](https://github.com/zabbix/python-zabbix-utils/actions/workflows/compatibility_50.yaml)
[](https://github.com/zabbix/python-zabbix-utils/actions/workflows/compatibility_60.yaml)
-[](https://github.com/zabbix/python-zabbix-utils/actions/workflows/compatibility_64.yaml)
[](https://github.com/zabbix/python-zabbix-utils/actions/workflows/compatibility_70.yaml)
+[](https://github.com/zabbix/python-zabbix-utils/actions/workflows/compatibility_72.yaml)
**zabbix_utils** is a Python library for working with [Zabbix API](https://www.zabbix.com/documentation/current/manual/api/reference) as well as with [Zabbix sender](https://www.zabbix.com/documentation/current/manpages/zabbix_sender) and [Zabbix get](https://www.zabbix.com/documentation/current/manpages/zabbix_get) protocols.
@@ -29,7 +29,7 @@ Supported versions:
Tested on:
-* Zabbix 5.0, 6.0, 6.4 and 7.0
+* Zabbix 5.0, 6.0, 7.0 and 7.2
* Python 3.8, 3.9, 3.10, 3.11 and 3.12
Dependencies:
diff --git a/examples/api/asynchronous/check_auth_state.py b/examples/api/asynchronous/check_auth_state.py
index 42f8c46..cf7e3df 100644
--- a/examples/api/asynchronous/check_auth_state.py
+++ b/examples/api/asynchronous/check_auth_state.py
@@ -25,7 +25,7 @@ async def main():
# Create an instance of the AsyncZabbixAPI class
api = AsyncZabbixAPI(ZABBIX_SERVER)
- # Authenticating with Zabbix API using the provided token.
+ # Authenticating with Zabbix API using the provided username and password.
await api.login(**ZABBIX_AUTH)
# Some actions when your session can be released
diff --git a/examples/api/asynchronous/clear_history.py b/examples/api/asynchronous/clear_history.py
new file mode 100644
index 0000000..c973d6e
--- /dev/null
+++ b/examples/api/asynchronous/clear_history.py
@@ -0,0 +1,46 @@
+# Copyright (C) 2001-2023 Zabbix SIA
+#
+# Zabbix SIA licenses this file to you under the MIT License.
+# See the LICENSE file in the project root for more information.
+
+import asyncio
+from zabbix_utils import AsyncZabbixAPI, APIRequestError
+
+# Zabbix server URL or IP address
+ZABBIX_SERVER = "127.0.0.1"
+
+# Zabbix server authentication credentials
+ZABBIX_AUTH = {
+ "user": "Admin", # Zabbix user name for authentication
+ "password": "zabbix" # Zabbix user password for authentication
+}
+
+# IDs of items for which the history should be cleared
+ITEM_IDS = [70060]
+
+
+async def main():
+ """
+ The main function to perform asynchronous tasks.
+ """
+
+ # Create an instance of the AsyncZabbixAPI class
+ api = AsyncZabbixAPI(ZABBIX_SERVER)
+
+ # Authenticating with Zabbix API using the provided username and password.
+ await api.login(**ZABBIX_AUTH)
+
+ # Clear history for items with specified IDs
+ try:
+ await api.history.clear(*ITEM_IDS)
+
+ # Alternative way to do the same (since v2.0.2):
+ # await api.history.clear(ITEM_IDS)
+ except APIRequestError as e:
+ print(f"An error occurred when attempting to delete items: {e}")
+ else:
+ # Logout to release the Zabbix API session
+ await api.logout()
+
+# Run the main coroutine
+asyncio.run(main())
diff --git a/examples/api/asynchronous/custom_client_session.py b/examples/api/asynchronous/custom_client_session.py
index 20a570b..e3bec44 100644
--- a/examples/api/asynchronous/custom_client_session.py
+++ b/examples/api/asynchronous/custom_client_session.py
@@ -34,7 +34,7 @@ async def main():
client_session=client_session
)
- # Authenticating with Zabbix API using the provided token.
+ # Authenticating with Zabbix API using the provided username and password.
await api.login(**ZABBIX_AUTH)
# Retrieve a list of hosts from the Zabbix server, including their host ID and name
diff --git a/examples/api/asynchronous/custom_ssl_context.py b/examples/api/asynchronous/custom_ssl_context.py
index 70cddfc..cd44eda 100644
--- a/examples/api/asynchronous/custom_ssl_context.py
+++ b/examples/api/asynchronous/custom_ssl_context.py
@@ -40,7 +40,7 @@ async def main():
client_session=client_session
)
- # Authenticating with Zabbix API using the provided token.
+ # Authenticating with Zabbix API using the provided username and password.
await api.login(**ZABBIX_AUTH)
# Retrieve a list of hosts from the Zabbix server, including their host ID and name
diff --git a/examples/api/asynchronous/delete_items.py b/examples/api/asynchronous/delete_items.py
new file mode 100644
index 0000000..ceca435
--- /dev/null
+++ b/examples/api/asynchronous/delete_items.py
@@ -0,0 +1,46 @@
+# Copyright (C) 2001-2023 Zabbix SIA
+#
+# Zabbix SIA licenses this file to you under the MIT License.
+# See the LICENSE file in the project root for more information.
+
+import asyncio
+from zabbix_utils import AsyncZabbixAPI, APIRequestError
+
+# Zabbix server URL or IP address
+ZABBIX_SERVER = "127.0.0.1"
+
+# Zabbix server authentication credentials
+ZABBIX_AUTH = {
+ "user": "Admin", # Zabbix user name for authentication
+ "password": "zabbix" # Zabbix user password for authentication
+}
+
+# Item IDs to be deleted
+ITEM_IDS = [70060]
+
+
+async def main():
+ """
+ The main function to perform asynchronous tasks.
+ """
+
+ # Create an instance of the AsyncZabbixAPI class
+ api = AsyncZabbixAPI(ZABBIX_SERVER)
+
+ # Authenticating with Zabbix API using the provided username and password.
+ await api.login(**ZABBIX_AUTH)
+
+ # Delete items with specified IDs
+ try:
+ await api.item.delete(*ITEM_IDS)
+
+ # Alternative way to do the same (since v2.0.2):
+ # await api.item.delete(ITEM_IDS)
+ except APIRequestError as e:
+ print(f"An error occurred when attempting to delete items: {e}")
+ else:
+ # Logout to release the Zabbix API session
+ await api.logout()
+
+# Run the main coroutine
+asyncio.run(main())
diff --git a/examples/api/asynchronous/disabling_validate_certs.py b/examples/api/asynchronous/disabling_validate_certs.py
index 145e56e..745f55d 100644
--- a/examples/api/asynchronous/disabling_validate_certs.py
+++ b/examples/api/asynchronous/disabling_validate_certs.py
@@ -31,7 +31,7 @@ async def main():
# Note: Ignoring SSL certificate validation may expose the connection to security risks.
api = AsyncZabbixAPI(**ZABBIX_PARAMS)
- # Authenticating with Zabbix API using the provided token.
+ # Authenticating with Zabbix API using the provided username and password.
await api.login(**ZABBIX_AUTH)
# Retrieve a list of users from the Zabbix server, including their user ID and name.
diff --git a/examples/api/asynchronous/get_request_parameters.py b/examples/api/asynchronous/get_request_parameters.py
new file mode 100644
index 0000000..ad7ee6f
--- /dev/null
+++ b/examples/api/asynchronous/get_request_parameters.py
@@ -0,0 +1,51 @@
+# Copyright (C) 2001-2023 Zabbix SIA
+#
+# Zabbix SIA licenses this file to you under the MIT License.
+# See the LICENSE file in the project root for more information.
+
+import asyncio
+from zabbix_utils import AsyncZabbixAPI
+
+# Zabbix server URL or IP address
+ZABBIX_SERVER = "127.0.0.1"
+
+# Zabbix server authentication credentials
+ZABBIX_AUTH = {
+ "user": "Admin", # Zabbix user name for authentication
+ "password": "zabbix" # Zabbix user password for authentication
+}
+
+
+async def main():
+ """
+ The main function to perform asynchronous tasks.
+ """
+
+ # Create an instance of the AsyncZabbixAPI class
+ api = AsyncZabbixAPI(ZABBIX_SERVER)
+
+ # Authenticating with Zabbix API using the provided username and password.
+ await api.login(**ZABBIX_AUTH)
+
+ # There are only three ways to pass parameters of type dictionary:
+ #
+ # 1. Specifying values directly with their keys:
+ problems = await api.problem.get(tags=[{"tag": "scope", "value": "notice", "operator": "0"}])
+ #
+ # 2. Unpacking dictionary keys and values using `**`:
+ # request_params = {"tags": [{"tag": "scope", "value": "notice", "operator": "0"}]}
+ # problems = await api.problem.get(**request_params)
+ #
+ # 3. Passing the dictionary directly as an argument (since v2.0.2):
+ # request_params = {"tags": [{"tag": "scope", "value": "notice", "operator": "0"}]}
+ # problems = await api.problem.get(request_params)
+
+ # Print the names of the retrieved users
+ for problem in problems:
+ print(problem['name'])
+
+ # Logout to release the Zabbix API session
+ await api.logout()
+
+# Run the main coroutine
+asyncio.run(main())
diff --git a/examples/api/synchronous/clear_history.py b/examples/api/synchronous/clear_history.py
new file mode 100644
index 0000000..abaa54b
--- /dev/null
+++ b/examples/api/synchronous/clear_history.py
@@ -0,0 +1,31 @@
+# Copyright (C) 2001-2023 Zabbix SIA
+#
+# Zabbix SIA licenses this file to you under the MIT License.
+# See the LICENSE file in the project root for more information.
+
+from zabbix_utils import ZabbixAPI, APIRequestError
+
+# Zabbix server details and authentication credentials
+ZABBIX_AUTH = {
+ "url": "127.0.0.1", # Zabbix server URL or IP address
+ "user": "Admin", # Zabbix user name for authentication
+ "password": "zabbix" # Zabbix user password for authentication
+}
+
+# IDs of items for which the history should be cleared
+ITEM_IDS = [70060]
+
+# Create an instance of the ZabbixAPI class with the specified authentication details
+api = ZabbixAPI(**ZABBIX_AUTH)
+
+# Clear history for items with specified IDs
+try:
+ api.history.clear(*ITEM_IDS)
+
+ # Alternative way to do the same (since v2.0.2):
+ # api.history.clear(*ITEM_IDS)
+except APIRequestError as e:
+ print(f"An error occurred when attempting to clear items' history: {e}")
+
+# Logout to release the Zabbix API session
+api.logout()
diff --git a/examples/api/synchronous/delete_items.py b/examples/api/synchronous/delete_items.py
new file mode 100644
index 0000000..3d77f6b
--- /dev/null
+++ b/examples/api/synchronous/delete_items.py
@@ -0,0 +1,31 @@
+# Copyright (C) 2001-2023 Zabbix SIA
+#
+# Zabbix SIA licenses this file to you under the MIT License.
+# See the LICENSE file in the project root for more information.
+
+from zabbix_utils import ZabbixAPI, APIRequestError
+
+# Zabbix server details and authentication credentials
+ZABBIX_AUTH = {
+ "url": "127.0.0.1", # Zabbix server URL or IP address
+ "user": "Admin", # Zabbix user name for authentication
+ "password": "zabbix" # Zabbix user password for authentication
+}
+
+# Item IDs to be deleted
+ITEM_IDS = [70060]
+
+# Create an instance of the ZabbixAPI class with the specified authentication details
+api = ZabbixAPI(**ZABBIX_AUTH)
+
+# Delete items with specified IDs
+try:
+ api.item.delete(*ITEM_IDS)
+
+ # Alternative way to do the same (since v2.0.2):
+ # api.item.delete(ITEM_IDS)
+except APIRequestError as e:
+ print(f"An error occurred when attempting to delete items: {e}")
+
+# Logout to release the Zabbix API session
+api.logout()
diff --git a/examples/api/synchronous/get_request_parameters.py b/examples/api/synchronous/get_request_parameters.py
new file mode 100644
index 0000000..ec6036c
--- /dev/null
+++ b/examples/api/synchronous/get_request_parameters.py
@@ -0,0 +1,36 @@
+# Copyright (C) 2001-2023 Zabbix SIA
+#
+# Zabbix SIA licenses this file to you under the MIT License.
+# See the LICENSE file in the project root for more information.
+
+from zabbix_utils import ZabbixAPI
+
+# Zabbix server details and authentication credentials
+ZABBIX_AUTH = {
+ "url": "127.0.0.1", # Zabbix server URL or IP address
+ "user": "Admin", # Zabbix user name for authentication
+ "password": "zabbix" # Zabbix user password for authentication
+}
+
+# Create an instance of the ZabbixAPI class with the specified authentication details
+api = ZabbixAPI(**ZABBIX_AUTH)
+
+# There are only three ways to pass parameters of type dictionary:
+#
+# 1. Specifying values directly with their keys:
+problems = api.problem.get(tags=[{"tag": "scope", "value": "notice", "operator": "0"}])
+#
+# 2. Unpacking dictionary keys and values using `**`:
+# request_params = {"tags": [{"tag": "scope", "value": "notice", "operator": "0"}]}
+# problems = api.problem.get(**request_params)
+#
+# 3. Passing the dictionary directly as an argument (since v2.0.2):
+# request_params = {"tags": [{"tag": "scope", "value": "notice", "operator": "0"}]}
+# problems = api.problem.get(request_params)
+
+# Print the names of the retrieved users
+for problem in problems:
+ print(problem['name'])
+
+# Logout to release the Zabbix API session
+api.logout()
diff --git a/tests/common.py b/tests/common.py
index cab5887..e8e66ec 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -81,6 +81,9 @@ class MockBasicAuth():
class MockSessionConn():
def __init__(self):
self._ssl = None
+ self.closed = False
+ def close(self):
+ self.closed = True
class MockSession():
def __init__(self, exception=None):
diff --git a/tests/test_zabbix_aioapi.py b/tests/test_zabbix_aioapi.py
index 2423d49..96f1417 100644
--- a/tests/test_zabbix_aioapi.py
+++ b/tests/test_zabbix_aioapi.py
@@ -289,8 +289,10 @@ async def test__prepare_request(self):
params={'user': DEFAULT_VALUES['user'], 'password': DEFAULT_VALUES['password']},
need_auth=True
)
- self.assertEqual(req.get('auth'), DEFAULT_VALUES['token'],
- "unexpected auth request parameter, must be: " + DEFAULT_VALUES['token'])
+ self.assertEqual(req.get('auth'), None,
+ "unexpected auth request parameter, must be: None")
+ self.assertEqual(headers.get('Authorization'), 'Bearer ' + DEFAULT_VALUES['token'],
+ "unexpected Authorization header, must be: Bearer " + DEFAULT_VALUES['token'])
self.zapi.client_session.del_auth()
await self.zapi.logout()
diff --git a/tests/test_zabbix_api.py b/tests/test_zabbix_api.py
index 49cfa43..63ec234 100644
--- a/tests/test_zabbix_api.py
+++ b/tests/test_zabbix_api.py
@@ -50,10 +50,13 @@ def mock_urlopen(*args, **kwargs):
ul,
urlopen=mock_urlopen):
- zapi = ZabbixAPI(
- http_user=DEFAULT_VALUES['user'],
- http_password=DEFAULT_VALUES['password']
- )
+ with self.assertRaises(APINotSupported,
+ msg="expected APINotSupported exception hasn't been raised"):
+ ZabbixAPI(
+ http_user=DEFAULT_VALUES['user'],
+ http_password=DEFAULT_VALUES['password']
+ )
+ zapi = ZabbixAPI()
with self.assertRaises(ProcessingError,
msg="expected ProcessingError exception hasn't been raised"):
zapi.hosts.get()
@@ -160,7 +163,9 @@ def test_login(self):
ZabbixAPI,
send_api_request=common.mock_send_sync_request):
- zapi = ZabbixAPI(http_user=DEFAULT_VALUES['user'], http_password=DEFAULT_VALUES['password'])
+ with self.assertRaises(APINotSupported, msg="expected APINotSupported exception hasn't been raised"):
+ ZabbixAPI(http_user=DEFAULT_VALUES['user'], http_password=DEFAULT_VALUES['password'])
+ zapi = ZabbixAPI()
with self.assertRaises(TypeError, msg="expected TypeError exception hasn't been raised"):
zapi = ZabbixAPI()
diff --git a/zabbix_utils/aioapi.py b/zabbix_utils/aioapi.py
index 97290ad..ccf411d 100644
--- a/zabbix_utils/aioapi.py
+++ b/zabbix_utils/aioapi.py
@@ -84,13 +84,17 @@ async def func(*args: Any, **kwargs: Any) -> Any:
# Support '_' suffix to avoid conflicts with python keywords
method = removesuffix(self.object, '_') + "." + removesuffix(name, '_')
+ # Support passing list of ids and params as a dict
+ params = kwargs or (
+ (args[0] if type(args[0]) in (list, dict,) else list(args)) if args else None)
+
log.debug("Executing %s method", method)
need_auth = method not in ModuleUtils.UNAUTH_METHODS
response = await self.parent.send_async_request(
method,
- args or kwargs,
+ params,
need_auth
)
return response.get('result')
@@ -133,6 +137,7 @@ def __init__(self, url: Optional[str] = None,
client_params["connector"] = aiohttp.TCPConnector(
ssl=self.validate_certs
)
+ # HTTP Auth unsupported since Zabbix 7.2
if http_user and http_password:
client_params["auth"] = aiohttp.BasicAuth(
login=http_user,
@@ -149,6 +154,10 @@ def __init__(self, url: Optional[str] = None,
self.__check_version(skip_version_check)
+ if self.version > 7.0 and http_user and http_password:
+ self.__close_session()
+ raise APINotSupported("HTTP authentication unsupported since Zabbix 7.2.")
+
def __getattr__(self, name: str) -> Callable:
"""Dynamic creation of an API object.
@@ -167,14 +176,18 @@ async def __aenter__(self) -> Callable:
async def __aexit__(self, *args) -> None:
await self.logout()
- async def __close_session(self) -> None:
+ async def __aclose_session(self) -> None:
if self.__internal_client:
await self.__internal_client.close()
async def __exception(self, exc) -> None:
- await self.__close_session()
+ await self.__aclose_session()
raise exc from exc
+ def __close_session(self) -> None:
+ if self.__internal_client:
+ self.__internal_client._connector.close()
+
def api_version(self) -> APIVersion:
"""Return object of Zabbix API version.
@@ -257,13 +270,13 @@ async def logout(self) -> None:
if self.__use_token:
self.__session_id = None
self.__use_token = False
- await self.__close_session()
+ await self.__aclose_session()
return
log.debug("Logout from Zabbix API")
await self.user.logout()
self.__session_id = None
- await self.__close_session()
+ await self.__aclose_session()
else:
log.debug("You're not logged in Zabbix API")
@@ -305,7 +318,9 @@ def __prepare_request(self, method: str, params: Optional[dict] = None,
if need_auth:
if not self.__session_id:
raise ProcessingError("You're not logged in Zabbix API")
- if self.version < 6.4 or self.client_session._default_auth is not None:
+ if self.version < 6.4:
+ request['auth'] = self.__session_id
+ elif self.version <= 7.0 and self.client_session._default_auth is not None:
request['auth'] = self.__session_id
else:
headers["Authorization"] = f"Bearer {self.__session_id}"
@@ -401,6 +416,7 @@ def send_sync_request(self, method: str, params: Optional[dict] = None,
request_json, headers = self.__prepare_request(method, params, need_auth)
+ # HTTP Auth unsupported since Zabbix 7.2
basic_auth = self.client_session._default_auth
if basic_auth is not None:
headers["Authorization"] = "Basic " + base64.b64encode(
@@ -429,9 +445,14 @@ def send_sync_request(self, method: str, params: Optional[dict] = None,
resp = ul.urlopen(req, context=ctx)
resp_json = json.loads(resp.read().decode('utf-8'))
except URLError as err:
+ self.__close_session()
raise ProcessingError(f"Unable to connect to {self.url}:", err) from None
except ValueError as err:
+ self.__close_session()
raise ProcessingError("Unable to parse json:", err) from None
+ except Exception as err:
+ self.__close_session()
+ raise ProcessingError(err) from None
return self.__check_response(method, resp_json)
diff --git a/zabbix_utils/aiosender.py b/zabbix_utils/aiosender.py
index 7748c56..6e51d4e 100644
--- a/zabbix_utils/aiosender.py
+++ b/zabbix_utils/aiosender.py
@@ -29,12 +29,12 @@
import logging
import configparser
-from typing import Callable, Union, Optional
+from typing import Callable, Union, Optional, Tuple
from .logger import EmptyHandler
from .common import ZabbixProtocol
from .exceptions import ProcessingError
-from .types import TrapperResponse, ItemValue, Cluster
+from .types import TrapperResponse, ItemValue, Cluster, Node
log = logging.getLogger(__name__)
log.addHandler(EmptyHandler())
@@ -138,99 +138,115 @@ def __create_request(self, items: list) -> dict:
"data": [i.to_json() for i in items]
}
- async def __chunk_send(self, items: list) -> dict:
- responses = {}
-
- packet = ZabbixProtocol.create_packet(self.__create_request(items), log, self.compression)
-
- for cluster in self.clusters:
- active_node = None
+ async def __send_to_cluster(self, cluster: Cluster, packet: bytes) -> Optional[Tuple[Node, dict]]:
+ active_node = None
+ active_node_idx = 0
+ for i, node in enumerate(cluster.nodes):
- for i, node in enumerate(cluster.nodes):
+ log.debug('Trying to send data to %s', node)
- log.debug('Trying to send data to %s', node)
+ connection_params = {
+ "host": node.address,
+ "port": node.port
+ }
- connection_params = {
- "host": node.address,
- "port": node.port
- }
+ if self.source_ip:
+ connection_params['local_addr'] = (self.source_ip, 0)
- if self.source_ip:
- connection_params['local_addr'] = (self.source_ip, 0)
+ if self.ssl_context is not None:
+ connection_params['ssl'] = self.ssl_context(self.tls)
+ if not isinstance(connection_params['ssl'], ssl.SSLContext):
+ raise TypeError(
+ 'Function "ssl_context" must return "ssl.SSLContext".') from None
- if self.ssl_context is not None:
- connection_params['ssl'] = self.ssl_context(self.tls)
- if not isinstance(connection_params['ssl'], ssl.SSLContext):
- raise TypeError(
- 'Function "ssl_context" must return "ssl.SSLContext".') from None
-
- connection = asyncio.open_connection(**connection_params)
-
- try:
- reader, writer = await asyncio.wait_for(connection, timeout=self.timeout)
- except asyncio.TimeoutError:
- log.debug(
- 'The connection to %s timed out after %d seconds',
- node,
- self.timeout
- )
- except (ConnectionRefusedError, socket.gaierror) as err:
- log.debug(
- 'An error occurred while trying to connect to %s: %s',
- node,
- getattr(err, 'msg', str(err))
- )
- else:
- if i > 0:
- cluster.nodes[0], cluster.nodes[i] = cluster.nodes[i], cluster.nodes[0]
- active_node = node
- break
-
- if active_node is None:
- log.error(
- 'Couldn\'t connect to all of cluster nodes: %s',
- str(list(cluster.nodes))
- )
- raise ProcessingError(
- f"Couldn't connect to all of cluster nodes: {list(cluster.nodes)}"
- )
+ connection = asyncio.open_connection(**connection_params)
try:
- writer.write(packet)
- send_data = writer.drain()
- await asyncio.wait_for(send_data, timeout=self.timeout)
- except (asyncio.TimeoutError, socket.timeout) as err:
- log.error(
- 'The connection to %s timed out after %d seconds while trying to send',
- active_node,
+ reader, writer = await asyncio.wait_for(connection, timeout=self.timeout)
+ except asyncio.TimeoutError:
+ log.debug(
+ 'The connection to %s timed out after %d seconds',
+ node,
self.timeout
)
- writer.close()
- await writer.wait_closed()
- raise err
- except (OSError, socket.error) as err:
- log.warning(
- 'An error occurred while trying to send to %s: %s',
- active_node,
+ except (ConnectionRefusedError, socket.gaierror) as err:
+ log.debug(
+ 'An error occurred while trying to connect to %s: %s',
+ node,
getattr(err, 'msg', str(err))
)
- writer.close()
- await writer.wait_closed()
- raise err
- try:
- response = await self.__get_response(reader)
- except (ConnectionResetError, asyncio.exceptions.IncompleteReadError) as err:
- log.debug('Get value error: %s', err)
- raise err
- log.debug('Response from %s: %s', active_node, response)
+ else:
+ active_node_idx = i
+ if i > 0:
+ cluster.nodes[0], cluster.nodes[i] = cluster.nodes[i], cluster.nodes[0]
+ active_node_idx = 0
+ active_node = node
+ break
+
+ if active_node is None:
+ log.error(
+ 'Couldn\'t connect to all of cluster nodes: %s',
+ str(list(cluster.nodes))
+ )
+ raise ProcessingError(
+ f"Couldn't connect to all of cluster nodes: {list(cluster.nodes)}"
+ )
- if response and response.get('response') != 'success':
+ try:
+ writer.write(packet)
+ send_data = writer.drain()
+ await asyncio.wait_for(send_data, timeout=self.timeout)
+ except (asyncio.TimeoutError, socket.timeout) as err:
+ log.error(
+ 'The connection to %s timed out after %d seconds while trying to send',
+ active_node,
+ self.timeout
+ )
+ writer.close()
+ await writer.wait_closed()
+ raise err
+ except (OSError, socket.error) as err:
+ log.warning(
+ 'An error occurred while trying to send to %s: %s',
+ active_node,
+ getattr(err, 'msg', str(err))
+ )
+ writer.close()
+ await writer.wait_closed()
+ raise err
+ try:
+ response = await self.__get_response(reader)
+ except (ConnectionResetError, asyncio.exceptions.IncompleteReadError) as err:
+ log.debug('Get value error: %s', err)
+ raise err
+ log.debug('Response from %s: %s', active_node, response)
+
+ if response and response.get('response') != 'success':
+ if response.get('redirect'):
+ log.debug(
+ 'Packet was redirected from %s to %s. Proxy group revision: %s.',
+ active_node,
+ response['redirect']['address'],
+ response['redirect']['revision']
+ )
+ cluster.nodes[active_node_idx] = Node(*response['redirect']['address'].split(':'))
+ active_node, response = await self.__send_to_cluster(cluster, packet)
+ else:
raise ProcessingError(response) from None
- responses[active_node] = response
+ writer.close()
+ await writer.wait_closed()
- writer.close()
- await writer.wait_closed()
+ return active_node, response
+
+ async def __chunk_send(self, items: list) -> dict:
+ responses = {}
+
+ packet = ZabbixProtocol.create_packet(self.__create_request(items), log, self.compression)
+
+ for cluster in self.clusters:
+ active_node, response = await self.__send_to_cluster(cluster, packet)
+ responses[active_node] = response
return responses
diff --git a/zabbix_utils/api.py b/zabbix_utils/api.py
index 1039757..c3ae9ad 100644
--- a/zabbix_utils/api.py
+++ b/zabbix_utils/api.py
@@ -83,13 +83,17 @@ def func(*args: Any, **kwargs: Any) -> Any:
# Support '_' suffix to avoid conflicts with python keywords
method = removesuffix(self.object, '_') + "." + removesuffix(name, '_')
+ # Support passing list of ids and params as a dict
+ params = kwargs or (
+ (args[0] if type(args[0]) in (list, dict,) else list(args)) if args else None)
+
log.debug("Executing %s method", method)
need_auth = method not in ModuleUtils.UNAUTH_METHODS
return self.parent.send_api_request(
method,
- args or kwargs,
+ params,
need_auth
).get('result')
@@ -131,16 +135,21 @@ def __init__(self, url: Optional[str] = None, token: Optional[str] = None,
self.validate_certs = validate_certs
self.timeout = timeout
+ # HTTP Auth unsupported since Zabbix 7.2
if http_user and http_password:
self.__basic_auth(http_user, http_password)
if ssl_context is not None:
if not isinstance(ssl_context, ssl.SSLContext):
- raise TypeError('Function "ssl_context" must return "ssl.SSLContext".') from None
+ raise TypeError(
+ 'Parameter "ssl_context" must be an "ssl.SSLContext".') from None
self.ssl_context = ssl_context
self.__check_version(skip_version_check)
+ if self.version > 7.0 and http_user and http_password:
+ raise APINotSupported("HTTP authentication unsupported since Zabbix 7.2.")
+
if token or user or password:
self.login(token, user, password)
@@ -316,7 +325,9 @@ def send_api_request(self, method: str, params: Optional[dict] = None,
if need_auth:
if not self.__session_id:
raise ProcessingError("You're not logged in Zabbix API")
- if self.version < 6.4 or self.__basic_cred is not None:
+ if self.version < 6.4:
+ request_json['auth'] = self.__session_id
+ elif self.version <= 7.0 and self.__basic_cred is not None:
request_json['auth'] = self.__session_id
else:
headers["Authorization"] = f"Bearer {self.__session_id}"
diff --git a/zabbix_utils/sender.py b/zabbix_utils/sender.py
index ec21b6f..cb34ca8 100644
--- a/zabbix_utils/sender.py
+++ b/zabbix_utils/sender.py
@@ -27,12 +27,12 @@
import logging
import configparser
-from typing import Callable, Optional, Union
+from typing import Callable, Optional, Union, Tuple
from .logger import EmptyHandler
from .common import ZabbixProtocol
from .exceptions import ProcessingError
-from .types import TrapperResponse, ItemValue, Cluster
+from .types import TrapperResponse, ItemValue, Cluster, Node
log = logging.getLogger(__name__)
log.addHandler(EmptyHandler())
@@ -116,7 +116,7 @@ def __load_config(self, filepath: str) -> None:
config.read_string('[root]\n' + cfg.read())
self.__read_config(config['root'])
- def __get_response(self, conn: socket) -> Optional[str]:
+ def __get_response(self, conn: socket) -> Optional[dict]:
try:
result = json.loads(
ZabbixProtocol.parse_sync_packet(conn, log, ProcessingError)
@@ -135,99 +135,116 @@ def __create_request(self, items: list) -> dict:
"data": [i.to_json() for i in items]
}
- def __chunk_send(self, items: list) -> dict:
- responses = {}
-
- packet = ZabbixProtocol.create_packet(self.__create_request(items), log, self.compression)
-
- for cluster in self.clusters:
- active_node = None
+ def __send_to_cluster(self, cluster: Cluster, packet: bytes) -> Optional[Tuple[Node, dict]]:
+ active_node = None
+ active_node_idx = 0
+ for i, node in enumerate(cluster.nodes):
- for i, node in enumerate(cluster.nodes):
+ log.debug('Trying to send data to %s', node)
- log.debug('Trying to send data to %s', node)
-
- try:
- if self.use_ipv6:
- connection = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
- else:
- connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- except socket.error:
- raise ProcessingError(f"Error creating socket for {node}") from None
-
- connection.settimeout(self.timeout)
-
- if self.source_ip:
- connection.bind((self.source_ip, 0,))
-
- try:
- connection.connect((node.address, node.port))
- except (TimeoutError, socket.timeout):
- log.debug(
- 'The connection to %s timed out after %d seconds',
- node,
- self.timeout
- )
- except (ConnectionRefusedError, socket.gaierror) as err:
- log.debug(
- 'An error occurred while trying to connect to %s: %s',
- node,
- getattr(err, 'msg', str(err))
- )
+ try:
+ if self.use_ipv6:
+ connection = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
else:
- if i > 0:
- cluster.nodes[0], cluster.nodes[i] = cluster.nodes[i], cluster.nodes[0]
- active_node = node
- break
-
- if active_node is None:
- log.error(
- 'Couldn\'t connect to all of cluster nodes: %s',
- str(list(cluster.nodes))
- )
- connection.close()
- raise ProcessingError(
- f"Couldn't connect to all of cluster nodes: {list(cluster.nodes)}"
- )
+ connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ except socket.error:
+ raise ProcessingError(f"Error creating socket for {node}") from None
+
+ connection.settimeout(self.timeout)
- if self.socket_wrapper is not None:
- connection = self.socket_wrapper(connection, self.tls)
+ if self.source_ip:
+ connection.bind((self.source_ip, 0,))
try:
- connection.sendall(packet)
- except (TimeoutError, socket.timeout) as err:
- log.error(
- 'The connection to %s timed out after %d seconds while trying to send',
- active_node,
+ connection.connect((node.address, node.port))
+ except (TimeoutError, socket.timeout):
+ log.debug(
+ 'The connection to %s timed out after %d seconds',
+ node,
self.timeout
)
- connection.close()
- raise err
- except (OSError, socket.error) as err:
- log.warning(
- 'An error occurred while trying to send to %s: %s',
- active_node,
+ except (ConnectionRefusedError, socket.gaierror) as err:
+ log.debug(
+ 'An error occurred while trying to connect to %s: %s',
+ node,
getattr(err, 'msg', str(err))
)
- connection.close()
- raise err
+ else:
+ active_node_idx = i
+ if i > 0:
+ cluster.nodes[0], cluster.nodes[i] = cluster.nodes[i], cluster.nodes[0]
+ active_node_idx = 0
+ active_node = node
+ break
+
+ if active_node is None:
+ log.error(
+ 'Couldn\'t connect to all of cluster nodes: %s',
+ str(list(cluster.nodes))
+ )
+ connection.close()
+ raise ProcessingError(
+ f"Couldn't connect to all of cluster nodes: {list(cluster.nodes)}"
+ )
- try:
- response = self.__get_response(connection)
- except ConnectionResetError as err:
- log.debug('Get value error: %s', err)
- raise err
- log.debug('Response from %s: %s', active_node, response)
+ if self.socket_wrapper is not None:
+ connection = self.socket_wrapper(connection, self.tls)
- if response and response.get('response') != 'success':
+ try:
+ connection.sendall(packet)
+ except (TimeoutError, socket.timeout) as err:
+ log.error(
+ 'The connection to %s timed out after %d seconds while trying to send',
+ active_node,
+ self.timeout
+ )
+ connection.close()
+ raise err
+ except (OSError, socket.error) as err:
+ log.warning(
+ 'An error occurred while trying to send to %s: %s',
+ active_node,
+ getattr(err, 'msg', str(err))
+ )
+ connection.close()
+ raise err
+
+ try:
+ response = self.__get_response(connection)
+ except ConnectionResetError as err:
+ log.debug('Get value error: %s', err)
+ raise err
+ log.debug('Response from %s: %s', active_node, response)
+
+ if response and response.get('response') != 'success':
+ if response.get('redirect'):
+ print(response)
+ log.debug(
+ 'Packet was redirected from %s to %s. Proxy group revision: %s.',
+ active_node,
+ response['redirect']['address'],
+ response['redirect']['revision']
+ )
+ cluster.nodes[active_node_idx] = Node(*response['redirect']['address'].split(':'))
+ active_node, response = self.__send_to_cluster(cluster, packet)
+ else:
raise socket.error(response)
- responses[active_node] = response
+ try:
+ connection.close()
+ except socket.error:
+ pass
- try:
- connection.close()
- except socket.error:
- pass
+ return active_node, response
+
+ def __chunk_send(self, items: list) -> dict:
+ responses = {}
+
+ packet = ZabbixProtocol.create_packet(self.__create_request(items), log, self.compression)
+
+ for cluster in self.clusters:
+ active_node, response = self.__send_to_cluster(cluster, packet)
+ responses[active_node] = response
return responses
diff --git a/zabbix_utils/types.py b/zabbix_utils/types.py
index 2a4274f..60dc6a5 100644
--- a/zabbix_utils/types.py
+++ b/zabbix_utils/types.py
@@ -349,7 +349,7 @@ class Cluster():
"""
def __init__(self, addr: list):
- self.__nodes = self.__parse_ha_node(addr)
+ self.nodes = self.__parse_ha_node(addr)
def __parse_ha_node(self, node_list: list) -> list:
nodes = []
@@ -363,21 +363,11 @@ def __parse_ha_node(self, node_list: list) -> list:
return nodes
def __str__(self) -> str:
- return json.dumps([(node.address, node.port) for node in self.__nodes])
+ return json.dumps([(node.address, node.port) for node in self.nodes])
def __repr__(self) -> str:
return self.__str__()
- @property
- def nodes(self) -> list:
- """Returns list of Node objects.
-
- Returns:
- list: List of Node objects
- """
-
- return self.__nodes
-
class AgentResponse:
"""Contains response from Zabbix agent/agent2.
diff --git a/zabbix_utils/version.py b/zabbix_utils/version.py
index 7a3edd3..15c3e95 100644
--- a/zabbix_utils/version.py
+++ b/zabbix_utils/version.py
@@ -22,7 +22,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
-__version__ = "2.0.1"
+__version__ = "2.0.2"
__min_supported__ = 5.0
-__max_supported__ = 7.0
+__max_supported__ = 7.2
pFad - Phonifier reborn
Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy