306 lines
9.6 KiB
Python
306 lines
9.6 KiB
Python
# pylint: disable=wrong-import-order
|
|
|
|
import logging
|
|
from typing import Mapping, Optional, Sequence, Tuple, Union
|
|
from warnings import warn
|
|
|
|
from packaging.version import Version
|
|
from requests import Session
|
|
|
|
__all__ = [
|
|
"ZabbixAPI",
|
|
"ZabbixAPIException",
|
|
"ZabbixAPIMethod",
|
|
"ZabbixAPIObject",
|
|
"ZabbixAPIObjectClass",
|
|
]
|
|
|
|
logger = logging.getLogger(__name__)
|
|
logger.addHandler(logging.NullHandler())
|
|
|
|
ZABBIX_5_4_0 = Version("5.4.0")
|
|
ZABBIX_6_4_0 = Version("6.4.0")
|
|
|
|
|
|
class ZabbixAPIException(Exception):
|
|
"""Generic Zabbix API exception
|
|
|
|
Codes:
|
|
-32700: invalid JSON. An error occurred on the server while
|
|
parsing the JSON text (typo, wrong quotes, etc.)
|
|
-32600: received JSON is not a valid JSON-RPC Request
|
|
-32601: requested remote-procedure does not exist
|
|
-32602: invalid method parameters
|
|
-32603: Internal JSON-RPC error
|
|
-32400: System error
|
|
-32300: Transport error
|
|
-32500: Application error
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args)
|
|
|
|
self.error = kwargs.get("error", None)
|
|
|
|
|
|
# pylint: disable=too-many-instance-attributes
|
|
class ZabbixAPI:
|
|
# pylint: disable=too-many-arguments
|
|
def __init__(
|
|
self,
|
|
server: str = "http://localhost/zabbix",
|
|
session: Optional[Session] = None,
|
|
use_authenticate: bool = False,
|
|
timeout: Optional[Union[float, int, Tuple[int, int]]] = None,
|
|
detect_version: bool = True,
|
|
):
|
|
"""
|
|
:param server: Base URI for zabbix web interface (omitting /api_jsonrpc.php)
|
|
:param session: optional pre-configured requests.Session instance
|
|
:param use_authenticate: Use old (Zabbix 1.8) style authentication
|
|
:param timeout: optional connect and read timeout in seconds, default: None
|
|
If you're using Requests >= 2.4 you can set it as
|
|
tuple: "(connect, read)" which is used to set individual
|
|
connect and read timeouts.
|
|
:param detect_version: autodetect Zabbix API version
|
|
"""
|
|
self.session = session or Session()
|
|
|
|
# Default headers for all requests
|
|
self.session.headers.update(
|
|
{
|
|
"Content-Type": "application/json-rpc",
|
|
"User-Agent": "python/pyzabbix",
|
|
"Cache-Control": "no-cache",
|
|
}
|
|
)
|
|
|
|
self.use_authenticate = use_authenticate
|
|
self.use_api_token = False
|
|
self.auth = ""
|
|
self.id = 0 # pylint: disable=invalid-name
|
|
|
|
self.timeout = timeout
|
|
|
|
if not server.endswith("/api_jsonrpc.php"):
|
|
server = server.rstrip("/") + "/api_jsonrpc.php"
|
|
self.url = server
|
|
logger.info("JSON-RPC Server Endpoint: %s", self.url)
|
|
|
|
self.version: Optional[Version] = None
|
|
self._detect_version = detect_version
|
|
|
|
def __enter__(self) -> "ZabbixAPI":
|
|
return self
|
|
|
|
# pylint: disable=inconsistent-return-statements
|
|
def __exit__(self, exception_type, exception_value, traceback):
|
|
if isinstance(exception_value, (ZabbixAPIException, type(None))):
|
|
if self.is_authenticated and not self.use_api_token:
|
|
# Logout the user if they are authenticated using username + password.
|
|
self.user.logout()
|
|
return True
|
|
return None
|
|
|
|
def login(
|
|
self,
|
|
user: str = "",
|
|
password: str = "",
|
|
api_token: Optional[str] = None,
|
|
) -> None:
|
|
"""Convenience method for calling user.authenticate
|
|
and storing the resulting auth token for further commands.
|
|
|
|
If use_authenticate is set, it uses the older (Zabbix 1.8)
|
|
authentication command
|
|
|
|
:param password: Password used to login into Zabbix
|
|
:param user: Username used to login into Zabbix
|
|
:param api_token: API Token to authenticate with
|
|
"""
|
|
|
|
if self._detect_version:
|
|
self.version = Version(self.api_version())
|
|
logger.info("Zabbix API version is: %s", self.version)
|
|
|
|
# If the API token is explicitly provided, use this instead.
|
|
if api_token is not None:
|
|
self.use_api_token = True
|
|
self.auth = api_token
|
|
return
|
|
|
|
# If we have an invalid auth token, we are not allowed to send a login
|
|
# request. Clear it before trying.
|
|
self.auth = ""
|
|
if self.use_authenticate:
|
|
self.auth = self.user.authenticate(user=user, password=password)
|
|
elif self.version and self.version >= ZABBIX_5_4_0:
|
|
self.auth = self.user.login(username=user, password=password)
|
|
else:
|
|
self.auth = self.user.login(user=user, password=password)
|
|
|
|
def check_authentication(self):
|
|
if self.use_api_token:
|
|
# We cannot use this call using an API Token
|
|
return True
|
|
# Convenience method for calling user.checkAuthentication of the current session
|
|
return self.user.checkAuthentication(sessionid=self.auth)
|
|
|
|
@property
|
|
def is_authenticated(self) -> bool:
|
|
if self.use_api_token:
|
|
# We cannot use this call using an API Token
|
|
return True
|
|
|
|
try:
|
|
self.user.checkAuthentication(sessionid=self.auth)
|
|
except ZabbixAPIException:
|
|
return False
|
|
return True
|
|
|
|
def confimport(
|
|
self,
|
|
confformat: str = "",
|
|
source: str = "",
|
|
rules: str = "",
|
|
) -> dict:
|
|
"""Alias for configuration.import because it clashes with
|
|
Python's import reserved keyword
|
|
:param rules:
|
|
:param source:
|
|
:param confformat:
|
|
"""
|
|
warn(
|
|
"ZabbixAPI.confimport(format, source, rules) has been deprecated, please use "
|
|
"ZabbixAPI.configuration['import'](format=format, source=source, rules=rules) instead",
|
|
DeprecationWarning,
|
|
2,
|
|
)
|
|
|
|
return self.configuration["import"](
|
|
format=confformat,
|
|
source=source,
|
|
rules=rules,
|
|
)
|
|
|
|
def api_version(self) -> str:
|
|
return self.apiinfo.version()
|
|
|
|
def do_request(
|
|
self,
|
|
method: str,
|
|
params: Optional[Union[Mapping, Sequence]] = None,
|
|
) -> dict:
|
|
payload = {
|
|
"jsonrpc": "2.0",
|
|
"method": method,
|
|
"params": params or {},
|
|
"id": self.id,
|
|
}
|
|
headers = {}
|
|
|
|
# We don't have to pass the auth token if asking for
|
|
# the apiinfo.version or user.checkAuthentication
|
|
anonymous_methods = {
|
|
"apiinfo.version",
|
|
"user.checkAuthentication",
|
|
"user.login",
|
|
}
|
|
if self.auth and method not in anonymous_methods:
|
|
if self.version and self.version >= ZABBIX_6_4_0:
|
|
headers["Authorization"] = f"Bearer {self.auth}"
|
|
else:
|
|
payload["auth"] = self.auth
|
|
|
|
logger.debug("Sending: %s", payload)
|
|
resp = self.session.post(
|
|
self.url,
|
|
json=payload,
|
|
headers=headers,
|
|
timeout=self.timeout,
|
|
)
|
|
logger.debug("Response Code: %s", resp.status_code)
|
|
|
|
# NOTE: Getting a 412 response code means the headers are not in the
|
|
# list of allowed headers.
|
|
resp.raise_for_status()
|
|
|
|
if not resp.text:
|
|
raise ZabbixAPIException("Received empty response")
|
|
|
|
try:
|
|
response = resp.json()
|
|
except ValueError as exception:
|
|
raise ZabbixAPIException(
|
|
f"Unable to parse json: {resp.text}"
|
|
) from exception
|
|
|
|
logger.debug("Response Body: %s", response)
|
|
|
|
self.id += 1
|
|
|
|
if "error" in response: # some exception
|
|
error = response["error"]
|
|
|
|
# some errors don't contain 'data': workaround for ZBX-9340
|
|
if "data" not in error:
|
|
error["data"] = "No data"
|
|
|
|
raise ZabbixAPIException(
|
|
f"Error {error['code']}: {error['message']}, {error['data']}",
|
|
error["code"],
|
|
error=error,
|
|
)
|
|
|
|
return response
|
|
|
|
def _object(self, attr: str) -> "ZabbixAPIObject":
|
|
"""Dynamically create an object class (ie: host)"""
|
|
return ZabbixAPIObject(attr, self)
|
|
|
|
def __getattr__(self, attr: str) -> "ZabbixAPIObject":
|
|
return self._object(attr)
|
|
|
|
def __getitem__(self, attr: str) -> "ZabbixAPIObject":
|
|
return self._object(attr)
|
|
|
|
|
|
# pylint: disable=too-few-public-methods
|
|
class ZabbixAPIMethod:
|
|
def __init__(self, method: str, parent: ZabbixAPI):
|
|
self._method = method
|
|
self._parent = parent
|
|
|
|
def __call__(self, *args, **kwargs):
|
|
if args and kwargs:
|
|
raise TypeError("Found both args and kwargs")
|
|
|
|
return self._parent.do_request(self._method, args or kwargs)["result"]
|
|
|
|
|
|
# pylint: disable=too-few-public-methods
|
|
class ZabbixAPIObject:
|
|
def __init__(self, name: str, parent: ZabbixAPI):
|
|
self._name = name
|
|
self._parent = parent
|
|
|
|
def _method(self, attr: str) -> ZabbixAPIMethod:
|
|
"""Dynamically create a method (ie: get)"""
|
|
return ZabbixAPIMethod(f"{self._name}.{attr}", self._parent)
|
|
|
|
def __getattr__(self, attr: str) -> ZabbixAPIMethod:
|
|
return self._method(attr)
|
|
|
|
def __getitem__(self, attr: str) -> ZabbixAPIMethod:
|
|
return self._method(attr)
|
|
|
|
|
|
class ZabbixAPIObjectClass(ZabbixAPIObject):
|
|
def __init__(self, *args, **kwargs):
|
|
warn(
|
|
"ZabbixAPIObjectClass has been renamed to ZabbixAPIObject",
|
|
DeprecationWarning,
|
|
2,
|
|
)
|
|
super().__init__(*args, **kwargs)
|