Skip to content

Commit

Permalink
Add Zabbix 7.2 support for httpapi and modules
Browse files Browse the repository at this point in the history
  • Loading branch information
mu1f407 committed Jan 13, 2025
1 parent a0f5d37 commit 21fa264
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 109 deletions.
1 change: 1 addition & 0 deletions .github/workflows/plugins-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jobs:
- version: "6.0"
- version: "6.4"
- version: "7.0"
- version: "7.2"
ansible:
# https://docs.ansible.com/ansible/latest/reference_appendices/release_and_maintenance.html#ansible-core-changelogs
- stable-2.15
Expand Down
103 changes: 41 additions & 62 deletions plugins/httpapi/zabbix.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
- name: http_login_password
"""

import inspect
import json
import base64

Expand All @@ -58,14 +59,18 @@
from ansible.module_utils.basic import to_text
from ansible.errors import AnsibleConnectionFailure
from ansible.plugins.httpapi import HttpApiBase
from ansible.module_utils.compat.version import StrictVersion
from ansible.module_utils.connection import ConnectionError


class HttpApi(HttpApiBase):
zbx_api_version = None
auth_key = None
auth = None
url_path = '/zabbix' # By default Zabbix WebUI is on http(s)://FQDN/zabbix

def __init__(self, connection):
super().__init__(connection)
self.connection._auth = {'Content-Type': 'application/json-rpc'}

def set_become(self, become_context):
"""As this is an http rpc call there is no elevation available
"""
Expand All @@ -75,39 +80,25 @@ def update_auth(self, response, response_text):
return None

def login(self, username, password):
self.auth_key = self.get_option('zabbix_auth_key')
if self.auth_key:
self.connection._auth = {'auth': self.auth_key}
if auth_key := self.get_option('zabbix_auth_key'):
self.auth = auth_key
return

if not self.auth_key:
# Provide "fake" auth so netcommon.connection does not replace our headers
self.connection._auth = {'auth': 'fake'}

# login() method is called "somehow" as a very first call to the REST API.
# This collection's code first of all executes api_version() but login() anyway
# is called first (I suspect due to complicated (for me) httpapi modules inheritance/communication
# model). Bottom line: at the time of login() execution we are not aware of Zabbix version.
# Proposed approach: first execute "user.login" with "user" parameter and if it fails then
# execute "user.login" with "username" parameter.
# Zabbix < 5.0 supports only "user" parameter.
# Zabbix >= 6.0 and <= 6.2 support both "user" and "username" parameters.
# Zabbix >= 6.4 supports only "username" parameter.
try:
# Zabbix <= 6.2
payload = self.payload_builder("user.login", user=username, password=password)
code, response = self.send_request(data=payload)
except ConnectionError:
# Zabbix >= 6.4
if StrictVersion(self.api_version()) >= StrictVersion('6.0'):
payload = self.payload_builder("user.login", username=username, password=password)
code, response = self.send_request(data=payload)
else:
payload = self.payload_builder("user.login", user=username, password=password)

code, response = self.send_request(data=payload)

if code == 200 and response != '':
# Replace auth with real api_key we got from Zabbix after successful login
self.connection._auth = {'auth': response}
self.auth = response

def logout(self):
if self.connection._auth and not self.auth_key:
if self.auth and not self.get_option('zabbix_auth_key'):
payload = self.payload_builder("user.logout")
self.send_request(data=payload)

Expand All @@ -119,48 +110,38 @@ def api_version(self):
self.url_path = ''
else:
self.url_path = '/' + url_path
if not self.zbx_api_version:
if not hasattr(self.connection, 'zbx_api_version'):
code, version = self.send_request(data=self.payload_builder('apiinfo.version'))
if code == 200 and len(version) != 0:
self.connection.zbx_api_version = version
else:
raise ConnectionError("Could not get API version from Zabbix. Got HTTP code %s. Got version %s" % (code, version))
self.zbx_api_version = self.connection.zbx_api_version
return self.zbx_api_version

def send_request(self, data=None, request_method="POST", path="/api_jsonrpc.php"):
path = self.url_path + path
if not data:
data = {}
if not hasattr(self.connection, 'zbx_api_version'):
code, version = self.send_request(data=self.payload_builder('apiinfo.version'))
if code == 200 and len(version) != 0:
self.connection.zbx_api_version = version
else:
raise ConnectionError("Could not get API version from Zabbix. Got HTTP code %s. Got version %s" % (code, version))
return self.connection.zbx_api_version

if self.connection._auth:
data['auth'] = self.connection._auth['auth']
def send_request(self, data, request_method="POST", path="/api_jsonrpc.php"):
headers = {}
path = self.url_path + path

hdrs = {
'Content-Type': 'application/json-rpc',
'Accept': 'application/json',
}
if self.auth and data['method'] not in ['user.login', 'apiinfo.version']:
if StrictVersion(self.api_version()) >= StrictVersion('6.4'):
headers['Authorization'] = 'Bearer ' + self.auth
else:
data['auth'] = self.auth

http_login_user = self.get_option('http_login_user')
http_login_password = self.get_option('http_login_password')
if http_login_user and http_login_user != '-42':
# Need to add Basic auth header
credentials = (http_login_user + ':' + http_login_password).encode('ascii')
hdrs['Authorization'] = 'Basic ' + base64.b64encode(credentials).decode("ascii")

if data['method'] in ['user.login', 'apiinfo.version']:
# user.login and apiinfo.version do not need "auth" in data
# we provided fake one in login() method to correctly handle HTTP basic auth header
data.pop('auth', None)
headers['Authorization'] = 'Basic ' + base64.b64encode(credentials).decode("ascii")

data = json.dumps(data)
try:
self._display_request(request_method, path)
self._display_request(request_method, path, data['method'])
response, response_data = self.connection.send(
path,
data,
json.dumps(data),
method=request_method,
headers=hdrs
headers=headers
)
value = to_text(response_data.getvalue())

Expand All @@ -171,11 +152,8 @@ def send_request(self, data=None, request_method="POST", path="/api_jsonrpc.php"
raise ConnectionError("Invalid JSON response: %s" % value)

if "error" in json_data:
# Get this response from Zabbix when we switch username to execute REST API
if "re-login" in json_data["error"]["data"]:
# Get this response from Zabbix when we switch username to execute REST API
if not self.auth_key:
# Provide "fake" auth so netcommon.connection does not replace our headers
self.connection._auth = {'auth': 'fake'}
# Need to login with new username/password
self.login(self.connection.get_option('remote_user'), self.connection.get_option('password'))
# Replace 'auth' field in payload with new one (we got from login process)
Expand All @@ -187,7 +165,7 @@ def send_request(self, data=None, request_method="POST", path="/api_jsonrpc.php"
path,
data,
method=request_method,
headers=hdrs
headers=headers
)
value = to_text(response_data.getvalue())

Expand Down Expand Up @@ -236,10 +214,11 @@ def send_request(self, data=None, request_method="POST", path="/api_jsonrpc.php"
except Exception as e:
raise e

def _display_request(self, request_method, path):
def _display_request(self, request_method, path, jsonrpc_method):
self.connection.queue_message(
"vvvv",
"Web Services: %s %s/%s" % (request_method, self.connection._url, path),
"Zabbix httpapi request: %s %s%s (%s)" % (
request_method, self.connection._url, path, jsonrpc_method),
)

def _get_response_value(self, response_data):
Expand Down
11 changes: 7 additions & 4 deletions plugins/modules/zabbix_host_events_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@

from ansible_collections.community.zabbix.plugins.module_utils.base import ZabbixBase
import ansible_collections.community.zabbix.plugins.module_utils.helpers as zabbix_utils

from ansible.module_utils.compat.version import LooseVersion

class Host(ZabbixBase):
def get_host(self, host_identifier, host_inventory, search_key):
Expand All @@ -299,9 +299,12 @@ def get_triggers_by_host_id_in_problem_state(self, host_id, trigger_severity, ta
def get_last_event_by_trigger_id(self, triggers_id):
""" Get the last event from triggerid"""
output = ["eventid", "clock", "acknowledged", "value"]
event = self._zapi.event.get({"output": output, "objectids": triggers_id,
"select_acknowledges": "extend", "selectTags": "extend", "limit": 1, "sortfield": "clock",
"sortorder": "DESC"})
parameters = {"output": output, "objectids": triggers_id, "selectAcknowledges": "extend",
"selectTags": "extend", "limit": 1, "sortfield": "clock", "sortorder": "DESC"}
if LooseVersion(self._zbx_api_version) <= LooseVersion("6.4"):
parameters["select_acknowledges"] = parameters["selectAcknowledges"]
del parameters["selectAcknowledges"]
event = self._zapi.event.get(parameters)
return event[0]


Expand Down
21 changes: 15 additions & 6 deletions plugins/modules/zabbix_host_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@

from ansible_collections.community.zabbix.plugins.module_utils.base import ZabbixBase
import ansible_collections.community.zabbix.plugins.module_utils.helpers as zabbix_utils
from ansible.module_utils.compat.version import LooseVersion


class Host(ZabbixBase):
Expand All @@ -132,15 +133,19 @@ def get_hosts_by_host_name(self, host_name, exact_match, host_inventory):
search_key = "search"
if exact_match:
search_key = "filter"
host_list = self._zapi.host.get({
parameters = {
"output": "extend",
"selectParentTemplates": ["name"],
search_key: {"host": [host_name]},
"selectInventory": host_inventory,
"selectGroups": "extend",
"selectHostGroups": "extend",
"selectTags": "extend",
"selectMacros": "extend"
})
}
if LooseVersion(self._zbx_api_version) < LooseVersion("6.2"):
parameters["selectGroups"] = parameters["selectHostGroups"]
del parameters["selectHostGroups"]
host_list = self._zapi.host.get(parameters)
if len(host_list) < 1:
self._module.fail_json(msg="Host not found: %s" % host_name)
else:
Expand All @@ -158,15 +163,19 @@ def get_hosts_by_ip(self, host_ips, host_inventory):
self._module.fail_json(msg="Host not found: %s" % host_ips)
host_list = []
for hostinterface in hostinterfaces:
host = self._zapi.host.get({
host_get_params = {
"output": "extend",
"selectGroups": "extend",
"selectHostGroups": "extend",
"selectParentTemplates": ["name"],
"hostids": hostinterface["hostid"],
"selectInventory": host_inventory,
"selectTags": "extend",
"selectMacros": "extend"
})
}
if LooseVersion(self._zbx_api_version) < LooseVersion("6.2"):
host_get_params["selectGroups"] = host_get_params["selectHostGroups"]
del host_get_params["selectHostGroups"]
host = self._zapi.host.get(host_get_params)
host[0]["hostinterfaces"] = hostinterface
host_list.append(host[0])
return host_list
Expand Down
45 changes: 29 additions & 16 deletions plugins/modules/zabbix_maintenance.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,15 +233,16 @@

from ansible_collections.community.zabbix.plugins.module_utils.base import ZabbixBase
import ansible_collections.community.zabbix.plugins.module_utils.helpers as zabbix_utils
from ansible.module_utils.compat.version import LooseVersion


class MaintenanceModule(ZabbixBase):
def create_maintenance(self, group_ids, host_ids, start_time,
maintenance_type, period, name, desc, tags):
end_time = start_time + period
parameters = {
"groupids": group_ids,
"hostids": host_ids,
"groups": [{"groupid": groupid} for groupid in group_ids],
"hosts": [{"hostid": hostid} for hostid in host_ids],
"name": name,
"maintenance_type": maintenance_type,
"active_since": str(start_time),
Expand All @@ -253,6 +254,11 @@ def create_maintenance(self, group_ids, host_ids, start_time,
"period": str(period),
}]
}
if LooseVersion(self._zbx_api_version) <= LooseVersion("6.0"):
parameters["groupids"] = group_ids
parameters["hostids"] = host_ids
del parameters["groups"]
del parameters["hosts"]
if tags is not None:
parameters["tags"] = tags
self._zapi.maintenance.create(parameters)
Expand All @@ -263,8 +269,8 @@ def update_maintenance(self, maintenance_id, group_ids, host_ids,
end_time = start_time + period
parameters = {
"maintenanceid": maintenance_id,
"groupids": group_ids,
"hostids": host_ids,
"groups": [{"groupid": groupid} for groupid in group_ids],
"hosts": [{"hostid": hostid} for hostid in host_ids],
"maintenance_type": maintenance_type,
"active_since": str(start_time),
"active_till": str(end_time),
Expand All @@ -275,27 +281,34 @@ def update_maintenance(self, maintenance_id, group_ids, host_ids,
"period": str(period),
}]
}
if LooseVersion(self._zbx_api_version) <= LooseVersion("6.0"):
parameters["groupids"] = group_ids
parameters["hostids"] = host_ids
del parameters["groups"]
del parameters["hosts"]
if tags is not None:
parameters["tags"] = tags
self._zapi.maintenance.update(parameters)
return 0, None, None

def get_maintenance(self, name):
maintenances = self._zapi.maintenance.get(
{
"filter":
{
"name": name,
},
"selectGroups": "extend",
"selectHosts": "extend",
"selectTags": "extend"
}
)
parameters = {
"filter": {"name": name},
"selectHostGroups": "extend",
"selectHosts": "extend",
"selectTags": "extend",
}
if LooseVersion(self._zbx_api_version) <= LooseVersion("6.0"):
parameters["selectGroups"] = parameters["selectHostGroups"]
del parameters["selectHostGroups"]
maintenances = self._zapi.maintenance.get(parameters)

for maintenance in maintenances:
maintenance["groupids"] = [group["groupid"] for group
in maintenance["groups"]] if "groups" in maintenance else []
in maintenance["hostgroups"]] if "hostgroups" in maintenance else []
if LooseVersion(self._zbx_api_version) <= LooseVersion("6.0"):
maintenance["groupids"] = [group["groupid"] for group
in maintenance["groups"]] if "groups" in maintenance else []
maintenance["hostids"] = [host["hostid"] for host
in maintenance["hosts"]] if "hosts" in maintenance else []
return 0, maintenance, None
Expand Down
Loading

0 comments on commit 21fa264

Please sign in to comment.