change: added configuration mover
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
# zabbix_utils
|
||||
#
|
||||
# Copyright (C) 2001-2023 Zabbix SIA
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation
|
||||
# files (the "Software"), to deal in the Software without restriction,
|
||||
# including without limitation the rights to use, copy, modify,
|
||||
# merge, publish, distribute, sublicense, and/or sell copies
|
||||
# of the Software, and to permit persons to whom the Software
|
||||
# is furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from .api import ZabbixAPI
|
||||
from .sender import Sender
|
||||
from .getter import Getter
|
||||
from .status import ZabbixStatus
|
||||
from .types import ItemValue, APIVersion
|
||||
from .exceptions import ModuleBaseException, APIRequestError, APINotSupported, ProcessingError
|
||||
|
||||
from .aiosender import AsyncSender
|
||||
from .aiogetter import AsyncGetter
|
||||
try:
|
||||
__import__('aiohttp')
|
||||
except ModuleNotFoundError:
|
||||
class AsyncZabbixAPI():
|
||||
def __init__(self, *args, **kwargs):
|
||||
raise ModuleNotFoundError("No module named 'aiohttp'")
|
||||
else:
|
||||
from .aioapi import AsyncZabbixAPI
|
||||
|
||||
__all__ = (
|
||||
'ZabbixAPI',
|
||||
'AsyncZabbixAPI',
|
||||
'APIVersion',
|
||||
'Sender',
|
||||
'AsyncSender',
|
||||
'ItemValue',
|
||||
'Getter',
|
||||
'AsyncGetter',
|
||||
'ZabbixStatus',
|
||||
'ModuleBaseException',
|
||||
'APIRequestError',
|
||||
'APINotSupported',
|
||||
'ProcessingError'
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,499 @@
|
||||
# zabbix_utils
|
||||
#
|
||||
# Copyright (C) 2001-2023 Zabbix SIA
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation
|
||||
# files (the "Software"), to deal in the Software without restriction,
|
||||
# including without limitation the rights to use, copy, modify,
|
||||
# merge, publish, distribute, sublicense, and/or sell copies
|
||||
# of the Software, and to permit persons to whom the Software
|
||||
# is furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import ssl
|
||||
import json
|
||||
import base64
|
||||
import aiohttp
|
||||
import logging
|
||||
|
||||
from uuid import uuid4
|
||||
import urllib.request as ul
|
||||
from textwrap import shorten
|
||||
from os import environ as env
|
||||
|
||||
from urllib.error import URLError
|
||||
from typing import Callable, Union, Optional, Coroutine, Any
|
||||
from aiohttp.client_exceptions import ContentTypeError
|
||||
|
||||
from .types import APIVersion
|
||||
from .common import ModuleUtils
|
||||
from .logger import EmptyHandler, SensitiveFilter
|
||||
from .exceptions import APIRequestError, APINotSupported, ProcessingError
|
||||
from .version import __version__, __min_supported__, __max_supported__
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.addHandler(EmptyHandler())
|
||||
log.addFilter(SensitiveFilter())
|
||||
|
||||
|
||||
class APIObject():
|
||||
"""Zabbix API object.
|
||||
|
||||
Args:
|
||||
name (str): Zabbix API object name.
|
||||
parent (class): Zabbix API parent of the object.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, parent: Callable):
|
||||
self.object = name
|
||||
self.parent = parent
|
||||
|
||||
def __getattr__(self, name: str) -> Callable:
|
||||
"""Dynamic creation of an API method.
|
||||
|
||||
Args:
|
||||
name (str): Zabbix API object method name.
|
||||
|
||||
Raises:
|
||||
TypeError: Raises if gets unexpected arguments.
|
||||
|
||||
Returns:
|
||||
Callable: Zabbix API method.
|
||||
"""
|
||||
|
||||
# For compatibility with Python less 3.9 versions
|
||||
def removesuffix(string: str, suffix: str) -> str:
|
||||
return str(string[:-len(suffix)]) if suffix and string.endswith(suffix) else string
|
||||
|
||||
async def func(*args: Any, **kwargs: Any) -> Any:
|
||||
if args and kwargs:
|
||||
await self.__exception(TypeError("Only args or kwargs should be used."))
|
||||
|
||||
# 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,
|
||||
params,
|
||||
need_auth
|
||||
)
|
||||
return response.get('result')
|
||||
|
||||
return func
|
||||
|
||||
|
||||
class AsyncZabbixAPI():
|
||||
"""Provide asynchronous interface for working with Zabbix API.
|
||||
|
||||
Args:
|
||||
url (str, optional): Zabbix API URL. Defaults to `http://localhost/zabbix/api_jsonrpc.php`.
|
||||
http_user (str, optional): Basic Authentication username. Defaults to `None`.
|
||||
http_password (str, optional): Basic Authentication password. Defaults to `None`.
|
||||
skip_version_check (bool, optional): Skip version compatibility check. Defaults to `False`.
|
||||
validate_certs (bool, optional): Specifying certificate validation. Defaults to `True`.
|
||||
client_session (aiohttp.ClientSession, optional): Client's session. Defaults to `None`.
|
||||
timeout (int, optional): Connection timeout to Zabbix API. Defaults to `30`.
|
||||
"""
|
||||
|
||||
__version = None
|
||||
__use_token = False
|
||||
__session_id = None
|
||||
__internal_client = None
|
||||
|
||||
def __init__(self, url: Optional[str] = None, token: Optional[str] = None,
|
||||
user: Optional[str] = None, password: Optional[str] = None,
|
||||
http_user: Optional[str] = None, http_password: Optional[str] = None,
|
||||
skip_version_check: bool = False, validate_certs: bool = True,
|
||||
client_session: Optional[aiohttp.ClientSession] = None, timeout: int = 30):
|
||||
|
||||
url = url or env.get('ZABBIX_URL') or 'http://localhost/zabbix/api_jsonrpc.php'
|
||||
|
||||
self.url = ModuleUtils.check_url(url)
|
||||
self.validate_certs = validate_certs
|
||||
self.timeout = timeout
|
||||
|
||||
self.__token = token
|
||||
self.__user = user
|
||||
self.__password = password
|
||||
|
||||
client_params: dict = {}
|
||||
|
||||
if client_session is 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,
|
||||
password=http_password
|
||||
)
|
||||
self.__internal_client = aiohttp.ClientSession(**client_params)
|
||||
self.client_session = self.__internal_client
|
||||
else:
|
||||
if http_user and http_password:
|
||||
raise AttributeError(
|
||||
"Parameters http_user/http_password shouldn't be used with client_session"
|
||||
)
|
||||
self.client_session = client_session
|
||||
|
||||
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.
|
||||
|
||||
Args:
|
||||
name (str): Zabbix API method name.
|
||||
|
||||
Returns:
|
||||
APIObject: Zabbix API object instance.
|
||||
"""
|
||||
|
||||
return APIObject(name, self)
|
||||
|
||||
async def __aenter__(self) -> Callable:
|
||||
return await self.login()
|
||||
|
||||
async def __aexit__(self, *args) -> None:
|
||||
await self.logout()
|
||||
|
||||
def __await__(self) -> Coroutine:
|
||||
return self.login().__await__()
|
||||
|
||||
async def __aclose_session(self) -> None:
|
||||
if self.__internal_client:
|
||||
await self.__internal_client.close()
|
||||
|
||||
async def __exception(self, exc) -> None:
|
||||
await self.__aclose_session()
|
||||
raise 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.
|
||||
|
||||
Returns:
|
||||
APIVersion: Object of Zabbix API version
|
||||
"""
|
||||
|
||||
if self.__version is None:
|
||||
self.__version = APIVersion(
|
||||
self.send_sync_request('apiinfo.version', {}, False).get('result')
|
||||
)
|
||||
return self.__version
|
||||
|
||||
@property
|
||||
def version(self) -> APIVersion:
|
||||
"""Return object of Zabbix API version.
|
||||
|
||||
Returns:
|
||||
APIVersion: Object of Zabbix API version.
|
||||
"""
|
||||
|
||||
return self.api_version()
|
||||
|
||||
async def login(self, token: Optional[str] = None, user: Optional[str] = None,
|
||||
password: Optional[str] = None) -> Callable:
|
||||
"""Login to Zabbix API.
|
||||
|
||||
Args:
|
||||
token (str, optional): Zabbix API token. Defaults to `None`.
|
||||
user (str, optional): Zabbix API username. Defaults to `None`.
|
||||
password (str, optional): Zabbix API user's password. Defaults to `None`.
|
||||
"""
|
||||
|
||||
token = token or self.__token or env.get('ZABBIX_TOKEN') or None
|
||||
user = user or self.__user or env.get('ZABBIX_USER') or None
|
||||
password = password or self.__password or env.get('ZABBIX_PASSWORD') or None
|
||||
|
||||
if token:
|
||||
if self.version < 5.4:
|
||||
await self.__exception(APINotSupported(
|
||||
message="Token usage",
|
||||
version=self.version
|
||||
))
|
||||
if user or password:
|
||||
await self.__exception(
|
||||
ProcessingError("Token cannot be used with username and password")
|
||||
)
|
||||
self.__use_token = True
|
||||
self.__session_id = token
|
||||
return self
|
||||
|
||||
if not user:
|
||||
await self.__exception(ProcessingError("Username is missing"))
|
||||
if not password:
|
||||
await self.__exception(ProcessingError("User password is missing"))
|
||||
|
||||
if self.version < 5.4:
|
||||
user_cred = {
|
||||
"user": user,
|
||||
"password": password
|
||||
}
|
||||
else:
|
||||
user_cred = {
|
||||
"username": user,
|
||||
"password": password
|
||||
}
|
||||
|
||||
log.debug(
|
||||
"Login to Zabbix API using username:%s password:%s", user, ModuleUtils.HIDING_MASK
|
||||
)
|
||||
self.__use_token = False
|
||||
self.__session_id = await self.user.login(**user_cred)
|
||||
|
||||
log.debug("Connected to Zabbix API version %s: %s", self.version, self.url)
|
||||
|
||||
return self
|
||||
|
||||
async def logout(self) -> None:
|
||||
"""Logout from Zabbix API."""
|
||||
|
||||
if self.__session_id:
|
||||
if self.__use_token:
|
||||
self.__session_id = None
|
||||
self.__use_token = False
|
||||
await self.__aclose_session()
|
||||
return
|
||||
|
||||
log.debug("Logout from Zabbix API")
|
||||
await self.user.logout()
|
||||
self.__session_id = None
|
||||
await self.__aclose_session()
|
||||
else:
|
||||
log.debug("You're not logged in Zabbix API")
|
||||
|
||||
async def check_auth(self) -> bool:
|
||||
"""Check authentication status in Zabbix API.
|
||||
|
||||
Returns:
|
||||
bool: User authentication status (`True`, `False`)
|
||||
"""
|
||||
|
||||
if not self.__session_id:
|
||||
log.debug("You're not logged in Zabbix API")
|
||||
return False
|
||||
|
||||
if self.__use_token:
|
||||
log.debug("Check auth session using token in Zabbix API")
|
||||
refresh_resp = await self.user.checkAuthentication(token=self.__session_id)
|
||||
else:
|
||||
log.debug("Check auth session using sessionid in Zabbix API")
|
||||
refresh_resp = await self.user.checkAuthentication(sessionid=self.__session_id)
|
||||
|
||||
return bool(refresh_resp.get('userid'))
|
||||
|
||||
def __prepare_request(self, method: str, params: Optional[dict] = None,
|
||||
need_auth=True) -> Union[dict, dict]:
|
||||
request = {
|
||||
'jsonrpc': '2.0',
|
||||
'method': method,
|
||||
'params': params or {},
|
||||
'id': str(uuid4()),
|
||||
}
|
||||
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json-rpc',
|
||||
'User-Agent': f"{__name__}/{__version__}"
|
||||
}
|
||||
|
||||
if need_auth:
|
||||
if not self.__session_id:
|
||||
raise ProcessingError("You're not logged in Zabbix API")
|
||||
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}"
|
||||
|
||||
log.debug(
|
||||
"Sending request to %s with body: %s",
|
||||
self.url,
|
||||
request
|
||||
)
|
||||
|
||||
return (request, headers)
|
||||
|
||||
def __check_response(self, method: str, response: dict) -> dict:
|
||||
if method not in ModuleUtils.FILES_METHODS:
|
||||
log.debug(
|
||||
"Received response body: %s",
|
||||
response
|
||||
)
|
||||
else:
|
||||
debug_json = response.copy()
|
||||
if debug_json.get('result'):
|
||||
debug_json['result'] = shorten(debug_json['result'], 200, placeholder='...')
|
||||
log.debug(
|
||||
"Received response body (clipped): %s",
|
||||
json.dumps(debug_json, indent=4, separators=(',', ': '))
|
||||
)
|
||||
|
||||
if 'error' in response:
|
||||
err = response['error'].copy()
|
||||
err['body'] = response.copy()
|
||||
raise APIRequestError(err)
|
||||
|
||||
return response
|
||||
|
||||
async def send_async_request(self, method: str, params: Optional[dict] = None,
|
||||
need_auth=True) -> dict:
|
||||
"""Function for sending asynchronous request to Zabbix API.
|
||||
|
||||
Args:
|
||||
method (str): Zabbix API method name.
|
||||
params (dict, optional): Params for request body. Defaults to `None`.
|
||||
need_auth (bool, optional): Authorization using flag. Defaults to `False`.
|
||||
|
||||
Raises:
|
||||
ProcessingError: Wrapping built-in exceptions during request processing.
|
||||
APIRequestError: Wrapping errors from Zabbix API.
|
||||
|
||||
Returns:
|
||||
dict: Dictionary with Zabbix API response.
|
||||
"""
|
||||
|
||||
try:
|
||||
request_json, headers = self.__prepare_request(method, params, need_auth)
|
||||
except ProcessingError as err:
|
||||
await self.__exception(err)
|
||||
|
||||
resp = await self.client_session.post(
|
||||
self.url,
|
||||
json=request_json,
|
||||
headers=headers,
|
||||
timeout=self.timeout
|
||||
)
|
||||
resp.raise_for_status()
|
||||
|
||||
try:
|
||||
resp_json = await resp.json()
|
||||
except ContentTypeError as err:
|
||||
await self.__exception(ProcessingError(f"Unable to connect to {self.url}:", err))
|
||||
except ValueError as err:
|
||||
await self.__exception(ProcessingError("Unable to parse json:", err))
|
||||
|
||||
try:
|
||||
return self.__check_response(method, resp_json)
|
||||
except APIRequestError as err:
|
||||
await self.__exception(err)
|
||||
|
||||
def send_sync_request(self, method: str, params: Optional[dict] = None,
|
||||
need_auth=True) -> dict:
|
||||
"""Function for sending synchronous request to Zabbix API.
|
||||
|
||||
Args:
|
||||
method (str): Zabbix API method name.
|
||||
params (dict, optional): Params for request body. Defaults to `None`.
|
||||
need_auth (bool, optional): Authorization using flag. Defaults to `False`.
|
||||
|
||||
Raises:
|
||||
ProcessingError: Wrapping built-in exceptions during request processing.
|
||||
APIRequestError: Wrapping errors from Zabbix API.
|
||||
|
||||
Returns:
|
||||
dict: Dictionary with Zabbix API response.
|
||||
"""
|
||||
|
||||
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(
|
||||
f"{basic_auth.login}:{basic_auth.password}".encode()
|
||||
).decode()
|
||||
|
||||
req = ul.Request(
|
||||
self.url,
|
||||
data=json.dumps(request_json).encode("utf-8"),
|
||||
headers=headers,
|
||||
method='POST'
|
||||
)
|
||||
req.timeout = self.timeout
|
||||
|
||||
# Disable SSL certificate validation if needed.
|
||||
if not self.validate_certs:
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
elif not isinstance(self.client_session._connector._ssl, bool):
|
||||
ctx = self.client_session._connector._ssl
|
||||
else:
|
||||
ctx = None
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
def __check_version(self, skip_check: bool) -> None:
|
||||
|
||||
skip_check_help = "If you're sure zabbix_utils will work properly with your current \
|
||||
Zabbix version you can skip this check by \
|
||||
specifying skip_version_check=True when create ZabbixAPI object."
|
||||
|
||||
if self.version < __min_supported__:
|
||||
if skip_check:
|
||||
log.debug(
|
||||
"Version of Zabbix API [%s] is less than the library supports. %s",
|
||||
self.version,
|
||||
"Further library use at your own risk!"
|
||||
)
|
||||
else:
|
||||
raise APINotSupported(
|
||||
f"Version of Zabbix API [{self.version}] is not supported by the library. " +
|
||||
f"The oldest supported version is {__min_supported__}.0. " + skip_check_help
|
||||
)
|
||||
|
||||
if self.version > __max_supported__:
|
||||
if skip_check:
|
||||
log.debug(
|
||||
"Version of Zabbix API [%s] is more than the library was tested on. %s",
|
||||
self.version,
|
||||
"Recommended to update the library. Further library use at your own risk!"
|
||||
)
|
||||
else:
|
||||
raise APINotSupported(
|
||||
f"Version of Zabbix API [{self.version}] was not tested with the library. " +
|
||||
f"The latest tested version is {__max_supported__}.0. " + skip_check_help
|
||||
)
|
||||
@@ -0,0 +1,140 @@
|
||||
# zabbix_utils
|
||||
#
|
||||
# Copyright (C) 2001-2023 Zabbix SIA
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation
|
||||
# files (the "Software"), to deal in the Software without restriction,
|
||||
# including without limitation the rights to use, copy, modify,
|
||||
# merge, publish, distribute, sublicense, and/or sell copies
|
||||
# of the Software, and to permit persons to whom the Software
|
||||
# is furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import ssl
|
||||
import socket
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Callable, Optional
|
||||
|
||||
from .logger import EmptyHandler
|
||||
from .types import AgentResponse
|
||||
from .common import ZabbixProtocol
|
||||
from .exceptions import ProcessingError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.addHandler(EmptyHandler())
|
||||
|
||||
|
||||
class AsyncGetter():
|
||||
"""Zabbix get asynchronous implementation.
|
||||
|
||||
Args:
|
||||
host (str, optional): Zabbix agent address. Defaults to `'127.0.0.1'`.
|
||||
|
||||
port (int, optional): Zabbix agent port. Defaults to `10050`.
|
||||
|
||||
timeout (int, optional): Connection timeout value. Defaults to `10`.
|
||||
|
||||
source_ip (str, optional): IP from which to establish connection. Defaults to `None`.
|
||||
|
||||
ssl_context (Callable, optional): Func(), returned prepared ssl.SSLContext. \
|
||||
Defaults to `None`.
|
||||
"""
|
||||
|
||||
def __init__(self, host: str = '127.0.0.1', port: int = 10050, timeout: int = 10,
|
||||
source_ip: Optional[str] = None, ssl_context: Optional[Callable] = None):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.timeout = timeout
|
||||
self.source_ip = source_ip
|
||||
|
||||
self.ssl_context = ssl_context
|
||||
if self.ssl_context:
|
||||
if not isinstance(self.ssl_context, Callable):
|
||||
raise TypeError('Value "ssl_context" should be a function.')
|
||||
|
||||
async def __get_response(self, reader: asyncio.StreamReader) -> Optional[str]:
|
||||
result = await ZabbixProtocol.parse_async_packet(reader, log, ProcessingError)
|
||||
|
||||
log.debug('Received data: %s', result)
|
||||
|
||||
return result
|
||||
|
||||
async def get(self, key: str) -> Optional[str]:
|
||||
"""Gets item value from Zabbix agent by specified key.
|
||||
|
||||
Args:
|
||||
key (str): Zabbix item key.
|
||||
|
||||
Returns:
|
||||
str: Value from Zabbix agent for specified key.
|
||||
"""
|
||||
|
||||
packet = ZabbixProtocol.create_packet(key, log)
|
||||
|
||||
connection_params = {
|
||||
"host": self.host,
|
||||
"port": self.port
|
||||
}
|
||||
|
||||
if self.source_ip:
|
||||
connection_params['local_addr'] = (self.source_ip, 0)
|
||||
|
||||
if self.ssl_context:
|
||||
connection_params['ssl'] = self.ssl_context()
|
||||
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)
|
||||
writer.write(packet)
|
||||
await writer.drain()
|
||||
except asyncio.TimeoutError as err:
|
||||
log.error(
|
||||
'The connection to %s timed out after %d seconds',
|
||||
f"{self.host}:{self.port}",
|
||||
self.timeout
|
||||
)
|
||||
raise err
|
||||
except (ConnectionRefusedError, socket.gaierror) as err:
|
||||
log.error(
|
||||
'An error occurred while trying to connect to %s: %s',
|
||||
f"{self.host}:{self.port}",
|
||||
getattr(err, 'msg', str(err))
|
||||
)
|
||||
raise err
|
||||
except (OSError, socket.error) as err:
|
||||
log.warning(
|
||||
'An error occurred while trying to send to %s: %s',
|
||||
f"{self.host}:{self.port}",
|
||||
getattr(err, 'msg', str(err))
|
||||
)
|
||||
raise err
|
||||
|
||||
try:
|
||||
response = await self.__get_response(reader)
|
||||
except (ConnectionResetError, asyncio.exceptions.IncompleteReadError) as err:
|
||||
log.debug('Get value error: %s', err)
|
||||
log.warning('Check access restrictions in Zabbix agent configuration.')
|
||||
raise err
|
||||
log.debug('Response from [%s:%s]: %s', self.host, self.port, response)
|
||||
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
|
||||
return AgentResponse(response)
|
||||
@@ -0,0 +1,315 @@
|
||||
# zabbix_utils
|
||||
#
|
||||
# Copyright (C) 2001-2023 Zabbix SIA
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation
|
||||
# files (the "Software"), to deal in the Software without restriction,
|
||||
# including without limitation the rights to use, copy, modify,
|
||||
# merge, publish, distribute, sublicense, and/or sell copies
|
||||
# of the Software, and to permit persons to whom the Software
|
||||
# is furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import ssl
|
||||
import json
|
||||
import socket
|
||||
import asyncio
|
||||
import logging
|
||||
import configparser
|
||||
|
||||
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, Node
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.addHandler(EmptyHandler())
|
||||
|
||||
|
||||
class AsyncSender():
|
||||
"""Zabbix sender asynchronous implementation.
|
||||
|
||||
Args:
|
||||
server (str, optional): Zabbix server address. Defaults to `'127.0.0.1'`.
|
||||
port (int, optional): Zabbix server port. Defaults to `10051`.
|
||||
use_config (bool, optional): Specifying configuration use. Defaults to `False`.
|
||||
timeout (int, optional): Connection timeout value. Defaults to `10`.
|
||||
use_ipv6 (bool, optional): Specifying IPv6 use instead of IPv4. Defaults to `False`.
|
||||
source_ip (str, optional): IP from which to establish connection. Defaults to `None`.
|
||||
chunk_size (int, optional): Number of packets in one chunk. Defaults to `250`.
|
||||
clusters (tuple|list, optional): List of Zabbix clusters. Defaults to `None`.
|
||||
ssl_context (Callable, optional): Func(`tls`), returned prepared ssl.SSLContext. \
|
||||
Defaults to `None`.
|
||||
compression (bool, optional): Specifying compression use. Defaults to `False`.
|
||||
config_path (str, optional): Path to Zabbix agent configuration file. Defaults to \
|
||||
`/etc/zabbix/zabbix_agentd.conf`.
|
||||
"""
|
||||
|
||||
def __init__(self, server: Optional[str] = None, port: int = 10051,
|
||||
use_config: bool = False, timeout: int = 10,
|
||||
use_ipv6: bool = False, source_ip: Optional[str] = None,
|
||||
chunk_size: int = 250, clusters: Union[tuple, list] = None,
|
||||
ssl_context: Optional[Callable] = None, compression: bool = False,
|
||||
config_path: Optional[str] = '/etc/zabbix/zabbix_agentd.conf'):
|
||||
self.timeout = timeout
|
||||
self.use_ipv6 = use_ipv6
|
||||
self.tls = {}
|
||||
|
||||
self.host = None
|
||||
self.source_ip = None
|
||||
self.chunk_size = chunk_size
|
||||
self.compression = compression
|
||||
|
||||
if ssl_context is not None:
|
||||
if not isinstance(ssl_context, Callable):
|
||||
raise TypeError('Value "ssl_context" should be a function.') from None
|
||||
self.ssl_context = ssl_context
|
||||
|
||||
if source_ip is not None:
|
||||
self.source_ip = source_ip
|
||||
|
||||
if use_config:
|
||||
self.clusters = []
|
||||
self.__load_config(config_path)
|
||||
return
|
||||
|
||||
if clusters is not None:
|
||||
if not (isinstance(clusters, tuple) or isinstance(clusters, list)):
|
||||
raise TypeError('Value "clusters" should be a tuple or a list.') from None
|
||||
|
||||
clusters = clusters.copy()
|
||||
|
||||
if server is not None:
|
||||
clusters.append([f"{server}:{port}"])
|
||||
|
||||
self.clusters = [Cluster(c) for c in clusters]
|
||||
else:
|
||||
self.clusters = [Cluster([f"{server or '127.0.0.1'}:{port}"])]
|
||||
|
||||
def __read_config(self, config: configparser.SectionProxy) -> None:
|
||||
server_row = config.get('ServerActive') or config.get('Server') or '127.0.0.1:10051'
|
||||
|
||||
for cluster in server_row.split(','):
|
||||
self.clusters.append(Cluster(cluster.strip().split(';')))
|
||||
|
||||
self.host = config.get('Hostname')
|
||||
if 'SourceIP' in config:
|
||||
self.source_ip = config.get('SourceIP')
|
||||
|
||||
for key in config:
|
||||
if key.startswith('tls'):
|
||||
self.tls[key] = config.get(key)
|
||||
|
||||
def __load_config(self, filepath: str) -> None:
|
||||
config = configparser.ConfigParser(strict=False)
|
||||
|
||||
with open(filepath, 'r', encoding='utf-8') as cfg:
|
||||
config.read_string('[root]\n' + cfg.read())
|
||||
self.__read_config(config['root'])
|
||||
|
||||
async def __get_response(self, reader: asyncio.StreamReader) -> Optional[str]:
|
||||
try:
|
||||
result = json.loads(
|
||||
await ZabbixProtocol.parse_async_packet(reader, log, ProcessingError)
|
||||
)
|
||||
except json.decoder.JSONDecodeError as err:
|
||||
log.debug('Unexpected response was received from Zabbix.')
|
||||
raise err
|
||||
|
||||
log.debug('Received data: %s', result)
|
||||
|
||||
return result
|
||||
|
||||
def __create_request(self, items: list) -> dict:
|
||||
return {
|
||||
"request": "sender data",
|
||||
"data": [i.to_json() for i in items]
|
||||
}
|
||||
|
||||
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):
|
||||
|
||||
log.debug('Trying to send data to %s', node)
|
||||
|
||||
connection_params = {
|
||||
"host": node.address,
|
||||
"port": node.port
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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:
|
||||
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)}"
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
async def send(self, items: list) -> TrapperResponse:
|
||||
"""Sends packets and receives an answer from Zabbix.
|
||||
|
||||
Args:
|
||||
items (list): List of ItemValue objects.
|
||||
|
||||
Returns:
|
||||
TrapperResponse: Response from Zabbix server/proxy.
|
||||
"""
|
||||
|
||||
# Split the list of items into chunks of size self.chunk_size.
|
||||
chunks = [items[i:i + self.chunk_size] for i in range(0, len(items), self.chunk_size)]
|
||||
|
||||
# Merge responses into a single TrapperResponse object.
|
||||
result = TrapperResponse()
|
||||
|
||||
# TrapperResponse details for each node and chunk.
|
||||
result.details = {}
|
||||
|
||||
for i, chunk in enumerate(chunks):
|
||||
|
||||
if not all(isinstance(item, ItemValue) for item in chunk):
|
||||
log.debug('Received unexpected item list. It must be a list of \
|
||||
ItemValue objects: %s', json.dumps(chunk))
|
||||
raise ProcessingError(f"Received unexpected item list. \
|
||||
It must be a list of ItemValue objects: {json.dumps(chunk)}")
|
||||
|
||||
resp_by_node = await self.__chunk_send(chunk)
|
||||
|
||||
node_step = 1
|
||||
for node, resp in resp_by_node.items():
|
||||
try:
|
||||
result.add(resp, (i + 1) * node_step)
|
||||
except ProcessingError as err:
|
||||
log.debug(err)
|
||||
raise ProcessingError(err) from None
|
||||
node_step += 1
|
||||
|
||||
if node not in result.details:
|
||||
result.details[node] = []
|
||||
result.details[node].append(TrapperResponse(i+1).add(resp))
|
||||
|
||||
return result
|
||||
|
||||
async def send_value(self, host: str, key: str,
|
||||
value: str, clock: Optional[int] = None,
|
||||
ns: Optional[int] = None) -> TrapperResponse:
|
||||
"""Sends one value and receives an answer from Zabbix.
|
||||
|
||||
Args:
|
||||
host (str): Specify host name the item belongs to (as registered in Zabbix frontend).
|
||||
key (str): Specify item key to send value to.
|
||||
value (str): Specify item value.
|
||||
clock (int, optional): Specify time in Unix timestamp format. Defaults to `None`.
|
||||
ns (int, optional): Specify time expressed in nanoseconds. Defaults to `None`.
|
||||
|
||||
Returns:
|
||||
TrapperResponse: Response from Zabbix server/proxy.
|
||||
"""
|
||||
|
||||
return await self.send([ItemValue(host or self.host or '', key, value, clock, ns)])
|
||||
@@ -0,0 +1,420 @@
|
||||
# zabbix_utils
|
||||
#
|
||||
# Copyright (C) 2001-2023 Zabbix SIA
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation
|
||||
# files (the "Software"), to deal in the Software without restriction,
|
||||
# including without limitation the rights to use, copy, modify,
|
||||
# merge, publish, distribute, sublicense, and/or sell copies
|
||||
# of the Software, and to permit persons to whom the Software
|
||||
# is furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import ssl
|
||||
import json
|
||||
import base64
|
||||
import logging
|
||||
import urllib.request as ul
|
||||
from textwrap import shorten
|
||||
|
||||
from uuid import uuid4
|
||||
from os import environ as env
|
||||
from urllib.error import URLError
|
||||
|
||||
from typing import Callable, Optional, Any
|
||||
|
||||
from .types import APIVersion
|
||||
from .common import ModuleUtils
|
||||
from .logger import EmptyHandler, SensitiveFilter
|
||||
from .exceptions import APIRequestError, APINotSupported, ProcessingError
|
||||
from .version import __version__, __min_supported__, __max_supported__
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.addHandler(EmptyHandler())
|
||||
log.addFilter(SensitiveFilter())
|
||||
|
||||
|
||||
class APIObject():
|
||||
"""Zabbix API object.
|
||||
|
||||
Args:
|
||||
name (str): Zabbix API object name.
|
||||
parent (class): Zabbix API parent of the object.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, parent: Callable):
|
||||
self.object = name
|
||||
self.parent = parent
|
||||
|
||||
def __getattr__(self, name: str) -> Callable:
|
||||
"""Dynamic creation of an API method.
|
||||
|
||||
Args:
|
||||
name (str): Zabbix API object method name.
|
||||
|
||||
Raises:
|
||||
TypeError: Raises if gets unexpected arguments.
|
||||
|
||||
Returns:
|
||||
Callable: Zabbix API method.
|
||||
"""
|
||||
|
||||
# For compatibility with Python less 3.9 versions
|
||||
def removesuffix(string: str, suffix: str) -> str:
|
||||
return str(string[:-len(suffix)]) if suffix and string.endswith(suffix) else string
|
||||
|
||||
def func(*args: Any, **kwargs: Any) -> Any:
|
||||
if args and kwargs:
|
||||
raise TypeError("Only args or kwargs should be used.")
|
||||
|
||||
# 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,
|
||||
params,
|
||||
need_auth
|
||||
).get('result')
|
||||
|
||||
return func
|
||||
|
||||
|
||||
class ZabbixAPI():
|
||||
"""Provide interface for working with Zabbix API.
|
||||
|
||||
Args:
|
||||
url (str, optional): Zabbix API URL. Defaults to `http://localhost/zabbix/api_jsonrpc.php`.
|
||||
token (str, optional): Zabbix API token. Defaults to `None`.
|
||||
user (str, optional): Zabbix API username. Defaults to `None`.
|
||||
password (str, optional): Zabbix API user's password. Defaults to `None`.
|
||||
http_user (str, optional): Basic Authentication username. Defaults to `None`.
|
||||
http_password (str, optional): Basic Authentication password. Defaults to `None`.
|
||||
skip_version_check (bool, optional): Skip version compatibility check. Defaults to `False`.
|
||||
validate_certs (bool, optional): Specifying certificate validation. Defaults to `True`.
|
||||
timeout (int, optional): Connection timeout to Zabbix API. Defaults to `30`.
|
||||
"""
|
||||
|
||||
__version = None
|
||||
__use_token = False
|
||||
__session_id = None
|
||||
__basic_cred = None
|
||||
|
||||
def __init__(self, url: Optional[str] = None, token: Optional[str] = None,
|
||||
user: Optional[str] = None, password: Optional[str] = None,
|
||||
http_user: Optional[str] = None, http_password: Optional[str] = None,
|
||||
skip_version_check: bool = False, validate_certs: bool = True,
|
||||
ssl_context: Optional[ssl.SSLContext] = None, timeout: int = 30):
|
||||
|
||||
url = url or env.get('ZABBIX_URL') or 'http://localhost/zabbix/api_jsonrpc.php'
|
||||
user = user or env.get('ZABBIX_USER') or None
|
||||
password = password or env.get('ZABBIX_PASSWORD') or None
|
||||
token = token or env.get('ZABBIX_TOKEN') or None
|
||||
|
||||
self.url = ModuleUtils.check_url(url)
|
||||
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(
|
||||
'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)
|
||||
|
||||
def __getattr__(self, name: str) -> Callable:
|
||||
"""Dynamic creation of an API object.
|
||||
|
||||
Args:
|
||||
name (str): Zabbix API method name.
|
||||
|
||||
Returns:
|
||||
APIObject: Zabbix API object instance.
|
||||
"""
|
||||
|
||||
return APIObject(name, self)
|
||||
|
||||
def __enter__(self) -> Callable:
|
||||
return self
|
||||
|
||||
def __exit__(self, *args) -> None:
|
||||
self.logout()
|
||||
|
||||
def __basic_auth(self, user: str, password: str) -> None:
|
||||
"""Enable Basic Authentication using.
|
||||
|
||||
Args:
|
||||
user (str): Basic Authentication username.
|
||||
password (str): Basic Authentication password.
|
||||
"""
|
||||
|
||||
log.debug(
|
||||
"Enable Basic Authentication with username:%s password:%s",
|
||||
user,
|
||||
ModuleUtils.HIDING_MASK
|
||||
)
|
||||
|
||||
self.__basic_cred = base64.b64encode(
|
||||
f"{user}:{password}".encode()
|
||||
).decode()
|
||||
|
||||
def api_version(self) -> APIVersion:
|
||||
"""Return object of Zabbix API version.
|
||||
|
||||
Returns:
|
||||
APIVersion: Object of Zabbix API version
|
||||
"""
|
||||
|
||||
if self.__version is None:
|
||||
self.__version = APIVersion(self.apiinfo.version())
|
||||
return self.__version
|
||||
|
||||
@property
|
||||
def version(self) -> APIVersion:
|
||||
"""Return object of Zabbix API version.
|
||||
|
||||
Returns:
|
||||
APIVersion: Object of Zabbix API version.
|
||||
"""
|
||||
|
||||
return self.api_version()
|
||||
|
||||
def login(self, token: Optional[str] = None, user: Optional[str] = None,
|
||||
password: Optional[str] = None) -> None:
|
||||
"""Login to Zabbix API.
|
||||
|
||||
Args:
|
||||
token (str, optional): Zabbix API token. Defaults to `None`.
|
||||
user (str, optional): Zabbix API username. Defaults to `None`.
|
||||
password (str, optional): Zabbix API user's password. Defaults to `None`.
|
||||
"""
|
||||
|
||||
if token:
|
||||
if self.version < 5.4:
|
||||
raise APINotSupported(
|
||||
message="Token usage",
|
||||
version=self.version
|
||||
)
|
||||
if user or password:
|
||||
raise ProcessingError(
|
||||
"Token cannot be used with username and password")
|
||||
self.__use_token = True
|
||||
self.__session_id = token
|
||||
return
|
||||
|
||||
if not user:
|
||||
raise ProcessingError("Username is missing")
|
||||
if not password:
|
||||
raise ProcessingError("User password is missing")
|
||||
|
||||
if self.version < 5.4:
|
||||
user_cred = {
|
||||
"user": user,
|
||||
"password": password
|
||||
}
|
||||
else:
|
||||
user_cred = {
|
||||
"username": user,
|
||||
"password": password
|
||||
}
|
||||
|
||||
log.debug(
|
||||
"Login to Zabbix API using username:%s password:%s", user, ModuleUtils.HIDING_MASK
|
||||
)
|
||||
self.__use_token = False
|
||||
self.__session_id = self.user.login(**user_cred)
|
||||
|
||||
log.debug("Connected to Zabbix API version %s: %s", self.version, self.url)
|
||||
|
||||
def logout(self) -> None:
|
||||
"""Logout from Zabbix API."""
|
||||
|
||||
if self.__session_id:
|
||||
if self.__use_token:
|
||||
self.__session_id = None
|
||||
self.__use_token = False
|
||||
return
|
||||
|
||||
log.debug("Logout from Zabbix API")
|
||||
self.user.logout()
|
||||
self.__session_id = None
|
||||
else:
|
||||
log.debug("You're not logged in Zabbix API")
|
||||
|
||||
def check_auth(self) -> bool:
|
||||
"""Check authentication status in Zabbix API.
|
||||
|
||||
Returns:
|
||||
bool: User authentication status (`True`, `False`)
|
||||
"""
|
||||
|
||||
if not self.__session_id:
|
||||
log.debug("You're not logged in Zabbix API")
|
||||
return False
|
||||
|
||||
if self.__use_token:
|
||||
log.debug("Check auth session using token in Zabbix API")
|
||||
refresh_resp = self.user.checkAuthentication(token=self.__session_id)
|
||||
else:
|
||||
log.debug("Check auth session using sessionid in Zabbix API")
|
||||
refresh_resp = self.user.checkAuthentication(sessionid=self.__session_id)
|
||||
|
||||
return bool(refresh_resp.get('userid'))
|
||||
|
||||
def send_api_request(self, method: str, params: Optional[dict] = None,
|
||||
need_auth=True) -> dict:
|
||||
"""Function for sending request to Zabbix API.
|
||||
|
||||
Args:
|
||||
method (str): Zabbix API method name.
|
||||
params (dict, optional): Params for request body. Defaults to `None`.
|
||||
need_auth (bool, optional): Authorization using flag. Defaults to `False`.
|
||||
|
||||
Raises:
|
||||
ProcessingError: Wrapping built-in exceptions during request processing.
|
||||
APIRequestError: Wrapping errors from Zabbix API.
|
||||
|
||||
Returns:
|
||||
dict: Dictionary with Zabbix API response.
|
||||
"""
|
||||
|
||||
request_json = {
|
||||
'jsonrpc': '2.0',
|
||||
'method': method,
|
||||
'params': params or {},
|
||||
'id': str(uuid4()),
|
||||
}
|
||||
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json-rpc',
|
||||
'User-Agent': f"{__name__}/{__version__}"
|
||||
}
|
||||
|
||||
if need_auth:
|
||||
if not self.__session_id:
|
||||
raise ProcessingError("You're not logged in Zabbix API")
|
||||
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}"
|
||||
|
||||
if self.__basic_cred is not None:
|
||||
headers["Authorization"] = f"Basic {self.__basic_cred}"
|
||||
|
||||
log.debug(
|
||||
"Sending request to %s with body: %s",
|
||||
self.url,
|
||||
request_json
|
||||
)
|
||||
|
||||
req = ul.Request(
|
||||
self.url,
|
||||
data=json.dumps(request_json).encode("utf-8"),
|
||||
headers=headers,
|
||||
method='POST'
|
||||
)
|
||||
req.timeout = self.timeout
|
||||
|
||||
# Disable SSL certificate validation if needed.
|
||||
if not self.validate_certs:
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
elif self.ssl_context is not None:
|
||||
ctx = self.ssl_context
|
||||
else:
|
||||
ctx = None
|
||||
|
||||
try:
|
||||
resp = ul.urlopen(req, context=ctx)
|
||||
resp_json = json.loads(resp.read().decode('utf-8'))
|
||||
except URLError as err:
|
||||
raise ProcessingError(f"Unable to connect to {self.url}:", err) from None
|
||||
except ValueError as err:
|
||||
raise ProcessingError("Unable to parse json:", err) from None
|
||||
|
||||
if method not in ModuleUtils.FILES_METHODS:
|
||||
log.debug(
|
||||
"Received response body: %s",
|
||||
resp_json
|
||||
)
|
||||
else:
|
||||
debug_json = resp_json.copy()
|
||||
if debug_json.get('result'):
|
||||
debug_json['result'] = shorten(debug_json['result'], 200, placeholder='...')
|
||||
log.debug(
|
||||
"Received response body (clipped): %s",
|
||||
json.dumps(debug_json, indent=4, separators=(',', ': '))
|
||||
)
|
||||
|
||||
if 'error' in resp_json:
|
||||
err = resp_json['error'].copy()
|
||||
err['body'] = request_json.copy()
|
||||
raise APIRequestError(err)
|
||||
|
||||
return resp_json
|
||||
|
||||
def __check_version(self, skip_check: bool) -> None:
|
||||
|
||||
skip_check_help = "If you're sure zabbix_utils will work properly with your current \
|
||||
Zabbix version you can skip this check by \
|
||||
specifying skip_version_check=True when create ZabbixAPI object."
|
||||
if self.version < __min_supported__:
|
||||
if skip_check:
|
||||
log.debug(
|
||||
"Version of Zabbix API [%s] is less than the library supports. %s",
|
||||
self.version,
|
||||
"Further library use at your own risk!"
|
||||
)
|
||||
else:
|
||||
raise APINotSupported(
|
||||
f"Version of Zabbix API [{self.version}] is not supported by the library. " +
|
||||
f"The oldest supported version is {__min_supported__}.0. " + skip_check_help
|
||||
)
|
||||
|
||||
if self.version > __max_supported__:
|
||||
if skip_check:
|
||||
log.debug(
|
||||
"Version of Zabbix API [%s] is more than the library was tested on. %s",
|
||||
self.version,
|
||||
"Recommended to update the library. Further library use at your own risk!"
|
||||
)
|
||||
else:
|
||||
raise APINotSupported(
|
||||
f"Version of Zabbix API [{self.version}] was not tested with the library. " +
|
||||
f"The latest tested version is {__max_supported__}.0. " + skip_check_help
|
||||
)
|
||||
@@ -0,0 +1,331 @@
|
||||
# zabbix_utils
|
||||
#
|
||||
# Copyright (C) 2001-2023 Zabbix SIA
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation
|
||||
# files (the "Software"), to deal in the Software without restriction,
|
||||
# including without limitation the rights to use, copy, modify,
|
||||
# merge, publish, distribute, sublicense, and/or sell copies
|
||||
# of the Software, and to permit persons to whom the Software
|
||||
# is furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import re
|
||||
import json
|
||||
import zlib
|
||||
import struct
|
||||
import asyncio
|
||||
|
||||
from textwrap import shorten
|
||||
from logging import Logger
|
||||
from socket import socket
|
||||
|
||||
from typing import Match, Union
|
||||
|
||||
|
||||
class ModuleUtils():
|
||||
|
||||
# Hidding mask for sensitive data
|
||||
HIDING_MASK = "*" * 8
|
||||
|
||||
# The main php-file of Zabbix API
|
||||
JSONRPC_FILE = 'api_jsonrpc.php'
|
||||
|
||||
# Methods working without auth token
|
||||
UNAUTH_METHODS = ('apiinfo.version', 'user.login', 'user.checkAuthentication')
|
||||
|
||||
# Methods returning files contents
|
||||
FILES_METHODS = ('configuration.export',)
|
||||
|
||||
# List of private fields and regular expressions to hide them
|
||||
PRIVATE_FIELDS = {
|
||||
"token": r"^.+$",
|
||||
"auth": r"^.+$",
|
||||
"passwd": r"^.+$",
|
||||
"sessionid": r"^.+$",
|
||||
"password": r"^.+$",
|
||||
"current_passwd": r"^.+$",
|
||||
"result": r"^[A-Za-z0-9]{32}$", # To hide only token or sessionid in result
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def check_url(cls, url: str) -> str:
|
||||
"""Check url completeness
|
||||
|
||||
Args:
|
||||
url (str): Unchecked URL of Zabbix API
|
||||
|
||||
Returns:
|
||||
str: Checked URL of Zabbix API
|
||||
"""
|
||||
|
||||
if not url.endswith(cls.JSONRPC_FILE):
|
||||
url += cls.JSONRPC_FILE if url[-1] == '/' else '/' + cls.JSONRPC_FILE
|
||||
if not url.startswith('http'):
|
||||
url = 'http://' + url
|
||||
|
||||
return url
|
||||
|
||||
@classmethod
|
||||
def mask_secret(cls, string: str, show_len: int = 4) -> str:
|
||||
"""Replace the most part of string to hiding mask.
|
||||
|
||||
Args:
|
||||
string (str): Raw string with without hiding.
|
||||
show_len (int, optional): Number of signs shown on each side of the string. \
|
||||
Defaults to 4.
|
||||
|
||||
Returns:
|
||||
str: String with hiding part.
|
||||
"""
|
||||
|
||||
# If show_len is 0 or the length of the string is smaller than the hiding mask length
|
||||
# and show_len from both sides of the string, return only hiding mask.
|
||||
if show_len == 0 or len(string) <= (len(cls.HIDING_MASK) + show_len*2):
|
||||
return cls.HIDING_MASK
|
||||
|
||||
# Return the string with the hiding mask, surrounded by the specified number of characters
|
||||
# to display on each side of the string.
|
||||
return f"{string[:show_len]}{cls.HIDING_MASK}{string[-show_len:]}"
|
||||
|
||||
@classmethod
|
||||
def hide_private(cls, input_data: dict, fields: dict = None) -> dict:
|
||||
"""Hide private data Zabbix info (e.g. token, password)
|
||||
|
||||
Args:
|
||||
input_data (dict): Input dictionary with private fields.
|
||||
fields (dict): Dictionary of private fields and their filtering regexps.
|
||||
|
||||
Returns:
|
||||
dict: Result dictionary without private data.
|
||||
"""
|
||||
|
||||
private_fields = fields if fields is not None else cls.PRIVATE_FIELDS
|
||||
|
||||
if not isinstance(input_data, dict):
|
||||
raise TypeError(f"Unsupported data type '{type(input_data).__name__}', \
|
||||
only 'dict' is expected")
|
||||
|
||||
def gen_repl(match: Match):
|
||||
return cls.mask_secret(match.group(0))
|
||||
|
||||
def hide_str(k, v):
|
||||
return re.sub(private_fields[k], gen_repl, v)
|
||||
|
||||
def hide_dict(v):
|
||||
return cls.hide_private(v, private_fields)
|
||||
|
||||
def hide_list(k, v):
|
||||
result = []
|
||||
for item in v:
|
||||
if isinstance(item, dict):
|
||||
result.append(hide_dict(item))
|
||||
continue
|
||||
if isinstance(item, list):
|
||||
result.append(hide_list(k, item))
|
||||
continue
|
||||
if isinstance(item, str):
|
||||
if k.rstrip('s') in private_fields:
|
||||
result.append(hide_str(k.rstrip('s'), item))
|
||||
continue
|
||||
# The 'result' regex is used to hide only token or
|
||||
# sessionid format for unknown values
|
||||
if 'result' in private_fields:
|
||||
result.append(hide_str('result', item))
|
||||
continue
|
||||
result.append(item)
|
||||
return result
|
||||
|
||||
result_data = input_data.copy()
|
||||
|
||||
for key, value in result_data.items():
|
||||
if isinstance(value, str):
|
||||
if key in private_fields:
|
||||
result_data[key] = hide_str(key, value)
|
||||
if isinstance(value, dict):
|
||||
result_data[key] = hide_dict(value)
|
||||
if isinstance(value, list):
|
||||
result_data[key] = hide_list(key, value)
|
||||
|
||||
return result_data
|
||||
|
||||
|
||||
class ZabbixProtocol():
|
||||
|
||||
ZABBIX_PROTOCOL = b'ZBXD'
|
||||
|
||||
HEADER_SIZE = 13
|
||||
|
||||
@classmethod
|
||||
def __prepare_request(cls, data: Union[bytes, str, list, dict]) -> bytes:
|
||||
if isinstance(data, bytes):
|
||||
return data
|
||||
if isinstance(data, str):
|
||||
return data.encode("utf-8")
|
||||
if isinstance(data, list) or isinstance(data, dict):
|
||||
return json.dumps(data, ensure_ascii=False).encode("utf-8")
|
||||
raise TypeError("Unsupported data type, only 'bytes', 'str', 'list' or 'dict' is expected")
|
||||
|
||||
@classmethod
|
||||
def create_packet(cls, payload: Union[bytes, str, list, dict],
|
||||
log: Logger, compression: bool = False) -> bytes:
|
||||
"""Create a packet for sending via the Zabbix protocol.
|
||||
|
||||
Args:
|
||||
payload (bytes|str|list|dict): Payload of the future packet
|
||||
log (Logger): Logger object
|
||||
compression (bool, optional): Compression use flag. Defaults to `False`.
|
||||
|
||||
Returns:
|
||||
bytes: Generated Zabbix protocol packet
|
||||
"""
|
||||
|
||||
request = cls.__prepare_request(payload)
|
||||
|
||||
log.debug('Request data: %s', shorten(request.decode("utf-8"), 200, placeholder='...'))
|
||||
|
||||
# 0x01 - Zabbix communications protocol
|
||||
flags = 0x01
|
||||
datalen = len(request)
|
||||
reserved = 0
|
||||
|
||||
if compression:
|
||||
# 0x02 - Using packet compression mode
|
||||
flags |= 0x02
|
||||
reserved = datalen
|
||||
request = zlib.compress(request)
|
||||
datalen = len(request)
|
||||
|
||||
header = struct.pack('<4sBII', cls.ZABBIX_PROTOCOL, flags, datalen, reserved)
|
||||
packet = header + request
|
||||
|
||||
log.debug('Content of the packet: %s', shorten(str(packet), 200, placeholder='...'))
|
||||
|
||||
return packet
|
||||
|
||||
@classmethod
|
||||
def receive_packet(cls, conn: socket, size: int, log: Logger) -> bytes:
|
||||
"""Receive a Zabbix protocol packet.
|
||||
|
||||
Args:
|
||||
conn (socket): Opened socket connection
|
||||
size (int): Expected packet size
|
||||
log (Logger): Logger object
|
||||
|
||||
Returns:
|
||||
bytes: Received packet content
|
||||
"""
|
||||
buf = b''
|
||||
|
||||
while len(buf) < size:
|
||||
chunk = conn.recv(size - len(buf))
|
||||
if not chunk:
|
||||
log.debug("Socket connection was closed before receiving expected amount of data.")
|
||||
break
|
||||
buf += chunk
|
||||
|
||||
return buf
|
||||
|
||||
@classmethod
|
||||
def parse_sync_packet(cls, conn: socket, log: Logger, exception) -> str:
|
||||
"""Parse a received synchronously Zabbix protocol packet.
|
||||
|
||||
Args:
|
||||
conn (socket): Opened socket connection
|
||||
log (Logger): Logger object
|
||||
exception: Exception type
|
||||
|
||||
Raises:
|
||||
exception: Depends on input exception type
|
||||
|
||||
Returns:
|
||||
str: Body of the received packet
|
||||
"""
|
||||
|
||||
response_header = cls.receive_packet(conn, cls.HEADER_SIZE, log)
|
||||
log.debug('Zabbix response header: %s', response_header)
|
||||
|
||||
if (not response_header.startswith(cls.ZABBIX_PROTOCOL) or
|
||||
len(response_header) != cls.HEADER_SIZE):
|
||||
log.debug('Unexpected response was received from Zabbix.')
|
||||
raise exception('Unexpected response was received from Zabbix.')
|
||||
|
||||
flags, datalen, reserved = struct.unpack('<BII', response_header[4:])
|
||||
|
||||
# 0x01 - Zabbix communications protocol
|
||||
if not flags & 0x01:
|
||||
raise exception(
|
||||
'Unexcepted flags were received. '
|
||||
'Check debug log for more information.'
|
||||
)
|
||||
# 0x04 - Using large packet mode
|
||||
if flags & 0x04:
|
||||
raise exception(
|
||||
'A large packet flag was received. '
|
||||
'Current module doesn\'t support large packets.'
|
||||
)
|
||||
# 0x02 - Using packet compression mode
|
||||
if flags & 0x02:
|
||||
response_body = zlib.decompress(cls.receive_packet(conn, datalen, log))
|
||||
else:
|
||||
response_body = cls.receive_packet(conn, datalen, log)
|
||||
|
||||
return response_body.decode("utf-8")
|
||||
|
||||
@classmethod
|
||||
async def parse_async_packet(cls, reader: asyncio.StreamReader, log: Logger, exception) -> str:
|
||||
"""Parse a received asynchronously Zabbix protocol packet.
|
||||
|
||||
Args:
|
||||
reader (StreamReader): Created asyncio.StreamReader
|
||||
log (Logger): Logger object
|
||||
exception: Exception type
|
||||
|
||||
Raises:
|
||||
exception: Depends on input exception type
|
||||
|
||||
Returns:
|
||||
str: Body of the received packet
|
||||
"""
|
||||
|
||||
response_header = await reader.readexactly(cls.HEADER_SIZE)
|
||||
log.debug('Zabbix response header: %s', response_header)
|
||||
|
||||
if (not response_header.startswith(cls.ZABBIX_PROTOCOL) or
|
||||
len(response_header) != cls.HEADER_SIZE):
|
||||
log.debug('Unexpected response was received from Zabbix.')
|
||||
raise exception('Unexpected response was received from Zabbix.')
|
||||
|
||||
flags, datalen, reserved = struct.unpack('<BII', response_header[4:])
|
||||
|
||||
# 0x01 - Zabbix communications protocol
|
||||
if not flags & 0x01:
|
||||
raise exception(
|
||||
'Unexcepted flags were received. '
|
||||
'Check debug log for more information.'
|
||||
)
|
||||
# 0x04 - Using large packet mode
|
||||
if flags & 0x04:
|
||||
raise exception(
|
||||
'A large packet flag was received. '
|
||||
'Current module doesn\'t support large packets.'
|
||||
)
|
||||
# 0x02 - Using packet compression mode
|
||||
if flags & 0x02:
|
||||
response_body = zlib.decompress(await reader.readexactly(datalen))
|
||||
else:
|
||||
response_body = await reader.readexactly(datalen)
|
||||
|
||||
return response_body.decode("utf-8")
|
||||
@@ -0,0 +1,68 @@
|
||||
# zabbix_utils
|
||||
#
|
||||
# Copyright (C) 2001-2023 Zabbix SIA
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation
|
||||
# files (the "Software"), to deal in the Software without restriction,
|
||||
# including without limitation the rights to use, copy, modify,
|
||||
# merge, publish, distribute, sublicense, and/or sell copies
|
||||
# of the Software, and to permit persons to whom the Software
|
||||
# is furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from typing import Union
|
||||
|
||||
from .common import ModuleUtils
|
||||
|
||||
|
||||
class ModuleBaseException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class APIRequestError(ModuleBaseException):
|
||||
"""Exception class when Zabbix API returns error by request.
|
||||
|
||||
Args:
|
||||
api_error (str|dict): Raw error message from Zabbix API.
|
||||
"""
|
||||
def __init__(self, api_error: Union[str, dict]):
|
||||
if isinstance(api_error, dict):
|
||||
api_error['body'] = ModuleUtils.hide_private(api_error['body'])
|
||||
super().__init__("{message} {data}".format(**api_error))
|
||||
for key, value in api_error.items():
|
||||
setattr(self, key, value)
|
||||
else:
|
||||
super().__init__(api_error)
|
||||
|
||||
|
||||
class APINotSupported(ModuleBaseException):
|
||||
"""Exception class when object/action is not supported by Zabbix API.
|
||||
|
||||
Args:
|
||||
message (str): Not supported object/action message.
|
||||
|
||||
version (str): Current version of Zabbix API.
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, version: str = None):
|
||||
if version:
|
||||
message = f"{message} is unsupported for Zabbix {version} version"
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class ProcessingError(ModuleBaseException):
|
||||
def __init__(self, *args):
|
||||
super().__init__(" ".join(map(str, args)))
|
||||
return
|
||||
@@ -0,0 +1,145 @@
|
||||
# zabbix_utils
|
||||
#
|
||||
# Copyright (C) 2001-2023 Zabbix SIA
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation
|
||||
# files (the "Software"), to deal in the Software without restriction,
|
||||
# including without limitation the rights to use, copy, modify,
|
||||
# merge, publish, distribute, sublicense, and/or sell copies
|
||||
# of the Software, and to permit persons to whom the Software
|
||||
# is furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import socket
|
||||
import logging
|
||||
from typing import Callable, Union
|
||||
|
||||
from .logger import EmptyHandler
|
||||
from .types import AgentResponse
|
||||
from .common import ZabbixProtocol
|
||||
from .exceptions import ProcessingError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.addHandler(EmptyHandler())
|
||||
|
||||
|
||||
class Getter():
|
||||
"""Zabbix get synchronous implementation.
|
||||
|
||||
Args:
|
||||
host (str, optional): Zabbix agent address. Defaults to `'127.0.0.1'`.
|
||||
|
||||
port (int, optional): Zabbix agent port. Defaults to `10050`.
|
||||
|
||||
timeout (int, optional): Connection timeout value. Defaults to `10`.
|
||||
|
||||
use_ipv6 (bool, optional): Specifying IPv6 use instead of IPv4. Defaults to `False`.
|
||||
|
||||
source_ip (str, optional): IP from which to establish connection. Defaults to `None`.
|
||||
|
||||
socket_wrapper (Callable, optional): Func(`conn`) to wrap socket. Defaults to `None`.
|
||||
"""
|
||||
|
||||
def __init__(self, host: str = '127.0.0.1', port: int = 10050, timeout: int = 10,
|
||||
use_ipv6: bool = False, source_ip: Union[str, None] = None,
|
||||
socket_wrapper: Union[Callable, None] = None):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.timeout = timeout
|
||||
self.use_ipv6 = use_ipv6
|
||||
self.source_ip = source_ip
|
||||
|
||||
self.socket_wrapper = socket_wrapper
|
||||
if self.socket_wrapper:
|
||||
if not isinstance(self.socket_wrapper, Callable):
|
||||
raise TypeError('Value "socket_wrapper" should be a function.')
|
||||
|
||||
def __get_response(self, conn: socket) -> Union[str, None]:
|
||||
result = ZabbixProtocol.parse_sync_packet(conn, log, ProcessingError)
|
||||
|
||||
log.debug('Received data: %s', result)
|
||||
|
||||
return result
|
||||
|
||||
def get(self, key: str) -> Union[str, None]:
|
||||
"""Gets item value from Zabbix agent by specified key.
|
||||
|
||||
Args:
|
||||
key (str): Zabbix item key.
|
||||
|
||||
Returns:
|
||||
str: Value from Zabbix agent for specified key.
|
||||
"""
|
||||
|
||||
packet = ZabbixProtocol.create_packet(key, log)
|
||||
|
||||
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 {self.host}:{self.port}") from None
|
||||
|
||||
connection.settimeout(self.timeout)
|
||||
|
||||
if self.source_ip:
|
||||
connection.bind((self.source_ip, 0,))
|
||||
|
||||
try:
|
||||
connection.connect((self.host, self.port))
|
||||
if self.socket_wrapper is not None:
|
||||
connection = self.socket_wrapper(connection)
|
||||
connection.sendall(packet)
|
||||
except (TimeoutError, socket.timeout) as err:
|
||||
log.error(
|
||||
'The connection to %s timed out after %d seconds',
|
||||
f"{self.host}:{self.port}",
|
||||
self.timeout
|
||||
)
|
||||
connection.close()
|
||||
raise err
|
||||
except (ConnectionRefusedError, socket.gaierror) as err:
|
||||
log.error(
|
||||
'An error occurred while trying to connect to %s: %s',
|
||||
f"{self.host}:{self.port}",
|
||||
getattr(err, 'msg', str(err))
|
||||
)
|
||||
connection.close()
|
||||
raise err
|
||||
except (OSError, socket.error) as err:
|
||||
log.warning(
|
||||
'An error occurred while trying to send to %s: %s',
|
||||
f"{self.host}:{self.port}",
|
||||
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)
|
||||
log.warning('Check access restrictions in Zabbix agent configuration.')
|
||||
raise err
|
||||
log.debug('Response from [%s:%s]: %s', self.host, self.port, response)
|
||||
|
||||
try:
|
||||
connection.close()
|
||||
except socket.error:
|
||||
pass
|
||||
|
||||
return AgentResponse(response)
|
||||
@@ -0,0 +1,53 @@
|
||||
# zabbix_utils
|
||||
#
|
||||
# Copyright (C) 2001-2023 Zabbix SIA
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation
|
||||
# files (the "Software"), to deal in the Software without restriction,
|
||||
# including without limitation the rights to use, copy, modify,
|
||||
# merge, publish, distribute, sublicense, and/or sell copies
|
||||
# of the Software, and to permit persons to whom the Software
|
||||
# is furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import json
|
||||
import logging
|
||||
from .common import ModuleUtils
|
||||
|
||||
|
||||
class EmptyHandler(logging.Handler):
|
||||
"""Empty logging handler."""
|
||||
|
||||
def emit(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class SensitiveFilter(logging.Filter):
|
||||
"""Filter to hide sensitive Zabbix info (password, auth) in logs"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __hide_data(self, raw_data):
|
||||
return json.dumps(ModuleUtils.hide_private(raw_data), indent=4, separators=(',', ': '))
|
||||
|
||||
def filter(self, record):
|
||||
if isinstance(record.args, tuple):
|
||||
record.args = tuple(self.__hide_data(arg)
|
||||
if isinstance(arg, dict) else arg for arg in record.args)
|
||||
if isinstance(record.args, dict):
|
||||
record.args = self.__hide_data(record.args)
|
||||
|
||||
return 1
|
||||
@@ -0,0 +1,316 @@
|
||||
# zabbix_utils
|
||||
#
|
||||
# Copyright (C) 2001-2023 Zabbix SIA
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation
|
||||
# files (the "Software"), to deal in the Software without restriction,
|
||||
# including without limitation the rights to use, copy, modify,
|
||||
# merge, publish, distribute, sublicense, and/or sell copies
|
||||
# of the Software, and to permit persons to whom the Software
|
||||
# is furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import json
|
||||
import socket
|
||||
import logging
|
||||
import configparser
|
||||
|
||||
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, Node
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.addHandler(EmptyHandler())
|
||||
|
||||
|
||||
class Sender():
|
||||
"""Zabbix sender synchronous implementation.
|
||||
|
||||
Args:
|
||||
server (str, optional): Zabbix server address. Defaults to `'127.0.0.1'`.
|
||||
port (int, optional): Zabbix server port. Defaults to `10051`.
|
||||
use_config (bool, optional): Specifying configuration use. Defaults to `False`.
|
||||
timeout (int, optional): Connection timeout value. Defaults to `10`.
|
||||
use_ipv6 (bool, optional): Specifying IPv6 use instead of IPv4. Defaults to `False`.
|
||||
source_ip (str, optional): IP from which to establish connection. Defaults to `None`.
|
||||
chunk_size (int, optional): Number of packets in one chunk. Defaults to `250`.
|
||||
clusters (tuple|list, optional): List of Zabbix clusters. Defaults to `None`.
|
||||
socket_wrapper (Callable, optional): Func(`conn`,`tls`) to wrap socket. Defaults to `None`.
|
||||
compression (bool, optional): Specifying compression use. Defaults to `False`.
|
||||
config_path (str, optional): Path to Zabbix agent configuration file. Defaults to \
|
||||
`/etc/zabbix/zabbix_agentd.conf`.
|
||||
"""
|
||||
|
||||
def __init__(self, server: Optional[str] = None, port: int = 10051,
|
||||
use_config: bool = False, timeout: int = 10,
|
||||
use_ipv6: bool = False, source_ip: Optional[str] = None,
|
||||
chunk_size: int = 250, clusters: Union[tuple, list] = None,
|
||||
socket_wrapper: Optional[Callable] = None, compression: bool = False,
|
||||
config_path: Optional[str] = '/etc/zabbix/zabbix_agentd.conf'):
|
||||
self.timeout = timeout
|
||||
self.use_ipv6 = use_ipv6
|
||||
self.tls = {}
|
||||
|
||||
self.host = None
|
||||
self.source_ip = None
|
||||
self.chunk_size = chunk_size
|
||||
self.compression = compression
|
||||
|
||||
if socket_wrapper is not None:
|
||||
if not isinstance(socket_wrapper, Callable):
|
||||
raise TypeError('Value "socket_wrapper" should be a function.') from None
|
||||
self.socket_wrapper = socket_wrapper
|
||||
|
||||
if source_ip is not None:
|
||||
self.source_ip = source_ip
|
||||
|
||||
if use_config:
|
||||
self.clusters = []
|
||||
self.__load_config(config_path)
|
||||
return
|
||||
|
||||
if clusters is not None:
|
||||
if not (isinstance(clusters, tuple) or isinstance(clusters, list)):
|
||||
raise TypeError('Value "clusters" should be a tuple or a list.') from None
|
||||
|
||||
clusters = clusters.copy()
|
||||
|
||||
if server is not None:
|
||||
clusters.append([f"{server}:{port}"])
|
||||
|
||||
self.clusters = [Cluster(c) for c in clusters]
|
||||
else:
|
||||
self.clusters = [Cluster([f"{server or '127.0.0.1'}:{port}"])]
|
||||
|
||||
def __read_config(self, config: configparser.SectionProxy) -> None:
|
||||
server_row = config.get('ServerActive') or config.get('Server') or '127.0.0.1:10051'
|
||||
|
||||
for cluster in server_row.split(','):
|
||||
self.clusters.append(Cluster(cluster.strip().split(';')))
|
||||
|
||||
self.host = config.get('Hostname')
|
||||
if 'SourceIP' in config:
|
||||
self.source_ip = config.get('SourceIP')
|
||||
|
||||
for key in config:
|
||||
if key.startswith('tls'):
|
||||
self.tls[key] = config.get(key)
|
||||
|
||||
def __load_config(self, filepath: str) -> None:
|
||||
config = configparser.ConfigParser(strict=False)
|
||||
|
||||
with open(filepath, 'r', encoding='utf-8') as cfg:
|
||||
config.read_string('[root]\n' + cfg.read())
|
||||
self.__read_config(config['root'])
|
||||
|
||||
def __get_response(self, conn: socket) -> Optional[dict]:
|
||||
try:
|
||||
result = json.loads(
|
||||
ZabbixProtocol.parse_sync_packet(conn, log, ProcessingError)
|
||||
)
|
||||
except json.decoder.JSONDecodeError as err:
|
||||
log.debug('Unexpected response was received from Zabbix.')
|
||||
raise err
|
||||
|
||||
log.debug('Received data: %s', result)
|
||||
|
||||
return result
|
||||
|
||||
def __create_request(self, items: list) -> dict:
|
||||
return {
|
||||
"request": "sender data",
|
||||
"data": [i.to_json() for i in items]
|
||||
}
|
||||
|
||||
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):
|
||||
|
||||
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))
|
||||
)
|
||||
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)}"
|
||||
)
|
||||
|
||||
if self.socket_wrapper is not None:
|
||||
connection = self.socket_wrapper(connection, self.tls)
|
||||
|
||||
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'):
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
def send(self, items: list) -> TrapperResponse:
|
||||
"""Sends packets and receives an answer from Zabbix.
|
||||
|
||||
Args:
|
||||
items (list): List of ItemValue objects.
|
||||
|
||||
Returns:
|
||||
TrapperResponse: Response from Zabbix server/proxy.
|
||||
"""
|
||||
|
||||
# Split the list of items into chunks of size self.chunk_size.
|
||||
chunks = [items[i:i + self.chunk_size] for i in range(0, len(items), self.chunk_size)]
|
||||
|
||||
# Merge responses into a single TrapperResponse object.
|
||||
try:
|
||||
result = TrapperResponse()
|
||||
except ProcessingError as err:
|
||||
log.debug(err)
|
||||
raise ProcessingError(err) from err
|
||||
|
||||
# TrapperResponse details for each node and chunk.
|
||||
result.details = {}
|
||||
|
||||
for i, chunk in enumerate(chunks):
|
||||
|
||||
if not all(isinstance(item, ItemValue) for item in chunk):
|
||||
log.debug('Received unexpected item list. It must be a list of \
|
||||
ItemValue objects: %s', json.dumps(chunk))
|
||||
raise ProcessingError(f"Received unexpected item list. \
|
||||
It must be a list of ItemValue objects: {json.dumps(chunk)}")
|
||||
|
||||
resp_by_node = self.__chunk_send(chunk)
|
||||
|
||||
node_step = 1
|
||||
for node, resp in resp_by_node.items():
|
||||
try:
|
||||
result.add(resp, (i + 1) * node_step)
|
||||
except ProcessingError as err:
|
||||
log.debug(err)
|
||||
raise ProcessingError(err) from None
|
||||
node_step += 1
|
||||
|
||||
if node not in result.details:
|
||||
result.details[node] = []
|
||||
result.details[node].append(TrapperResponse(i+1).add(resp))
|
||||
|
||||
return result
|
||||
|
||||
def send_value(self, host: str, key: str,
|
||||
value: str, clock: Optional[int] = None,
|
||||
ns: Optional[int] = None) -> TrapperResponse:
|
||||
"""Sends one value and receives an answer from Zabbix.
|
||||
|
||||
Args:
|
||||
host (str): Specify host name the item belongs to (as registered in Zabbix frontend).
|
||||
key (str): Specify item key to send value to.
|
||||
value (str): Specify item value.
|
||||
clock (int, optional): Specify time in Unix timestamp format. Defaults to `None`.
|
||||
ns (int, optional): Specify time expressed in nanoseconds. Defaults to `None`.
|
||||
|
||||
Returns:
|
||||
TrapperResponse: Response from Zabbix server/proxy.
|
||||
"""
|
||||
|
||||
return self.send([ItemValue(host or self.host or '', key, value, clock, ns)])
|
||||
@@ -0,0 +1,155 @@
|
||||
# zabbix_utils
|
||||
#
|
||||
# Copyright (C) 2001-2023 Zabbix SIA
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation
|
||||
# files (the "Software"), to deal in the Software without restriction,
|
||||
# including without limitation the rights to use, copy, modify,
|
||||
# merge, publish, distribute, sublicense, and/or sell copies
|
||||
# of the Software, and to permit persons to whom the Software
|
||||
# is furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import json
|
||||
import socket
|
||||
import logging
|
||||
from typing import Callable, Union
|
||||
|
||||
from .logger import EmptyHandler
|
||||
from .common import ZabbixProtocol
|
||||
from .exceptions import ProcessingError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.addHandler(EmptyHandler())
|
||||
|
||||
|
||||
class ZabbixStatus():
|
||||
"""Zabbix server status receiver.
|
||||
|
||||
Args:
|
||||
server (str, optional): Zabbix server address. Defaults to `'127.0.0.1'`.
|
||||
|
||||
port (int, optional): Zabbix server port. Defaults to `10051`.
|
||||
|
||||
timeout (int, optional): Connection timeout value. Defaults to `10`.
|
||||
|
||||
use_ipv6 (bool, optional): Specifying IPv6 use instead of IPv4. Defaults to `False`.
|
||||
|
||||
source_ip (str, optional): IP from which to establish connection. Defaults to `None`.
|
||||
|
||||
socket_wrapper (Callable, optional): Func(`conn`) to wrap socket. Defaults to `None`.
|
||||
"""
|
||||
|
||||
def __init__(self, server: str = '127.0.0.1', port: int = 10051, timeout: int = 10,
|
||||
use_ipv6: bool = False, source_ip: Union[str, None] = None,
|
||||
socket_wrapper: Union[Callable, None] = None):
|
||||
self.server = server
|
||||
self.port = port
|
||||
self.timeout = timeout
|
||||
self.use_ipv6 = use_ipv6
|
||||
self.source_ip = source_ip
|
||||
|
||||
self.socket_wrapper = socket_wrapper
|
||||
if self.socket_wrapper:
|
||||
if not isinstance(self.socket_wrapper, Callable):
|
||||
raise TypeError('Value "socket_wrapper" should be a function.')
|
||||
|
||||
def __get_response(self, conn: socket) -> Union[str, None]:
|
||||
result = ZabbixProtocol.parse_sync_packet(conn, log, ProcessingError)
|
||||
|
||||
log.debug('Received data: %s', result)
|
||||
|
||||
return result
|
||||
|
||||
def __create_request(self, sid: str) -> dict:
|
||||
return {
|
||||
"request": "status.get",
|
||||
"type": "full",
|
||||
"sid": sid,
|
||||
}
|
||||
|
||||
def status(self, sid: str) -> Union[str, None]:
|
||||
"""Gets status metrics from Zabbix server.
|
||||
|
||||
Args:
|
||||
sid (str): Zabbix API session id.
|
||||
|
||||
Returns:
|
||||
str: Zabbix server statistics data in JSON.
|
||||
"""
|
||||
|
||||
packet = ZabbixProtocol.create_packet(self.__create_request(sid), log)
|
||||
|
||||
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 {self.server}:{self.port}") from None
|
||||
|
||||
connection.settimeout(self.timeout)
|
||||
|
||||
if self.source_ip:
|
||||
connection.bind((self.source_ip, 0,))
|
||||
|
||||
try:
|
||||
connection.connect((self.server, self.port))
|
||||
if self.socket_wrapper is not None:
|
||||
connection = self.socket_wrapper(connection)
|
||||
connection.sendall(packet)
|
||||
except (TimeoutError, socket.timeout) as err:
|
||||
log.error(
|
||||
'The connection to %s timed out after %d seconds',
|
||||
f"{self.server}:{self.port}",
|
||||
self.timeout
|
||||
)
|
||||
connection.close()
|
||||
raise err
|
||||
except (ConnectionRefusedError, socket.gaierror) as err:
|
||||
log.error(
|
||||
'An error occurred while trying to connect to %s: %s',
|
||||
f"{self.server}:{self.port}",
|
||||
getattr(err, 'msg', str(err))
|
||||
)
|
||||
connection.close()
|
||||
raise err
|
||||
except (OSError, socket.error) as err:
|
||||
log.warning(
|
||||
'An error occurred while trying to send to %s: %s',
|
||||
f"{self.server}:{self.port}",
|
||||
getattr(err, 'msg', str(err))
|
||||
)
|
||||
connection.close()
|
||||
raise err
|
||||
|
||||
try:
|
||||
response = json.loads(self.__get_response(connection))
|
||||
except ConnectionResetError as err:
|
||||
log.debug('Get value error: %s', err)
|
||||
log.warning('Check access restrictions in Zabbix agent configuration.')
|
||||
raise err
|
||||
except json.decoder.JSONDecodeError as err:
|
||||
log.debug('Parsing value error: %s', err)
|
||||
raise err
|
||||
log.debug('Response from [%s:%s]: %s', self.server, self.port, response)
|
||||
|
||||
try:
|
||||
connection.close()
|
||||
except socket.error:
|
||||
pass
|
||||
|
||||
return response
|
||||
@@ -0,0 +1,401 @@
|
||||
# zabbix_utils
|
||||
#
|
||||
# Copyright (C) 2001-2023 Zabbix SIA
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation
|
||||
# files (the "Software"), to deal in the Software without restriction,
|
||||
# including without limitation the rights to use, copy, modify,
|
||||
# merge, publish, distribute, sublicense, and/or sell copies
|
||||
# of the Software, and to permit persons to whom the Software
|
||||
# is furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import re
|
||||
import json
|
||||
from typing import Union, Any, List
|
||||
from decimal import Decimal
|
||||
|
||||
from .exceptions import ProcessingError
|
||||
|
||||
from .version import __max_supported__
|
||||
|
||||
|
||||
class APIVersion():
|
||||
"""Zabbix API version object.
|
||||
|
||||
Args:
|
||||
apiver (str): Raw version in string format.
|
||||
"""
|
||||
|
||||
def __init__(self, apiver: str):
|
||||
self.__raw = apiver
|
||||
self.__first, self.__second, self.__third = self.__parse_version(self.__raw)
|
||||
|
||||
def __getitem__(self, index: int) -> Any:
|
||||
# Get a symbol from the raw version string by index
|
||||
# For compatibility with using Zabbix version as a string
|
||||
return self.__raw[index]
|
||||
|
||||
def is_lts(self) -> bool:
|
||||
"""Check if the current version is LTS.
|
||||
|
||||
Returns:
|
||||
bool: `True` if the current version is LTS.
|
||||
"""
|
||||
|
||||
return self.__second == 0
|
||||
|
||||
@property
|
||||
def major(self) -> float:
|
||||
"""Get major version number.
|
||||
|
||||
Returns:
|
||||
float: A major version number.
|
||||
"""
|
||||
|
||||
return float(f"{self.__first}.{self.__second}")
|
||||
|
||||
@property
|
||||
def minor(self) -> int:
|
||||
"""Get minor version number.
|
||||
|
||||
Returns:
|
||||
int: A minor version number.
|
||||
"""
|
||||
|
||||
return self.__third
|
||||
|
||||
def __parse_version(self, ver: str) -> List[Any]:
|
||||
# Parse the version string into a list of integers.
|
||||
match = re.fullmatch(r'(\d+)\.(\d+)\.(\d+)', ver)
|
||||
if match is None:
|
||||
raise ValueError(
|
||||
f"Unable to parse version of Zabbix API: {ver}. " +
|
||||
f"Default '{__max_supported__}.0' format is expected."
|
||||
) from None
|
||||
return list(map(int, match.groups()))
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__raw
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.__raw
|
||||
|
||||
def __eq__(self, other: Union[float, str]) -> bool:
|
||||
if isinstance(other, float):
|
||||
return self.major == other
|
||||
if isinstance(other, str):
|
||||
return [self.__first, self.__second, self.__third] == self.__parse_version(other)
|
||||
raise TypeError(
|
||||
f"'==' not supported between instances of '{type(self).__name__}' and \
|
||||
'{type(other).__name__}', only 'float' or 'str' is expected"
|
||||
)
|
||||
|
||||
def __gt__(self, other: Union[float, str]) -> bool:
|
||||
if isinstance(other, float):
|
||||
return self.major > other
|
||||
if isinstance(other, str):
|
||||
return [self.__first, self.__second, self.__third] > self.__parse_version(other)
|
||||
raise TypeError(
|
||||
f"'>' not supported between instances of '{type(self).__name__}' and \
|
||||
'{type(other).__name__}', only 'float' or 'str' is expected"
|
||||
)
|
||||
|
||||
def __lt__(self, other: Union[float, str]) -> bool:
|
||||
if isinstance(other, float):
|
||||
return self.major < other
|
||||
if isinstance(other, str):
|
||||
return [self.__first, self.__second, self.__third] < self.__parse_version(other)
|
||||
raise TypeError(
|
||||
f"'<' not supported between instances of '{type(self).__name__}' and \
|
||||
'{type(other).__name__}', only 'float' or 'str' is expected"
|
||||
)
|
||||
|
||||
def __ne__(self, other: Any) -> bool:
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __ge__(self, other: Any) -> bool:
|
||||
return not self.__lt__(other)
|
||||
|
||||
def __le__(self, other: Any) -> bool:
|
||||
return not self.__gt__(other)
|
||||
|
||||
|
||||
class TrapperResponse():
|
||||
"""Contains response from Zabbix server/proxy.
|
||||
|
||||
Args:
|
||||
chunk (int, optional): Current chunk number. Defaults to `1`.
|
||||
"""
|
||||
|
||||
def __init__(self, chunk: int = 1):
|
||||
self.__processed = 0
|
||||
self.__failed = 0
|
||||
self.__total = 0
|
||||
self.__time = 0
|
||||
self.__chunk = chunk
|
||||
self.details = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
result = {}
|
||||
for key, value in self.__dict__.items():
|
||||
if key == 'details':
|
||||
continue
|
||||
result[
|
||||
key[len(f"_{self.__class__.__name__}__"):]
|
||||
] = str(value) if isinstance(value, Decimal) else value
|
||||
|
||||
return json.dumps(result)
|
||||
|
||||
def parse(self, response: dict) -> dict:
|
||||
"""Parse response from Zabbix.
|
||||
|
||||
Args:
|
||||
response (dict): Raw response from Zabbix.
|
||||
|
||||
Raises:
|
||||
ProcessingError: Raises if unexpected response received
|
||||
"""
|
||||
|
||||
fields = {
|
||||
"processed": ('[Pp]rocessed', r'\d+'),
|
||||
"failed": ('[Ff]ailed', r'\d+'),
|
||||
"total": ('[Tt]otal', r'\d+'),
|
||||
"time": ('[Ss]econds spent', r'\d+\.\d+')
|
||||
}
|
||||
|
||||
pattern = re.compile(
|
||||
r";\s+?".join([rf"{r[0]}:\s+?(?P<{k}>{r[1]})" for k, r in fields.items()])
|
||||
)
|
||||
|
||||
info = response.get('info')
|
||||
if not info:
|
||||
raise ProcessingError(f"Received unexpected response: {response}")
|
||||
|
||||
res = pattern.search(info).groupdict()
|
||||
|
||||
return res
|
||||
|
||||
def add(self, response: dict, chunk: Union[int, None] = None):
|
||||
"""Add and merge response data from Zabbix.
|
||||
|
||||
Args:
|
||||
response (dict): Raw response from Zabbix.
|
||||
chunk (int, optional): Chunk number. Defaults to `None`.
|
||||
"""
|
||||
|
||||
resp = self.parse(response)
|
||||
|
||||
def add_value(cls, key, value):
|
||||
setattr(
|
||||
cls,
|
||||
key,
|
||||
getattr(cls, key) + value
|
||||
)
|
||||
|
||||
for k, v in resp.items():
|
||||
add_value(
|
||||
self,
|
||||
f"_{self.__class__.__name__}__{k}",
|
||||
Decimal(v) if '.' in v else int(v)
|
||||
)
|
||||
if chunk is not None:
|
||||
self.__chunk = chunk
|
||||
|
||||
return self
|
||||
|
||||
@property
|
||||
def processed(self) -> int:
|
||||
"""Returns number of processed packets.
|
||||
|
||||
Returns:
|
||||
int: Number of processed packets.
|
||||
"""
|
||||
|
||||
return self.__processed
|
||||
|
||||
@property
|
||||
def failed(self) -> int:
|
||||
"""Returns number of failed packets.
|
||||
|
||||
Returns:
|
||||
int: Number of failed packets.
|
||||
"""
|
||||
|
||||
return self.__failed
|
||||
|
||||
@property
|
||||
def total(self) -> int:
|
||||
"""Returns total number of packets.
|
||||
|
||||
Returns:
|
||||
int: Total number of packets.
|
||||
"""
|
||||
|
||||
return self.__total
|
||||
|
||||
@property
|
||||
def time(self) -> int:
|
||||
"""Returns value of spent time.
|
||||
|
||||
Returns:
|
||||
int: Spent time for the packets sending.
|
||||
"""
|
||||
|
||||
return self.__time
|
||||
|
||||
@property
|
||||
def chunk(self) -> int:
|
||||
"""Returns current chunk number.
|
||||
|
||||
Returns:
|
||||
int: Number of the current chunk.
|
||||
"""
|
||||
|
||||
return self.__chunk
|
||||
|
||||
|
||||
class ItemValue():
|
||||
"""Contains data of a single item value.
|
||||
|
||||
Args:
|
||||
host (str): Specify host name the item belongs to (as registered in Zabbix frontend).
|
||||
key (str): Specify item key to send value to.
|
||||
value (str): Specify item value.
|
||||
clock (int, optional): Specify time in Unix timestamp format. Defaults to `None`.
|
||||
ns (int, optional): Specify time expressed in nanoseconds. Defaults to `None`.
|
||||
"""
|
||||
|
||||
def __init__(self, host: str, key: str, value: str,
|
||||
clock: Union[int, None] = None, ns: Union[int, None] = None):
|
||||
self.host = str(host)
|
||||
self.key = str(key)
|
||||
self.value = str(value)
|
||||
self.clock = None
|
||||
self.ns = None
|
||||
|
||||
if clock is not None:
|
||||
try:
|
||||
self.clock = int(clock)
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
'The clock value must be expressed in the Unix Timestamp format') from None
|
||||
|
||||
if ns is not None:
|
||||
try:
|
||||
self.ns = int(ns)
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
'The ns value must be expressed in the integer value of nanoseconds') from None
|
||||
|
||||
def __str__(self) -> str:
|
||||
return json.dumps(self.to_json(), ensure_ascii=False)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.__str__()
|
||||
|
||||
def to_json(self) -> dict:
|
||||
"""Represents ItemValue object in dictionary for json.
|
||||
|
||||
Returns:
|
||||
dict: Object attributes in dictionary.
|
||||
"""
|
||||
|
||||
return {k: v for k, v in self.__dict__.items() if v is not None}
|
||||
|
||||
|
||||
class Node():
|
||||
"""Contains one Zabbix node object.
|
||||
|
||||
Args:
|
||||
addr (str): Listen address of Zabbix server.
|
||||
port (int, str): Listen port of Zabbix server.
|
||||
|
||||
Raises:
|
||||
TypeError: Raises if not integer value was received.
|
||||
"""
|
||||
|
||||
def __init__(self, addr: str, port: Union[int, str]):
|
||||
self.address = addr if addr != '0.0.0.0/0' else '127.0.0.1'
|
||||
try:
|
||||
self.port = int(port)
|
||||
except ValueError:
|
||||
raise TypeError('Port must be an integer value') from None
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.address}:{self.port}"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.__str__()
|
||||
|
||||
|
||||
class Cluster():
|
||||
"""Contains Zabbix node objects in a cluster object.
|
||||
|
||||
Args:
|
||||
addr (list): Raw list of node addresses.
|
||||
"""
|
||||
|
||||
def __init__(self, addr: list):
|
||||
self.nodes = self.__parse_ha_node(addr)
|
||||
|
||||
def __parse_ha_node(self, node_list: list) -> list:
|
||||
nodes = []
|
||||
for node_item in node_list:
|
||||
node_item = node_item.strip()
|
||||
if ':' in node_item:
|
||||
nodes.append(Node(*node_item.split(':')))
|
||||
else:
|
||||
nodes.append(Node(node_item, '10051'))
|
||||
|
||||
return nodes
|
||||
|
||||
def __str__(self) -> str:
|
||||
return json.dumps([(node.address, node.port) for node in self.nodes])
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.__str__()
|
||||
|
||||
|
||||
class AgentResponse:
|
||||
"""Contains response from Zabbix agent/agent2.
|
||||
|
||||
Args:
|
||||
response (string): Raw response from Zabbix.
|
||||
"""
|
||||
|
||||
def __init__(self, response: str):
|
||||
error_code = 'ZBX_NOTSUPPORTED'
|
||||
self.raw = response
|
||||
if response == error_code:
|
||||
self.value = None
|
||||
self.error = 'Not supported by Zabbix Agent'
|
||||
elif response.startswith(error_code + '\0'):
|
||||
self.value = None
|
||||
self.error = response[len(error_code)+1:]
|
||||
else:
|
||||
idx = response.find('\0')
|
||||
if idx == -1:
|
||||
self.value = response
|
||||
else:
|
||||
self.value = response[:idx]
|
||||
self.error = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return json.dumps({
|
||||
'error': self.error,
|
||||
'raw': self.raw,
|
||||
'value': self.value,
|
||||
})
|
||||
@@ -0,0 +1,28 @@
|
||||
# zabbix_utils
|
||||
#
|
||||
# Copyright (C) 2001-2023 Zabbix SIA
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation
|
||||
# files (the "Software"), to deal in the Software without restriction,
|
||||
# including without limitation the rights to use, copy, modify,
|
||||
# merge, publish, distribute, sublicense, and/or sell copies
|
||||
# of the Software, and to permit persons to whom the Software
|
||||
# is furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
__version__ = "2.0.3"
|
||||
|
||||
__min_supported__ = 6.0
|
||||
__max_supported__ = 7.4
|
||||
Reference in New Issue
Block a user