Browse Source

In the Zabbix monitoring hook, support Zabbix 7.2's authentication changes (#1003).

Dan Helfman 3 months ago
parent
commit
dce0528057

+ 1 - 0
NEWS

@@ -1,5 +1,6 @@
 1.9.13.dev0
  * #1001: Fix a ZFS error during snapshot cleanup.
+ * #1003: In the Zabbix monitoring hook, support Zabbix 7.2's authentication changes.
  * Add a "verify_tls" option to the Uptime Kuma monitoring hook for disabling TLS verification.
 
 1.9.12

+ 4 - 1
borgmatic/config/schema.yaml

@@ -1907,6 +1907,8 @@ properties:
     zabbix:
         type: object
         additionalProperties: false
+        required:
+            - server
         properties:
             itemid:
                 type: integer
@@ -1929,7 +1931,8 @@ properties:
             server:
                 type: string
                 description: |
-                    The address of your Zabbix instance.
+                    The API endpoint URL of your Zabbix instance, usually ending
+                    with "/api_jsonrpc.php". Required.
                 example: https://zabbix.your-domain.com
             username:
                 type: string

+ 67 - 29
borgmatic/hooks/monitoring/zabbix.py

@@ -16,6 +16,42 @@ def initialize_monitor(
     pass
 
 
+def send_zabbix_request(server, headers, data):
+    '''
+    Given a Zabbix server URL, HTTP headers as a dict, and valid Zabbix JSON payload data as a dict,
+    send a request to the Zabbix server via API.
+
+    Return the response "result" value or None.
+    '''
+    logging.getLogger('urllib3').setLevel(logging.ERROR)
+
+    logger.debug(f'Sending a "{data["method"]}" request to the Zabbix server')
+
+    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'Zabbix error: {error}')
+
+        return None
+
+    try:
+        result = response.json().get('result')
+        error_message = result['data'][0]['error']
+    except requests.exceptions.JSONDecodeError:
+        logger.warning('Zabbix error: Cannot parse API response')
+
+        return None
+    except (TypeError, KeyError, IndexError):
+        return result
+    else:
+        logger.warning(f'Zabbix error: {error_message}')
+
+        return None
+
+
 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.
@@ -48,6 +84,7 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
         )
     except ValueError as error:
         logger.warning(f'Zabbix credential error: {error}')
+
         return
 
     server = hook_config.get('server')
@@ -57,13 +94,9 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
     value = state_config.get('value')
     headers = {'Content-Type': 'application/json-rpc'}
 
-    logger.info(f'Updating Zabbix{dry_run_label}')
+    logger.info(f'Pinging Zabbix{dry_run_label}')
     logger.debug(f'Using Zabbix URL: {server}')
 
-    if server is None:
-        logger.warning('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'Updating {itemid} on Zabbix')
@@ -74,8 +107,8 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
             'id': 1,
         }
 
-    elif (host and key) is not None:
-        logger.info(f'Updating Host:{host} and Key:{key} on Zabbix')
+    elif host is not None and key is not None:
+        logger.info(f'Updating Host: "{host}" and Key: "{key}" on Zabbix')
         data = {
             'jsonrpc': '2.0',
             'method': 'history.push',
@@ -85,58 +118,63 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
 
     elif host is not None:
         logger.warning('Key missing for Zabbix')
-        return
 
+        return
     elif key is not None:
         logger.warning('Host missing for Zabbix')
+
         return
     else:
         logger.warning('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('Using API key auth for Zabbix')
-        headers['Authorization'] = 'Bearer ' + api_key
-
-    elif (username and password) is not None:
-        logger.info('Using user/pass auth with user {username} for Zabbix')
-        auth_data = {
+        headers['Authorization'] = f'Bearer {api_key}'
+    elif username is not None and password is not None:
+        logger.info(f'Using user/pass auth with user {username} for Zabbix')
+        login_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'Zabbix error: {error}')
+            result = send_zabbix_request(server, headers, login_data)
+
+            if not result:
                 return
 
+            headers['Authorization'] = f'Bearer {result}'
     elif username is not None:
         logger.warning('Password missing for Zabbix authentication')
-        return
 
+        return
     elif password is not None:
         logger.warning('Username missing for Zabbix authentication')
+
         return
     else:
         logger.warning('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'Zabbix error: {error}')
+        send_zabbix_request(server, headers, data)
+
+    if username is not None and password is not None:
+        logout_data = {
+            'jsonrpc': '2.0',
+            'method': 'user.logout',
+            'params': [],
+            'id': 1,
+        }
+
+        if not dry_run:
+            send_zabbix_request(server, headers, logout_data)
 
 
 def destroy_monitor(ping_url_or_uuid, config, monitoring_log_level, dry_run):  # pragma: no cover

+ 4 - 2
docs/how-to/monitor-your-backups.md

@@ -710,8 +710,10 @@ zabbix:
         - fail
 ```
 
-This hook requires the Zabbix server be running version 7.0. ([Support for Zabbix
-7.2+](https://projects.torsion.org/borgmatic-collective/borgmatic/issues/1003) is planned.)
+This hook requires the Zabbix server be running version 7.0.
+
+<span class="minilink minilink-addedin">New in version 1.9.3</span> Zabbix 7.2+
+is supported as well.
 
 
 ### Authentication methods

+ 168 - 101
tests/unit/hooks/monitoring/test_zabbix.py

@@ -24,7 +24,6 @@ DATA_HOST_KEY_WITH_KEY_VALUE = {
     'method': 'history.push',
     'params': {'host': HOST, 'key': KEY, 'value': VALUE},
     'id': 1,
-    'auth': '3fe6ed01a69ebd79907a120bcd04e494',
 }
 
 DATA_ITEMID = {
@@ -39,7 +38,6 @@ DATA_HOST_KEY_WITH_ITEMID = {
     'method': 'history.push',
     'params': {'itemid': ITEMID, 'value': VALUE},
     'id': 1,
-    'auth': '3fe6ed01a69ebd79907a120bcd04e494',
 }
 
 DATA_USER_LOGIN = {
@@ -49,17 +47,99 @@ DATA_USER_LOGIN = {
     'id': 1,
 }
 
-AUTH_HEADERS_API_KEY = {
+DATA_USER_LOGOUT = {
+    'jsonrpc': '2.0',
+    'method': 'user.logout',
+    'params': [],
+    'id': 1,
+}
+
+AUTH_HEADERS_LOGIN = {
+    'Content-Type': 'application/json-rpc',
+}
+
+AUTH_HEADERS = {
     'Content-Type': 'application/json-rpc',
     'Authorization': f'Bearer {API_KEY}',
 }
 
-AUTH_HEADERS_USERNAME_PASSWORD = {'Content-Type': 'application/json-rpc'}
+
+def test_send_zabbix_request_with_post_error_bails():
+    server = flexmock()
+    headers = flexmock()
+    data = {'method': 'do.stuff'}
+    response = flexmock(ok=False)
+    response.should_receive('raise_for_status').and_raise(
+        module.requests.exceptions.RequestException
+    )
+
+    flexmock(module.requests).should_receive('post').with_args(
+        server, headers=headers, json=data
+    ).and_return(response)
+
+    assert module.send_zabbix_request(server, headers, data) is None
+
+
+def test_send_zabbix_request_with_invalid_json_response_bails():
+    server = flexmock()
+    headers = flexmock()
+    data = {'method': 'do.stuff'}
+    flexmock(module.requests.exceptions.JSONDecodeError).should_receive('__init__')
+    response = flexmock(ok=True)
+    response.should_receive('json').and_raise(module.requests.exceptions.JSONDecodeError)
+
+    flexmock(module.requests).should_receive('post').with_args(
+        server, headers=headers, json=data
+    ).and_return(response)
+
+    assert module.send_zabbix_request(server, headers, data) is None
+
+
+def test_send_zabbix_request_with_success_returns_response_result():
+    server = flexmock()
+    headers = flexmock()
+    data = {'method': 'do.stuff'}
+    response = flexmock(ok=True)
+    response.should_receive('json').and_return({'result': {'foo': 'bar'}})
+
+    flexmock(module.requests).should_receive('post').with_args(
+        server, headers=headers, json=data
+    ).and_return(response)
+
+    assert module.send_zabbix_request(server, headers, data) == {'foo': 'bar'}
+
+
+def test_send_zabbix_request_with_success_passes_through_missing_result():
+    server = flexmock()
+    headers = flexmock()
+    data = {'method': 'do.stuff'}
+    response = flexmock(ok=True)
+    response.should_receive('json').and_return({})
+
+    flexmock(module.requests).should_receive('post').with_args(
+        server, headers=headers, json=data
+    ).and_return(response)
+
+    assert module.send_zabbix_request(server, headers, data) is None
+
+
+def test_send_zabbix_request_with_error_bails():
+    server = flexmock()
+    headers = flexmock()
+    data = {'method': 'do.stuff'}
+    response = flexmock(ok=True)
+    response.should_receive('json').and_return({'result': {'data': [{'error': 'oops'}]}})
+
+    flexmock(module.requests).should_receive('post').with_args(
+        server, headers=headers, json=data
+    ).and_return(response)
+
+    assert module.send_zabbix_request(server, headers, data) is None
 
 
 def test_ping_monitor_with_non_matching_state_bails():
     hook_config = {'api_key': API_KEY}
-    flexmock(module.requests).should_receive('post').never()
+    flexmock(module).should_receive('send_zabbix_request').never()
 
     module.ping_monitor(
         hook_config,
@@ -79,7 +159,7 @@ def test_ping_monitor_config_with_api_key_only_bails():
         'resolve_credential'
     ).replace_with(lambda value, config: value)
     flexmock(module.logger).should_receive('warning').once()
-    flexmock(module.requests).should_receive('post').never()
+    flexmock(module).should_receive('send_zabbix_request').never()
 
     module.ping_monitor(
         hook_config,
@@ -99,7 +179,7 @@ def test_ping_monitor_config_with_host_only_bails():
         'resolve_credential'
     ).replace_with(lambda value, config: value)
     flexmock(module.logger).should_receive('warning').once()
-    flexmock(module.requests).should_receive('post').never()
+    flexmock(module).should_receive('send_zabbix_request').never()
 
     module.ping_monitor(
         hook_config,
@@ -119,7 +199,7 @@ def test_ping_monitor_config_with_key_only_bails():
         'resolve_credential'
     ).replace_with(lambda value, config: value)
     flexmock(module.logger).should_receive('warning').once()
-    flexmock(module.requests).should_receive('post').never()
+    flexmock(module).should_receive('send_zabbix_request').never()
 
     module.ping_monitor(
         hook_config,
@@ -139,7 +219,7 @@ def test_ping_monitor_config_with_server_only_bails():
         'resolve_credential'
     ).replace_with(lambda value, config: value)
     flexmock(module.logger).should_receive('warning').once()
-    flexmock(module.requests).should_receive('post').never()
+    flexmock(module).should_receive('send_zabbix_request').never()
 
     module.ping_monitor(
         hook_config,
@@ -158,7 +238,7 @@ def test_ping_monitor_config_user_password_no_zabbix_data_bails():
         'resolve_credential'
     ).replace_with(lambda value, config: value)
     flexmock(module.logger).should_receive('warning').once()
-    flexmock(module.requests).should_receive('post').never()
+    flexmock(module).should_receive('send_zabbix_request').never()
 
     module.ping_monitor(
         hook_config,
@@ -177,7 +257,7 @@ def test_ping_monitor_config_api_key_no_zabbix_data_bails():
         'resolve_credential'
     ).replace_with(lambda value, config: value)
     flexmock(module.logger).should_receive('warning').once()
-    flexmock(module.requests).should_receive('post').never()
+    flexmock(module).should_receive('send_zabbix_request').never()
 
     module.ping_monitor(
         hook_config,
@@ -197,7 +277,7 @@ def test_ping_monitor_config_itemid_no_auth_data_bails():
         'resolve_credential'
     ).replace_with(lambda value, config: value)
     flexmock(module.logger).should_receive('warning').once()
-    flexmock(module.requests).should_receive('post').never()
+    flexmock(module).should_receive('send_zabbix_request').never()
 
     module.ping_monitor(
         hook_config,
@@ -217,7 +297,7 @@ def test_ping_monitor_config_host_and_key_no_auth_data_bails():
         'resolve_credential'
     ).replace_with(lambda value, config: value)
     flexmock(module.logger).should_receive('warning').once()
-    flexmock(module.requests).should_receive('post').never()
+    flexmock(module).should_receive('send_zabbix_request').never()
 
     module.ping_monitor(
         hook_config,
@@ -236,11 +316,35 @@ def test_ping_monitor_config_host_and_key_with_api_key_auth_data_successful():
     flexmock(module.borgmatic.hooks.credential.parse).should_receive(
         'resolve_credential'
     ).replace_with(lambda value, config: value)
-    flexmock(module.requests).should_receive('post').with_args(
+    flexmock(module).should_receive('send_zabbix_request').with_args(
+        f'{SERVER}',
+        headers=AUTH_HEADERS,
+        data=DATA_HOST_KEY,
+    ).once()
+    flexmock(module.logger).should_receive('warning').never()
+
+    module.ping_monitor(
+        hook_config,
+        {},
+        'config.yaml',
+        borgmatic.hooks.monitoring.monitor.State.FAIL,
+        monitoring_log_level=1,
+        dry_run=False,
+    )
+
+
+def test_ping_monitor_config_adds_missing_api_endpoint_to_server_url():
+    # 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.borgmatic.hooks.credential.parse).should_receive(
+        'resolve_credential'
+    ).replace_with(lambda value, config: value)
+    flexmock(module).should_receive('send_zabbix_request').with_args(
         f'{SERVER}',
-        headers=AUTH_HEADERS_API_KEY,
-        json=DATA_HOST_KEY,
-    ).and_return(flexmock(ok=True)).once()
+        headers=AUTH_HEADERS,
+        data=DATA_HOST_KEY,
+    ).once()
     flexmock(module.logger).should_receive('warning').never()
 
     module.ping_monitor(
@@ -259,7 +363,7 @@ def test_ping_monitor_config_host_and_missing_key_bails():
         'resolve_credential'
     ).replace_with(lambda value, config: value)
     flexmock(module.logger).should_receive('warning').once()
-    flexmock(module.requests).should_receive('post').never()
+    flexmock(module).should_receive('send_zabbix_request').never()
 
     module.ping_monitor(
         hook_config,
@@ -277,7 +381,7 @@ def test_ping_monitor_config_key_and_missing_host_bails():
         'resolve_credential'
     ).replace_with(lambda value, config: value)
     flexmock(module.logger).should_receive('warning').once()
-    flexmock(module.requests).should_receive('post').never()
+    flexmock(module).should_receive('send_zabbix_request').never()
 
     module.ping_monitor(
         hook_config,
@@ -303,24 +407,26 @@ def test_ping_monitor_config_host_and_key_with_username_password_auth_data_succe
     flexmock(module.borgmatic.hooks.credential.parse).should_receive(
         'resolve_credential'
     ).replace_with(lambda value, config: value)
-    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(
+    flexmock(module).should_receive('send_zabbix_request').with_args(
         f'{SERVER}',
-        headers=AUTH_HEADERS_USERNAME_PASSWORD,
-        json=DATA_USER_LOGIN,
-    ).and_return(auth_response).once()
+        headers=AUTH_HEADERS_LOGIN,
+        data=DATA_USER_LOGIN,
+    ).and_return('fakekey').once()
 
     flexmock(module.logger).should_receive('warning').never()
 
-    flexmock(module.requests).should_receive('post').with_args(
+    flexmock(module).should_receive('send_zabbix_request').with_args(
+        f'{SERVER}',
+        headers=AUTH_HEADERS,
+        data=DATA_HOST_KEY_WITH_KEY_VALUE,
+    ).once()
+
+    flexmock(module).should_receive('send_zabbix_request').with_args(
         f'{SERVER}',
-        headers=AUTH_HEADERS_USERNAME_PASSWORD,
-        json=DATA_HOST_KEY_WITH_KEY_VALUE,
-    ).and_return(flexmock(ok=True)).once()
+        headers=AUTH_HEADERS,
+        data=DATA_USER_LOGOUT,
+    ).once()
 
     module.ping_monitor(
         hook_config,
@@ -344,24 +450,22 @@ def test_ping_monitor_config_host_and_key_with_username_password_auth_data_and_a
     flexmock(module.borgmatic.hooks.credential.parse).should_receive(
         'resolve_credential'
     ).replace_with(lambda value, config: value)
-    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(
+    flexmock(module).should_receive('send_zabbix_request').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(
+        headers=AUTH_HEADERS_LOGIN,
+        data=DATA_USER_LOGIN,
+    ).and_return(None).once()
+    flexmock(module).should_receive('send_zabbix_request').with_args(
+        f'{SERVER}',
+        headers=AUTH_HEADERS,
+        data=DATA_HOST_KEY_WITH_KEY_VALUE,
+    ).never()
+
+    flexmock(module).should_receive('send_zabbix_request').with_args(
         f'{SERVER}',
-        headers=AUTH_HEADERS_USERNAME_PASSWORD,
-        json=DATA_HOST_KEY_WITH_KEY_VALUE,
+        headers=AUTH_HEADERS,
+        data=DATA_USER_LOGOUT,
     ).never()
 
     module.ping_monitor(
@@ -386,7 +490,7 @@ def test_ping_monitor_config_host_and_key_with_username_and_missing_password_bai
         'resolve_credential'
     ).replace_with(lambda value, config: value)
     flexmock(module.logger).should_receive('warning').once()
-    flexmock(module.requests).should_receive('post').never()
+    flexmock(module).should_receive('send_zabbix_request').never()
 
     module.ping_monitor(
         hook_config,
@@ -410,7 +514,7 @@ def test_ping_monitor_config_host_and_key_with_password_and_missing_username_bai
         'resolve_credential'
     ).replace_with(lambda value, config: value)
     flexmock(module.logger).should_receive('warning').once()
-    flexmock(module.requests).should_receive('post').never()
+    flexmock(module).should_receive('send_zabbix_request').never()
 
     module.ping_monitor(
         hook_config,
@@ -429,11 +533,11 @@ def test_ping_monitor_config_itemid_with_api_key_auth_data_successful():
     flexmock(module.borgmatic.hooks.credential.parse).should_receive(
         'resolve_credential'
     ).replace_with(lambda value, config: value)
-    flexmock(module.requests).should_receive('post').with_args(
+    flexmock(module).should_receive('send_zabbix_request').with_args(
         f'{SERVER}',
-        headers=AUTH_HEADERS_API_KEY,
-        json=DATA_ITEMID,
-    ).and_return(flexmock(ok=True)).once()
+        headers=AUTH_HEADERS,
+        data=DATA_ITEMID,
+    ).once()
     flexmock(module.logger).should_receive('warning').never()
 
     module.ping_monitor(
@@ -454,63 +558,26 @@ def test_ping_monitor_config_itemid_with_username_password_auth_data_successful(
     flexmock(module.borgmatic.hooks.credential.parse).should_receive(
         'resolve_credential'
     ).replace_with(lambda value, config: value)
-    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(
+    flexmock(module).should_receive('send_zabbix_request').with_args(
         f'{SERVER}',
-        headers=AUTH_HEADERS_USERNAME_PASSWORD,
-        json=DATA_USER_LOGIN,
-    ).and_return(auth_response).once()
+        headers=AUTH_HEADERS_LOGIN,
+        data=DATA_USER_LOGIN,
+    ).and_return('fakekey').once()
 
     flexmock(module.logger).should_receive('warning').never()
 
-    flexmock(module.requests).should_receive('post').with_args(
+    flexmock(module).should_receive('send_zabbix_request').with_args(
         f'{SERVER}',
-        headers=AUTH_HEADERS_USERNAME_PASSWORD,
-        json=DATA_HOST_KEY_WITH_ITEMID,
-    ).and_return(flexmock(ok=True)).once()
-
-    module.ping_monitor(
-        hook_config,
-        {},
-        'config.yaml',
-        borgmatic.hooks.monitoring.monitor.State.FAIL,
-        monitoring_log_level=1,
-        dry_run=False,
-    )
-
-
-def test_ping_monitor_config_itemid_with_username_password_auth_data_and_push_post_error_bails():
-    hook_config = {'server': SERVER, 'itemid': ITEMID, 'username': USERNAME, 'password': PASSWORD}
-
-    flexmock(module.borgmatic.hooks.credential.parse).should_receive(
-        'resolve_credential'
-    ).replace_with(lambda value, config: value)
-    auth_response = flexmock(ok=True)
-    auth_response.should_receive('json').and_return(
-        {'jsonrpc': '2.0', 'result': '3fe6ed01a69ebd79907a120bcd04e494', 'id': 1}
-    )
+        headers=AUTH_HEADERS,
+        data=DATA_HOST_KEY_WITH_ITEMID,
+    ).once()
 
-    flexmock(module.requests).should_receive('post').with_args(
+    flexmock(module).should_receive('send_zabbix_request').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
+        headers=AUTH_HEADERS,
+        data=DATA_USER_LOGOUT,
     ).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,
@@ -528,7 +595,7 @@ def test_ping_monitor_with_credential_error_bails():
     flexmock(module.borgmatic.hooks.credential.parse).should_receive(
         'resolve_credential'
     ).and_raise(ValueError)
-    flexmock(module.requests).should_receive('post').never()
+    flexmock(module).should_receive('send_zabbix_request').never()
     flexmock(module.logger).should_receive('warning').once()
 
     module.ping_monitor(