diff options
Diffstat (limited to 'ansible_collections/community/zabbix/plugins/httpapi')
-rw-r--r-- | ansible_collections/community/zabbix/plugins/httpapi/zabbix.py | 227 |
1 files changed, 227 insertions, 0 deletions
diff --git a/ansible_collections/community/zabbix/plugins/httpapi/zabbix.py b/ansible_collections/community/zabbix/plugins/httpapi/zabbix.py new file mode 100644 index 000000000..3db65532c --- /dev/null +++ b/ansible_collections/community/zabbix/plugins/httpapi/zabbix.py @@ -0,0 +1,227 @@ +# (c) 2021, Markus Fischbacher (fischbacher.markus@gmail.com) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# Quick Link to Zabbix API docs: https://www.zabbix.com/documentation/current/manual/api + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = """ +--- +name: zabbix +author: + - Markus Fischbacher (@rockaut) + - Evgeny Yurchenko (@BGmot) +short_description: HttpApi Plugin for Zabbix +description: + - This HttpApi plugin provides methods to connect to Zabbix over their HTTP(S)-based api. +version_added: 1.8.0 +options: + zabbix_auth_key: + type: str + description: + - Specifies API authentication key + env: + - name: ANSIBLE_ZABBIX_AUTH_KEY + vars: + - name: ansible_zabbix_auth_key + zabbix_url_path: + type: str + description: + - Specifies path portion in Zabbix WebUI URL, e.g. for https://myzabbixfarm.com/zabbixeu zabbix_url_path=zabbixeu + default: zabbix + env: + - name: ANSIBLE_ZABBIX_URL_PATH + vars: + - name: ansible_zabbix_url_path + http_login_user: + type: str + description: + - The http user to access zabbix url with Basic Auth + vars: + - name: http_login_user + http_login_password: + type: str + description: + - The http password to access zabbix url with Basic Auth + vars: + - name: http_login_password +""" + +import json +import base64 + +from uuid import uuid4 + +from ansible.module_utils.basic import to_text +from ansible.errors import AnsibleConnectionFailure +from ansible.plugins.httpapi import HttpApiBase +from ansible.module_utils.connection import ConnectionError + + +class HttpApi(HttpApiBase): + zbx_api_version = None + auth_key = None + url_path = '/zabbix' # By default Zabbix WebUI is on http(s)://FQDN/zabbix + + def set_become(self, become_context): + """As this is an http rpc call there is no elevation available + """ + pass + + 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} + 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 + payload = self.payload_builder("user.login", username=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} + + def logout(self): + if self.connection._auth and not self.auth_key: + payload = self.payload_builder("user.logout") + self.send_request(data=payload) + + def api_version(self): + url_path = self.get_option('zabbix_url_path') + if isinstance(url_path, str): + # zabbix_url_path provided (even if it is an empty string) + if url_path == '': + 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 self.connection._auth: + data['auth'] = self.connection._auth['auth'] + + hdrs = { + 'Content-Type': 'application/json-rpc', + 'Accept': 'application/json', + } + 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) + + data = json.dumps(data) + try: + self._display_request(request_method, path) + response, response_data = self.connection.send( + path, + data, + method=request_method, + headers=hdrs + ) + value = to_text(response_data.getvalue()) + + try: + json_data = json.loads(value) if value else {} + if "result" in json_data: + json_data = json_data["result"] + # JSONDecodeError only available on Python 3.5+ + except ValueError: + raise ConnectionError("Invalid JSON response: %s" % value) + + try: + # Some methods return bool not a dict in "result" + iter(json_data) + except TypeError: + # Do not try to find "error" if it is not a dict + return response.getcode(), json_data + + if "error" in json_data: + raise ConnectionError("REST API returned %s when sending %s" % (json_data["error"], data)) + + return response.getcode(), json_data + except AnsibleConnectionFailure as e: + self.connection.queue_message("vvv", "AnsibleConnectionFailure: %s" % e) + if to_text("Could not connect to") in to_text(e): + raise + if to_text("401") in to_text(e): + return 401, "Authentication failure" + else: + return 404, "Object not found" + except Exception as e: + raise e + + def _display_request(self, request_method, path): + self.connection.queue_message( + "vvvv", + "Web Services: %s %s/%s" % (request_method, self.connection._url, path), + ) + + def _get_response_value(self, response_data): + return to_text(response_data.getvalue()) + + def _response_to_json(self, response_text): + try: + return json.loads(response_text) if response_text else {} + # JSONDecodeError only available on Python 3.5+ + except ValueError: + raise ConnectionError("Invalid JSON response: %s" % response_text) + + @staticmethod + def payload_builder(method_, auth_=None, **kwargs): + reqid = str(uuid4()) + req = {'jsonrpc': '2.0', 'method': method_, 'id': reqid} + req['params'] = (kwargs) + + return req + + def handle_httperror(self, exc): + # The method defined in ansible.plugins.httpapi + # We need to override it to avoid endless re-tries if HTTP authentication fails + + if exc.code == 401: + return False + + return exc |