Flesh out the Zabbix monitoring hook tests, add a logo to the documentation, etc.
All checks were successful
build / test (push) Successful in 6m8s
build / docs (push) Successful in 2m9s

This commit is contained in:
Dan Helfman 2024-10-29 10:33:19 -07:00
parent c85bf46ad9
commit 129f3e753c
7 changed files with 281 additions and 128 deletions

2
NEWS
View File

@ -30,6 +30,8 @@
* Update the "--match-archives" and "--archive" flags to support Borg 2 series names or archive
hashes.
* Add a "--match-archives" flag to the "prune" action.
* Add a Zabbix monitoring hook. See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#zabbix-hook
1.8.14
* #896: Fix an error in borgmatic rcreate/init on an empty repository directory with Borg 1.4.

View File

@ -69,6 +69,7 @@ borgmatic is powered by [Borg Backup](https://www.borgbackup.org/).
<a href="https://ntfy.sh/"><img src="docs/static/ntfy.png" alt="ntfy" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://grafana.com/oss/loki/"><img src="docs/static/loki.png" alt="Loki" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://github.com/caronc/apprise/wiki"><img src="docs/static/apprise.png" alt="Apprise" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://www.zabbix.com/"><img src="docs/static/zabbix.png" alt="Zabbix" height="40px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://www.borgbase.com/?utm_source=borgmatic"><img src="docs/static/borgbase.png" alt="BorgBase" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>

View File

@ -1622,12 +1622,14 @@ properties:
host:
type: string
description: |
Host name where the item is stored. Required if "itemid" is not set.
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.
Key of the host where the item is stored. Required if
"itemid" is not set.
example: borg.status
server:
type: string
@ -1637,17 +1639,20 @@ properties:
username:
type: string
description: |
The username used for authentication. Not needed if using an API key.
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.
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.
The API key used for authentication. Not needed if using
an username/password.
example: fakekey
start:
type: object

View File

@ -1,4 +1,3 @@
import json
import logging
import requests
@ -52,23 +51,23 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
logger.warning(f'{config_filename}: Server missing for Zabbix')
return
# Determine the zabbix method used to store the value: itemid or host/key
# 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,
'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,
'jsonrpc': '2.0',
'method': 'history.push',
'params': {'host': host, 'key': key, 'value': value},
'id': 1,
}
elif host is not None:
@ -90,13 +89,10 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
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
'jsonrpc': '2.0',
'method': 'user.login',
'params': {'username': username, 'password': password},
'id': 1,
}
if not dry_run:
logging.getLogger('urllib3').setLevel(logging.ERROR)
@ -107,6 +103,7 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
response.raise_for_status()
except requests.exceptions.RequestException as error:
logger.warning(f'{config_filename}: Zabbix error: {error}')
return
elif username is not None:
logger.warning(f'{config_filename}: Password missing for Zabbix authentication')
@ -118,7 +115,6 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
else:
logger.warning(f'{config_filename}: Authentication data missing for Zabbix')
return
if not dry_run:
logging.getLogger('urllib3').setLevel(logging.ERROR)

View File

@ -566,14 +566,16 @@ 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.
[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.
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 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 defaults to a lower-case string of the state.
An example configuration is shown here with all the available options.
@ -601,19 +603,22 @@ zabbix:
- 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.
### Authentication methods
Keep in mind that `host` is referring to the 'Host name' on the
Zabbix host and not the 'Visual name'.
Authentication can be accomplished via `api_key` or both `username` and
`password`. If all three are declared, only `api_key` is used.
### Items
The item to be updated can be chosen by either declaring the `itemid` or both
`host` and `key`. If all three are declared, only `itemid` is used.
Keep in mind that `host` is referring to the "Host name" on the Zabbix server
and not the "Visual name".
## Scripting borgmatic

BIN
docs/static/zabbix.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@ -1,5 +1,3 @@
from enum import Enum
from flexmock import flexmock
import borgmatic.hooks.monitor
@ -15,58 +13,70 @@ KEY = 'borg.status'
VALUE = 'fail'
DATA_HOST_KEY = {
"jsonrpc": "2.0",
"method": "history.push",
"params": {"host": HOST, "key": KEY, "value": VALUE},
"id": 1,
'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_HOST_KEY_WITH_KEY_VALUE = {
'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,
'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_HOST_KEY_WITH_ITEMID = {
'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,
'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}'
'Authorization': f'Bearer {API_KEY}',
}
AUTH_HEADERS_USERNAME_PASSWORD = {
'Content-Type': 'application/json-rpc'
}
AUTH_HEADERS_USERNAME_PASSWORD = {'Content-Type': 'application/json-rpc'}
def test_ping_monitor_with_non_matching_state_exits_early():
hook_config = {'api_key': API_KEY}
flexmock(module.requests).should_receive('post').never()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
)
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
# 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
}
hook_config = {'api_key': API_KEY}
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
module.ping_monitor(
hook_config,
@ -77,13 +87,13 @@ def test_ping_monitor_config_with_api_key_only_exit_early():
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
# This test should exit early since only providing a HOST is not enough
# for the hook to work
hook_config = {
'host': HOST
}
hook_config = {'host': HOST}
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
module.ping_monitor(
hook_config,
@ -94,13 +104,13 @@ def test_ping_monitor_config_with_host_only_exit_early():
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
# This test should exit early since only providing a KEY is not enough
# for the hook to work
hook_config = {
'key': KEY
}
hook_config = {'key': KEY}
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
module.ping_monitor(
hook_config,
@ -111,13 +121,13 @@ def test_ping_monitor_config_with_key_only_exit_early():
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
# This test should exit early since only providing a SERVER is not enough
# for the hook to work
hook_config = {
'server': SERVER
}
hook_config = {'server': SERVER}
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
module.ping_monitor(
hook_config,
@ -128,14 +138,12 @@ def test_ping_monitor_config_with_server_only_exit_early():
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
}
hook_config = {'server': SERVER, 'username': USERNAME, 'password': PASSWORD}
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
module.ping_monitor(
hook_config,
@ -146,13 +154,12 @@ def test_ping_monitor_config_user_password_no_zabbix_data_exit_early():
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
}
hook_config = {'server': SERVER, 'api_key': API_KEY}
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
module.ping_monitor(
hook_config,
@ -163,14 +170,13 @@ def test_ping_monitor_config_api_key_no_zabbix_data_exit_early():
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
# 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
}
hook_config = {'server': SERVER, 'itemid': ITEMID}
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
module.ping_monitor(
hook_config,
@ -181,15 +187,13 @@ def test_ping_monitor_config_itemid_no_auth_data_exit_early():
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
# 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
}
hook_config = {'server': SERVER, 'host': HOST, 'key': KEY}
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').never()
module.ping_monitor(
hook_config,
@ -200,15 +204,11 @@ def test_ping_monitor_config_host_and_key_no_auth_data_exit_early():
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
}
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,
@ -225,6 +225,37 @@ def test_ping_monitor_config_host_and_key_with_api_key_auth_data_successful():
dry_run=False,
)
def test_ping_monitor_config_host_and_missing_key_exits_early():
hook_config = {'server': SERVER, 'host': HOST, 'api_key': API_KEY}
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').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_key_and_missing_host_exits_early():
hook_config = {'server': SERVER, 'key': KEY, 'api_key': API_KEY}
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').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.
@ -233,11 +264,13 @@ def test_ping_monitor_config_host_and_key_with_username_password_auth_data_succe
'host': HOST,
'key': KEY,
'username': USERNAME,
'password': PASSWORD
'password': PASSWORD,
}
auth_response = flexmock(ok=True)
auth_response.should_receive('json').and_return({"jsonrpc":"2.0","result":"3fe6ed01a69ebd79907a120bcd04e494","id":1})
auth_response.should_receive('json').and_return(
{'jsonrpc': '2.0', 'result': '3fe6ed01a69ebd79907a120bcd04e494', 'id': 1}
)
flexmock(module.requests).should_receive('post').with_args(
f'{SERVER}',
@ -250,7 +283,7 @@ def test_ping_monitor_config_host_and_key_with_username_password_auth_data_succe
flexmock(module.requests).should_receive('post').with_args(
f'{SERVER}',
headers=AUTH_HEADERS_USERNAME_PASSWORD,
json=DATA_HOST_KEY_WITH_TOKEN,
json=DATA_HOST_KEY_WITH_KEY_VALUE,
).and_return(flexmock(ok=True)).once()
module.ping_monitor(
@ -262,14 +295,92 @@ def test_ping_monitor_config_host_and_key_with_username_password_auth_data_succe
dry_run=False,
)
def test_ping_monitor_config_host_and_key_with_username_password_auth_data_and_auth_post_error_exits_early():
hook_config = {
'server': SERVER,
'host': HOST,
'key': KEY,
'username': USERNAME,
'password': PASSWORD,
}
auth_response = flexmock(ok=False)
auth_response.should_receive('json').and_return(
{'jsonrpc': '2.0', 'result': '3fe6ed01a69ebd79907a120bcd04e494', 'id': 1}
)
auth_response.should_receive('raise_for_status').and_raise(
module.requests.ConnectionError
).once()
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').once()
flexmock(module.requests).should_receive('post').with_args(
f'{SERVER}',
headers=AUTH_HEADERS_USERNAME_PASSWORD,
json=DATA_HOST_KEY_WITH_KEY_VALUE,
).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_and_missing_password_exits_early():
hook_config = {
'server': SERVER,
'host': HOST,
'key': KEY,
'username': USERNAME,
}
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').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_passing_and_missing_username_exits_early():
hook_config = {
'server': SERVER,
'host': HOST,
'key': KEY,
'password': PASSWORD,
}
flexmock(module.logger).should_receive('warning').once()
flexmock(module.requests).should_receive('post').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_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
}
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,
@ -286,18 +397,16 @@ def test_ping_monitor_config_itemid_with_api_key_auth_data_successful():
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
}
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})
auth_response.should_receive('json').and_return(
{'jsonrpc': '2.0', 'result': '3fe6ed01a69ebd79907a120bcd04e494', 'id': 1}
)
flexmock(module.requests).should_receive('post').with_args(
f'{SERVER}',
@ -310,7 +419,7 @@ def test_ping_monitor_config_itemid_with_username_password_auth_data_successful(
flexmock(module.requests).should_receive('post').with_args(
f'{SERVER}',
headers=AUTH_HEADERS_USERNAME_PASSWORD,
json=DATA_HOST_KEY_WITH_TOKEN,
json=DATA_HOST_KEY_WITH_ITEMID,
).and_return(flexmock(ok=True)).once()
module.ping_monitor(
@ -321,4 +430,39 @@ def test_ping_monitor_config_itemid_with_username_password_auth_data_successful(
monitoring_log_level=1,
dry_run=False,
)
test_ping_monitor_config_itemid_with_username_password_auth_data_successful()
def test_ping_monitor_config_itemid_with_username_password_auth_data_and_push_post_error_exits_early():
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()
push_response = flexmock(ok=False)
push_response.should_receive('raise_for_status').and_raise(
module.requests.ConnectionError
).once()
flexmock(module.requests).should_receive('post').with_args(
f'{SERVER}',
headers=AUTH_HEADERS_USERNAME_PASSWORD,
json=DATA_HOST_KEY_WITH_ITEMID,
).and_return(push_response).once()
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,
)