Add Zabbix monitoring hook.

Merge pull request #85 from tony1661/zabbix-hook
This commit is contained in:
Dan Helfman 2024-10-29 09:01:15 -07:00 committed by GitHub
commit c85bf46ad9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 599 additions and 0 deletions

View File

@ -1609,6 +1609,86 @@ properties:
example:
- start
- finish
zabbix:
type: object
additionalProperties: false
properties:
itemid:
type: integer
description: |
The ID of the Zabbix item used for collecting data.
Unique across the entire Zabbix system.
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 Zabbix instance.
example: https://zabbix.your-domain.com
username:
type: string
description: |
The username used for authentication. Not needed if using an API key.
example: testuser
password:
type: string
description: |
The password used for authentication. Not needed if using an API key.
example: fakepassword
api_key:
type: string
description: |
The API key used for authentication. Not needed if using an username/password.
example: fakekey
start:
type: object
properties:
value:
type: ["integer", "string"]
description: |
The value to set the item to on start.
example: STARTED
finish:
type: object
properties:
value:
type: ["integer", "string"]
description: |
The value to set the item to on finish.
example: FINISH
fail:
type: object
properties:
value:
type: ["integer", "string"]
description: |
The value to set the item 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']

View File

@ -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,
}

View File

@ -9,6 +9,7 @@ MONITOR_HOOK_NAMES = (
'ntfy',
'pagerduty',
'uptime_kuma',
'zabbix',
)

139
borgmatic/hooks/zabbix.py Normal file
View File

@ -0,0 +1,139 @@
import json
import logging
import requests
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):
'''
Update the configured Zabbix item using either the itemid, or a host and key.
If this is a dry run, then don't actually update anything.
'''
run_states = hook_config.get('states', ['fail'])
if state.name.lower() not in run_states:
return
dry_run_label = ' (dry run; not actually updating)' if dry_run else ''
state_config = hook_config.get(
state.name.lower(),
{
'value': state.name.lower(),
},
)
server = hook_config.get('server')
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}: Updating Zabbix{dry_run_label}')
logger.debug(f'{config_filename}: Using Zabbix URL: {server}')
if server is None:
logger.warning(f'{config_filename}: Server missing for Zabbix')
return
# 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')
return
elif key is not None:
logger.warning(f'{config_filename}: Host missing for Zabbix.')
return
else:
logger.warning(f'{config_filename}: No zabbix itemid or host/key provided.')
return
# Determine the authentication method: API key or username/password
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')
auth_data = {
"jsonrpc": "2.0",
"method": "user.login",
"params": {
"username": username,
"password": password
},
"id": 1
}
if not dry_run:
logging.getLogger('urllib3').setLevel(logging.ERROR)
try:
response = requests.post(server, headers=headers, json=auth_data)
data['auth'] = response.json().get('result')
if not response.ok:
response.raise_for_status()
except requests.exceptions.RequestException as error:
logger.warning(f'{config_filename}: Zabbix error: {error}')
elif username is not None:
logger.warning(f'{config_filename}: Password missing for Zabbix authentication')
return
elif password is not None:
logger.warning(f'{config_filename}: Username missing for Zabbix authentication')
return
else:
logger.warning(f'{config_filename}: Authentication data missing for Zabbix')
return
if not dry_run:
logging.getLogger('urllib3').setLevel(logging.ERROR)
try:
response = requests.post(server, headers=headers, json=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

View File

@ -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,58 @@ Heartbeat Retry = 360 # = 10 minutes
Resend Notification every X times = 1
```
## Zabbix hook
<span class="minilink minilink-addedin">New in version 1.9.0</span>
[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 its own. Instead, it relies on
your Zabbix instance to notify and perform escalations based on the Zabbix
configuration. The `states` defined in the configuration will determine which states
will trigger the hook. The value defined in the configuration of each state is
used to populate the data of the configured Zabbix item. If none are provided,
it default to a lower-case string of the state.
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
```
### Zabbix 7.0+
This hook requires the Zabbix server be running version 7.0+
<span class="minilink minilink-addedin">Authentication Methods</span>
Authentication can be accomplished via `api_key` or `username` and `password`.
If both are declared, `api_key` will be chosen.
<span class="minilink minilink-addedin">Items</span> 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` is referring to the 'Host name' on the
Zabbix host and not the 'Visual name'.
## Scripting borgmatic

View File

@ -0,0 +1,324 @@
from enum import Enum
from flexmock import flexmock
import borgmatic.hooks.monitor
from borgmatic.hooks import zabbix as module
SERVER = 'https://zabbix.com/zabbix/api_jsonrpc.php'
ITEMID = 55105
USERNAME = 'testuser'
PASSWORD = 'fakepassword'
API_KEY = 'fakekey'
HOST = 'borg-server'
KEY = 'borg.status'
VALUE = 'fail'
DATA_HOST_KEY = {
"jsonrpc": "2.0",
"method": "history.push",
"params": {"host": HOST, "key": KEY, "value": VALUE},
"id": 1,
}
DATA_HOST_KEY_WITH_TOKEN = {
"jsonrpc": "2.0",
"method": "history.push",
"params": {"host": HOST, "key": KEY, "value": VALUE},
"id": 1,
"auth": "3fe6ed01a69ebd79907a120bcd04e494"
}
DATA_ITEMID = {
"jsonrpc": "2.0",
"method": "history.push",
"params": {"itemid": ITEMID, "value": VALUE},
"id": 1,
}
DATA_HOST_KEY_WITH_TOKEN = {
"jsonrpc": "2.0",
"method": "history.push",
"params": {"itemid": ITEMID, "value": VALUE},
"id": 1,
"auth": "3fe6ed01a69ebd79907a120bcd04e494"
}
DATA_USER_LOGIN = {
"jsonrpc": "2.0",
"method": "user.login",
"params": {"username": USERNAME, "password": PASSWORD},
"id": 1,
}
AUTH_HEADERS_API_KEY = {
'Content-Type': 'application/json-rpc',
'Authorization': f'Bearer {API_KEY}'
}
AUTH_HEADERS_USERNAME_PASSWORD = {
'Content-Type': 'application/json-rpc'
}
def test_ping_monitor_config_with_api_key_only_exit_early():
# This test should exit early since only providing an API KEY is not enough
# for the hook to work
hook_config = {
'api_key': API_KEY
}
flexmock(module.logger).should_receive('warning').once()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_config_with_host_only_exit_early():
# This test should exit early since only providing a HOST is not enough
# for the hook to work
hook_config = {
'host': HOST
}
flexmock(module.logger).should_receive('warning').once()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_config_with_key_only_exit_early():
# This test should exit early since only providing a KEY is not enough
# for the hook to work
hook_config = {
'key': KEY
}
flexmock(module.logger).should_receive('warning').once()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_config_with_server_only_exit_early():
# This test should exit early since only providing a SERVER is not enough
# for the hook to work
hook_config = {
'server': SERVER
}
flexmock(module.logger).should_receive('warning').once()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_config_user_password_no_zabbix_data_exit_early():
# This test should exit early since there are HOST/KEY or ITEMID provided to publish data to
hook_config = {
'server': SERVER,
'username': USERNAME,
'password': PASSWORD
}
flexmock(module.logger).should_receive('warning').once()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_config_api_key_no_zabbix_data_exit_early():
# This test should exit early since there are HOST/KEY or ITEMID provided to publish data to
hook_config = {
'server': SERVER,
'api_key': API_KEY
}
flexmock(module.logger).should_receive('warning').once()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_config_itemid_no_auth_data_exit_early():
# This test should exit early since there is no authentication provided
# and Zabbix requires authentication to use it's API
hook_config = {
'server': SERVER,
'itemid': ITEMID
}
flexmock(module.logger).should_receive('warning').once()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_config_host_and_key_no_auth_data_exit_early():
# This test should exit early since there is no authentication provided
# and Zabbix requires authentication to use it's API
hook_config = {
'server': SERVER,
'host': HOST,
'key': KEY
}
flexmock(module.logger).should_receive('warning').once()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_config_host_and_key_with_api_key_auth_data_successful():
# This test should simulate a successful POST to a Zabbix server. This test uses API_KEY
# to authenticate and HOST/KEY to know which item to populate in Zabbix.
hook_config = {
'server': SERVER,
'host': HOST,
'key': KEY,
'api_key': API_KEY
}
flexmock(module.requests).should_receive('post').with_args(
f'{SERVER}',
headers=AUTH_HEADERS_API_KEY,
json=DATA_HOST_KEY,
).and_return(flexmock(ok=True)).once()
flexmock(module.logger).should_receive('warning').never()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_config_host_and_key_with_username_password_auth_data_successful():
# This test should simulate a successful POST to a Zabbix server. This test uses USERNAME/PASSWORD
# to authenticate and HOST/KEY to know which item to populate in Zabbix.
hook_config = {
'server': SERVER,
'host': HOST,
'key': KEY,
'username': USERNAME,
'password': PASSWORD
}
auth_response = flexmock(ok=True)
auth_response.should_receive('json').and_return({"jsonrpc":"2.0","result":"3fe6ed01a69ebd79907a120bcd04e494","id":1})
flexmock(module.requests).should_receive('post').with_args(
f'{SERVER}',
headers=AUTH_HEADERS_USERNAME_PASSWORD,
json=DATA_USER_LOGIN,
).and_return(auth_response).once()
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').with_args(
f'{SERVER}',
headers=AUTH_HEADERS_USERNAME_PASSWORD,
json=DATA_HOST_KEY_WITH_TOKEN,
).and_return(flexmock(ok=True)).once()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_config_itemid_with_api_key_auth_data_successful():
# This test should simulate a successful POST to a Zabbix server. This test uses API_KEY
# to authenticate and HOST/KEY to know which item to populate in Zabbix.
hook_config = {
'server': SERVER,
'itemid': ITEMID,
'api_key': API_KEY
}
flexmock(module.requests).should_receive('post').with_args(
f'{SERVER}',
headers=AUTH_HEADERS_API_KEY,
json=DATA_ITEMID,
).and_return(flexmock(ok=True)).once()
flexmock(module.logger).should_receive('warning').never()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_config_itemid_with_username_password_auth_data_successful():
# This test should simulate a successful POST to a Zabbix server. This test uses USERNAME/PASSWORD
# to authenticate and HOST/KEY to know which item to populate in Zabbix.
hook_config = {
'server': SERVER,
'itemid': ITEMID,
'username': USERNAME,
'password': PASSWORD
}
auth_response = flexmock(ok=True)
auth_response.should_receive('json').and_return({"jsonrpc":"2.0","result":"3fe6ed01a69ebd79907a120bcd04e494","id":1})
flexmock(module.requests).should_receive('post').with_args(
f'{SERVER}',
headers=AUTH_HEADERS_USERNAME_PASSWORD,
json=DATA_USER_LOGIN,
).and_return(auth_response).once()
flexmock(module.logger).should_receive('warning').never()
flexmock(module.requests).should_receive('post').with_args(
f'{SERVER}',
headers=AUTH_HEADERS_USERNAME_PASSWORD,
json=DATA_HOST_KEY_WITH_TOKEN,
).and_return(flexmock(ok=True)).once()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
test_ping_monitor_config_itemid_with_username_password_auth_data_successful()