diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index 847ca6f5..09b6847b 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -1602,6 +1602,88 @@ properties: example: - start - finish + zabbix: + type: object + additionalProperties: false + properties: + itemid: + type: integer + description: | + The itemid to publish to + for details. + example: 55105 + host: + type: string + description: | + Host name where the item is stored. + Required if 'itemid' is not set + example: borg-server + key: + type: string + description: | + Key of the host where the item is stored. + Required if 'itemid' is not set + example: borg.status + server: + type: string + description: | + The address of your self-hosted zabbix instance. + example: https://zabbix.your-domain.com + username: + type: string + description: | + The username used for authentication. + example: testuser + password: + type: string + description: | + The password used for authentication. + example: fakepassword + api_key: + type: string + description: | + The api_key used for authentication. + example: fakekey + start: + type: object + properties: + value: + type: string + description: | + The string to set the item value to on start. + example: STARTED + finish: + type: object + properties: + value: + type: string + description: | + The string to set the item value to on finish. + example: FINISH + fail: + type: object + properties: + value: + type: string + description: | + The string to set the item value to on fail. + example: ERROR + states: + type: array + items: + type: string + enum: + - start + - finish + - fail + uniqueItems: true + description: | + List of one or more monitoring states to ping for: "start", + "finish", and/or "fail". Defaults to pinging for failure + only. + example: + - start + - finish apprise: type: object required: ['services'] diff --git a/borgmatic/hooks/dispatch.py b/borgmatic/hooks/dispatch.py index 6131ef6b..be85af8c 100644 --- a/borgmatic/hooks/dispatch.py +++ b/borgmatic/hooks/dispatch.py @@ -14,6 +14,7 @@ from borgmatic.hooks import ( postgresql, sqlite, uptimekuma, + zabbix, ) logger = logging.getLogger(__name__) @@ -32,6 +33,7 @@ HOOK_NAME_TO_MODULE = { 'postgresql_databases': postgresql, 'sqlite_databases': sqlite, 'uptime_kuma': uptimekuma, + 'zabbix': zabbix, } diff --git a/borgmatic/hooks/monitor.py b/borgmatic/hooks/monitor.py index 9512fe99..de4951b7 100644 --- a/borgmatic/hooks/monitor.py +++ b/borgmatic/hooks/monitor.py @@ -9,6 +9,7 @@ MONITOR_HOOK_NAMES = ( 'ntfy', 'pagerduty', 'uptime_kuma', + 'zabbix', ) diff --git a/borgmatic/hooks/zabbix.py b/borgmatic/hooks/zabbix.py new file mode 100644 index 00000000..ea17d7a0 --- /dev/null +++ b/borgmatic/hooks/zabbix.py @@ -0,0 +1,93 @@ +import logging +import requests +import json + +logger = logging.getLogger(__name__) + + +def initialize_monitor( + ping_url, config, config_filename, monitoring_log_level, dry_run +): # pragma: no cover + ''' + No initialization is necessary for this monitor. + ''' + pass + + +def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run): + ''' + Ping the configured Zabbix topic. Use the given configuration filename in any log entries. + If this is a dry run, then don't actually ping anything. + ''' + + run_states = hook_config.get('states', ['fail']) + + if state.name.lower() in run_states: + dry_run_label = ' (dry run; not actually pinging)' if dry_run else '' + + state_config = hook_config.get(state.name.lower(),{'value': f'invalid',},) + + base_url = hook_config.get('server', 'https://cloud.zabbix.com/zabbix/api_jsonrpc.php') + username = hook_config.get('username') + password = hook_config.get('password') + api_key = hook_config.get('api_key') + itemid = hook_config.get('itemid') + host = hook_config.get('host') + key = hook_config.get('key') + + value = state_config.get('value') + + headers = {'Content-Type': 'application/json-rpc'} + + logger.info(f'{config_filename}: Pinging zabbix {dry_run_label}') + logger.debug(f'{config_filename}: Using zabbix ping URL {base_url}') + + # Determine the zabbix method used to store the value: itemid or host/key + if (itemid) is not None: + logger.info(f'{config_filename}: Updating {itemid} on zabbix') + data = {"jsonrpc":"2.0","method":"history.push","params":{"itemid":itemid,"value":value},"id":1} + + elif (host and key) is not None: + logger.info(f'{config_filename}: Updating Host:{host} and Key:{key} on zabbix') + data = {"jsonrpc":"2.0","method":"history.push","params":{"host":host,"key":key,"value":value},"id":1} + + elif (host) is not None: + logger.warning( f'{config_filename}: Key missing for zabbix authentication' ) + + elif (key) is not None: + logger.warning( f'{config_filename}: Host missing for zabbix authentication' ) + + # Determine the authentication method: API key or username/password + auth = None + if (api_key) is not None: + logger.info(f'{config_filename}: Using API key auth for zabbix') + headers['Authorization'] = 'Bearer ' + api_key + + elif (username and password) is not None: + logger.info(f'{config_filename}: Using user/pass auth with user {username} for zabbix') + response = requests.post(base_url, headers=headers, data='{"jsonrpc":"2.0","method":"user.login","params":{"username":"'+username+'","password":"'+password+'"},"id":1}') + data['auth'] = response.json().get('result') + + elif username is not None: + logger.warning( f'{config_filename}: Password missing for zabbix authentication, defaulting to no auth' ) + + elif password is not None: + logger.warning( f'{config_filename}: Username missing for zabbix authentication, defaulting to no auth' ) + + if not dry_run: + logging.getLogger('urllib3').setLevel(logging.ERROR) + try: + response = requests.post(base_url, headers=headers, data=json.dumps(data)) + if not response.ok: + response.raise_for_status() + except requests.exceptions.RequestException as error: + logger.warning(f'{config_filename}: zabbix error: {error}') + + +def destroy_monitor( + ping_url_or_uuid, config, config_filename, monitoring_log_level, dry_run +): # pragma: no cover + ''' + No destruction is necessary for this monitor. + ''' + pass diff --git a/docs/how-to/monitor-your-backups.md b/docs/how-to/monitor-your-backups.md index e96c4040..ddc540b9 100644 --- a/docs/how-to/monitor-your-backups.md +++ b/docs/how-to/monitor-your-backups.md @@ -47,6 +47,7 @@ them as backups happen: * [ntfy](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#ntfy-hook) * [PagerDuty](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pagerduty-hook) * [Uptime Kuma](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#uptime-kuma-hook) + * [Zabbix](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#zabbix-hook) The idea is that you'll receive an alert when something goes wrong or when the service doesn't hear from borgmatic for a configured interval (if supported). @@ -562,6 +563,55 @@ Heartbeat Retry = 360 # = 10 minutes Resend Notification every X times = 1 ``` +## zabbix hook + +New in version 1.8.15 +[zabbix](https://www.zabbix.com/) is an open-source monitoring tool used for tracking and managing the performance and availability of networks, servers, and applications in real-time. + +This hook does not do any notifications on it's own. Instead, it relies on +your zabbix instance to notify and perform escalations based on the zabbix +configuration. The `states` list will choose which states to trigger on. +Each state can have its own custom values configured. These values are +populated in the item data in zabbix. If none are provided, will use the +default. + +An example configuration is shown here with all the available options. + +```yaml +zabbix: + server: http://cloud.zabbix.com/zabbix/api_jsonrpc.php + + username: myuser + password: secret + api_key: b2ecba64d8beb47fc161ae48b164cfd7104a79e8e48e6074ef5b141d8a0aeeca + + host: "borg-server" + key: borg.status + itemid: 55105 + + start: + value: "STARTED" + finish: + value: "OK" + fail: + value: "ERROR" + states: + - start + - finish + - fail +``` + +Authentication Methods +Authentication can be accomplished via `api_key` or `username` and `password`. +If both are declared, `api_key` will be chosen. + +Items The item +to be updated can be chosen by either declaring the `itemid` or +`host` and `key`. If both are declared, `itemid` will be chosen. + +Keep in mind that `host` the Host name on the host and not the +Visual name. + ## Scripting borgmatic