Bläddra i källkod

Flesh out the Zabbix monitoring hook tests, add a logo to the documentation, etc.

Dan Helfman 7 månader sedan
förälder
incheckning
129f3e753c
7 ändrade filer med 281 tillägg och 128 borttagningar
  1. 2 0
      NEWS
  2. 1 0
      README.md
  3. 10 5
      borgmatic/config/schema.yaml
  4. 14 18
      borgmatic/hooks/zabbix.py
  5. 21 16
      docs/how-to/monitor-your-backups.md
  6. BIN
      docs/static/zabbix.png
  7. 233 89
      tests/unit/hooks/test_zabbix.py

+ 2 - 0
NEWS

@@ -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.

+ 1 - 0
README.md

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

+ 10 - 5
borgmatic/config/schema.yaml

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

+ 14 - 18
borgmatic/hooks/zabbix.py

@@ -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)

+ 21 - 16
docs/how-to/monitor-your-backups.md

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


+ 233 - 89
tests/unit/hooks/test_zabbix.py

@@ -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_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.
+
+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,
-        'itemid': ITEMID,
-        'api_key': API_KEY
+        '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}
     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,
+    )