Browse Source

When making HTTP requests in monitoring hooks, set "borgmatic" as the user agent (#1139).

Dan Helfman 23 hours ago
parent
commit
563cb8441e

+ 1 - 0
NEWS

@@ -4,6 +4,7 @@
    use.
    use.
  * #1126: Create LVM snapshots as read-write to avoid an error when snapshotting ext4 filesystems
  * #1126: Create LVM snapshots as read-write to avoid an error when snapshotting ext4 filesystems
    with orphaned files that need recovery.
    with orphaned files that need recovery.
+ * #1139: When making HTTP requests in monitoring hooks, set "borgmatic" as the user agent.
  * When running tests, use Ruff for faster and more comprehensive code linting and formatting,
  * When running tests, use Ruff for faster and more comprehensive code linting and formatting,
    replacing Flake8, Black, isort, etc.
    replacing Flake8, Black, isort, etc.
  * Switch from pipx to uv for installing development tools, and added tox-uv for speeding up test
  * Switch from pipx to uv for installing development tools, and added tox-uv for speeding up test

+ 5 - 1
borgmatic/hooks/monitoring/cronhub.py

@@ -49,7 +49,11 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
     if not dry_run:
     if not dry_run:
         logging.getLogger('urllib3').setLevel(logging.ERROR)
         logging.getLogger('urllib3').setLevel(logging.ERROR)
         try:
         try:
-            response = requests.get(ping_url, timeout=TIMEOUT_SECONDS)
+            response = requests.get(
+                ping_url,
+                timeout=TIMEOUT_SECONDS,
+                headers={'User-Agent': 'borgmatic'},
+            )
             if not response.ok:
             if not response.ok:
                 response.raise_for_status()
                 response.raise_for_status()
         except requests.exceptions.RequestException as error:
         except requests.exceptions.RequestException as error:

+ 5 - 1
borgmatic/hooks/monitoring/cronitor.py

@@ -44,7 +44,11 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
     if not dry_run:
     if not dry_run:
         logging.getLogger('urllib3').setLevel(logging.ERROR)
         logging.getLogger('urllib3').setLevel(logging.ERROR)
         try:
         try:
-            response = requests.get(ping_url, timeout=TIMEOUT_SECONDS)
+            response = requests.get(
+                ping_url,
+                timeout=TIMEOUT_SECONDS,
+                headers={'User-Agent': 'borgmatic'},
+            )
             if not response.ok:
             if not response.ok:
                 response.raise_for_status()
                 response.raise_for_status()
         except requests.exceptions.RequestException as error:
         except requests.exceptions.RequestException as error:

+ 1 - 0
borgmatic/hooks/monitoring/healthchecks.py

@@ -91,6 +91,7 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
                 data=payload.encode('utf-8'),
                 data=payload.encode('utf-8'),
                 verify=hook_config.get('verify_tls', True),
                 verify=hook_config.get('verify_tls', True),
                 timeout=TIMEOUT_SECONDS,
                 timeout=TIMEOUT_SECONDS,
+                headers={'User-Agent': 'borgmatic'},
             )
             )
             if not response.ok:
             if not response.ok:
                 response.raise_for_status()
                 response.raise_for_status()

+ 5 - 4
borgmatic/hooks/monitoring/loki.py

@@ -65,16 +65,17 @@ class Loki_log_buffer:
             # Skip as there are not logs to send yet
             # Skip as there are not logs to send yet
             return
             return
 
 
-        request_body = self.to_request()
         self.root['streams'][0]['values'] = []
         self.root['streams'][0]['values'] = []
-        request_header = {'Content-Type': 'application/json'}
 
 
         try:
         try:
             result = requests.post(
             result = requests.post(
                 self.url,
                 self.url,
-                headers=request_header,
-                data=request_body,
+                data=self.to_request(),
                 timeout=TIMEOUT_SECONDS,
                 timeout=TIMEOUT_SECONDS,
+                headers={
+                    'Content-Type': 'application/json',
+                    'User-Agent': 'borgmatic',
+                },
             )
             )
             result.raise_for_status()
             result.raise_for_status()
         except requests.RequestException:
         except requests.RequestException:

+ 2 - 1
borgmatic/hooks/monitoring/ntfy.py

@@ -49,6 +49,7 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
         logger.debug(f'Using Ntfy ping URL {base_url}/{topic}')
         logger.debug(f'Using Ntfy ping URL {base_url}/{topic}')
 
 
         headers = {
         headers = {
+            'User-Agent': 'borgmatic',
             'X-Title': state_config.get('title'),
             'X-Title': state_config.get('title'),
             'X-Message': state_config.get('message'),
             'X-Message': state_config.get('message'),
             'X-Priority': state_config.get('priority'),
             'X-Priority': state_config.get('priority'),
@@ -94,9 +95,9 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
             try:
             try:
                 response = requests.post(
                 response = requests.post(
                     f'{base_url}/{topic}',
                     f'{base_url}/{topic}',
-                    headers=headers,
                     auth=auth,
                     auth=auth,
                     timeout=TIMEOUT_SECONDS,
                     timeout=TIMEOUT_SECONDS,
+                    headers=headers,
                 )
                 )
                 if not response.ok:
                 if not response.ok:
                     response.raise_for_status()
                     response.raise_for_status()

+ 1 - 0
borgmatic/hooks/monitoring/pagerduty.py

@@ -102,6 +102,7 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
             EVENTS_API_URL,
             EVENTS_API_URL,
             data=payload.encode('utf-8'),
             data=payload.encode('utf-8'),
             timeout=TIMEOUT_SECONDS,
             timeout=TIMEOUT_SECONDS,
+            headers={'User-Agent': 'borgmatic'},
         )
         )
         if not response.ok:
         if not response.ok:
             response.raise_for_status()
             response.raise_for_status()

+ 4 - 1
borgmatic/hooks/monitoring/pushover.py

@@ -82,9 +82,12 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
         try:
         try:
             response = requests.post(
             response = requests.post(
                 'https://api.pushover.net/1/messages.json',
                 'https://api.pushover.net/1/messages.json',
-                headers={'Content-type': 'application/x-www-form-urlencoded'},
                 data=data,
                 data=data,
                 timeout=TIMEOUT_SECONDS,
                 timeout=TIMEOUT_SECONDS,
+                headers={
+                    'Content-type': 'application/x-www-form-urlencoded',
+                    'User-Agent': 'borgmatic',
+                },
             )
             )
             if not response.ok:
             if not response.ok:
                 response.raise_for_status()
                 response.raise_for_status()

+ 5 - 1
borgmatic/hooks/monitoring/sentry.py

@@ -67,7 +67,11 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
 
 
     logging.getLogger('urllib3').setLevel(logging.ERROR)
     logging.getLogger('urllib3').setLevel(logging.ERROR)
     try:
     try:
-        response = requests.post(f'{cron_url}?status={status}', timeout=TIMEOUT_SECONDS)
+        response = requests.post(
+            f'{cron_url}?status={status}',
+            timeout=TIMEOUT_SECONDS,
+            headers={'User-Agent': 'borgmatic'},
+        )
         if not response.ok:
         if not response.ok:
             response.raise_for_status()
             response.raise_for_status()
     except requests.exceptions.RequestException as error:
     except requests.exceptions.RequestException as error:

+ 1 - 0
borgmatic/hooks/monitoring/uptime_kuma.py

@@ -47,6 +47,7 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
             f'{push_url}?{query}',
             f'{push_url}?{query}',
             verify=hook_config.get('verify_tls', True),
             verify=hook_config.get('verify_tls', True),
             timeout=TIMEOUT_SECONDS,
             timeout=TIMEOUT_SECONDS,
+            headers={'User-Agent': 'borgmatic'},
         )
         )
         if not response.ok:
         if not response.ok:
             response.raise_for_status()
             response.raise_for_status()

+ 1 - 1
borgmatic/hooks/monitoring/zabbix.py

@@ -101,7 +101,7 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
     host = hook_config.get('host')
     host = hook_config.get('host')
     key = hook_config.get('key')
     key = hook_config.get('key')
     value = state_config.get('value')
     value = state_config.get('value')
-    headers = {'Content-Type': 'application/json-rpc'}
+    headers = {'Content-Type': 'application/json-rpc', 'User-Agent': 'borgmatic'}
 
 
     logger.info(f'Pinging Zabbix{dry_run_label}')
     logger.info(f'Pinging Zabbix{dry_run_label}')
     logger.debug(f'Using Zabbix URL: {server}')
     logger.debug(f'Using Zabbix URL: {server}')

+ 5 - 0
tests/unit/hooks/monitoring/test_cronhub.py

@@ -8,6 +8,7 @@ def test_ping_monitor_rewrites_ping_url_for_start_state():
     flexmock(module.requests).should_receive('get').with_args(
     flexmock(module.requests).should_receive('get').with_args(
         'https://example.com/start/abcdef',
         'https://example.com/start/abcdef',
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(flexmock(ok=True))
     ).and_return(flexmock(ok=True))
 
 
     module.ping_monitor(
     module.ping_monitor(
@@ -25,6 +26,7 @@ def test_ping_monitor_rewrites_ping_url_and_state_for_start_state():
     flexmock(module.requests).should_receive('get').with_args(
     flexmock(module.requests).should_receive('get').with_args(
         'https://example.com/start/abcdef',
         'https://example.com/start/abcdef',
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(flexmock(ok=True))
     ).and_return(flexmock(ok=True))
 
 
     module.ping_monitor(
     module.ping_monitor(
@@ -42,6 +44,7 @@ def test_ping_monitor_rewrites_ping_url_for_finish_state():
     flexmock(module.requests).should_receive('get').with_args(
     flexmock(module.requests).should_receive('get').with_args(
         'https://example.com/finish/abcdef',
         'https://example.com/finish/abcdef',
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(flexmock(ok=True))
     ).and_return(flexmock(ok=True))
 
 
     module.ping_monitor(
     module.ping_monitor(
@@ -59,6 +62,7 @@ def test_ping_monitor_rewrites_ping_url_for_fail_state():
     flexmock(module.requests).should_receive('get').with_args(
     flexmock(module.requests).should_receive('get').with_args(
         'https://example.com/fail/abcdef',
         'https://example.com/fail/abcdef',
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(flexmock(ok=True))
     ).and_return(flexmock(ok=True))
 
 
     module.ping_monitor(
     module.ping_monitor(
@@ -111,6 +115,7 @@ def test_ping_monitor_with_other_error_logs_warning():
     flexmock(module.requests).should_receive('get').with_args(
     flexmock(module.requests).should_receive('get').with_args(
         'https://example.com/start/abcdef',
         'https://example.com/start/abcdef',
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(response)
     ).and_return(response)
     flexmock(module.logger).should_receive('warning').once()
     flexmock(module.logger).should_receive('warning').once()
 
 

+ 4 - 0
tests/unit/hooks/monitoring/test_cronitor.py

@@ -8,6 +8,7 @@ def test_ping_monitor_hits_ping_url_for_start_state():
     flexmock(module.requests).should_receive('get').with_args(
     flexmock(module.requests).should_receive('get').with_args(
         'https://example.com/run',
         'https://example.com/run',
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(flexmock(ok=True))
     ).and_return(flexmock(ok=True))
 
 
     module.ping_monitor(
     module.ping_monitor(
@@ -25,6 +26,7 @@ def test_ping_monitor_hits_ping_url_for_finish_state():
     flexmock(module.requests).should_receive('get').with_args(
     flexmock(module.requests).should_receive('get').with_args(
         'https://example.com/complete',
         'https://example.com/complete',
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(flexmock(ok=True))
     ).and_return(flexmock(ok=True))
 
 
     module.ping_monitor(
     module.ping_monitor(
@@ -42,6 +44,7 @@ def test_ping_monitor_hits_ping_url_for_fail_state():
     flexmock(module.requests).should_receive('get').with_args(
     flexmock(module.requests).should_receive('get').with_args(
         'https://example.com/fail',
         'https://example.com/fail',
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(flexmock(ok=True))
     ).and_return(flexmock(ok=True))
 
 
     module.ping_monitor(
     module.ping_monitor(
@@ -94,6 +97,7 @@ def test_ping_monitor_with_other_error_logs_warning():
     flexmock(module.requests).should_receive('get').with_args(
     flexmock(module.requests).should_receive('get').with_args(
         'https://example.com/run',
         'https://example.com/run',
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(response)
     ).and_return(response)
     flexmock(module.logger).should_receive('warning').once()
     flexmock(module.logger).should_receive('warning').once()
 
 

+ 13 - 0
tests/unit/hooks/monitoring/test_healthchecks.py

@@ -99,6 +99,7 @@ def test_ping_monitor_hits_ping_url_for_start_state():
         data=b'',
         data=b'',
         verify=True,
         verify=True,
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(flexmock(ok=True))
     ).and_return(flexmock(ok=True))
 
 
     module.ping_monitor(
     module.ping_monitor(
@@ -123,6 +124,7 @@ def test_ping_monitor_hits_ping_url_for_finish_state():
         data=payload.encode('utf-8'),
         data=payload.encode('utf-8'),
         verify=True,
         verify=True,
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(flexmock(ok=True))
     ).and_return(flexmock(ok=True))
 
 
     module.ping_monitor(
     module.ping_monitor(
@@ -147,6 +149,7 @@ def test_ping_monitor_hits_ping_url_for_fail_state():
         data=payload.encode('utf'),
         data=payload.encode('utf'),
         verify=True,
         verify=True,
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(flexmock(ok=True))
     ).and_return(flexmock(ok=True))
 
 
     module.ping_monitor(
     module.ping_monitor(
@@ -171,6 +174,7 @@ def test_ping_monitor_hits_ping_url_for_log_state():
         data=payload.encode('utf'),
         data=payload.encode('utf'),
         verify=True,
         verify=True,
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(flexmock(ok=True))
     ).and_return(flexmock(ok=True))
 
 
     module.ping_monitor(
     module.ping_monitor(
@@ -195,6 +199,7 @@ def test_ping_monitor_with_ping_uuid_hits_corresponding_url():
         data=payload.encode('utf-8'),
         data=payload.encode('utf-8'),
         verify=True,
         verify=True,
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(flexmock(ok=True))
     ).and_return(flexmock(ok=True))
 
 
     module.ping_monitor(
     module.ping_monitor(
@@ -219,6 +224,7 @@ def test_ping_monitor_skips_ssl_verification_when_verify_tls_false():
         data=payload.encode('utf-8'),
         data=payload.encode('utf-8'),
         verify=False,
         verify=False,
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(flexmock(ok=True))
     ).and_return(flexmock(ok=True))
 
 
     module.ping_monitor(
     module.ping_monitor(
@@ -243,6 +249,7 @@ def test_ping_monitor_executes_ssl_verification_when_verify_tls_true():
         data=payload.encode('utf-8'),
         data=payload.encode('utf-8'),
         verify=True,
         verify=True,
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(flexmock(ok=True))
     ).and_return(flexmock(ok=True))
 
 
     module.ping_monitor(
     module.ping_monitor(
@@ -299,6 +306,7 @@ def test_ping_monitor_hits_ping_url_when_states_matching():
         data=b'',
         data=b'',
         verify=True,
         verify=True,
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(flexmock(ok=True))
     ).and_return(flexmock(ok=True))
 
 
     module.ping_monitor(
     module.ping_monitor(
@@ -321,6 +329,7 @@ def test_ping_monitor_adds_create_query_parameter_when_create_slug_true():
         data=b'',
         data=b'',
         verify=True,
         verify=True,
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(flexmock(ok=True))
     ).and_return(flexmock(ok=True))
 
 
     module.ping_monitor(
     module.ping_monitor(
@@ -343,6 +352,7 @@ def test_ping_monitor_does_not_add_create_query_parameter_when_create_slug_false
         data=b'',
         data=b'',
         verify=True,
         verify=True,
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(flexmock(ok=True))
     ).and_return(flexmock(ok=True))
 
 
     module.ping_monitor(
     module.ping_monitor(
@@ -362,6 +372,7 @@ def test_ping_monitor_does_not_add_create_query_parameter_when_ping_url_is_uuid(
         data=b'',
         data=b'',
         verify=True,
         verify=True,
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(flexmock(ok=True))
     ).and_return(flexmock(ok=True))
 
 
     module.ping_monitor(
     module.ping_monitor(
@@ -401,6 +412,7 @@ def test_ping_monitor_with_connection_error_logs_warning():
         data=b'',
         data=b'',
         verify=True,
         verify=True,
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_raise(module.requests.exceptions.ConnectionError)
     ).and_raise(module.requests.exceptions.ConnectionError)
     flexmock(module.logger).should_receive('warning').once()
     flexmock(module.logger).should_receive('warning').once()
 
 
@@ -428,6 +440,7 @@ def test_ping_monitor_with_other_error_logs_warning():
         data=b'',
         data=b'',
         verify=True,
         verify=True,
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(response)
     ).and_return(response)
     flexmock(module.logger).should_receive('warning').once()
     flexmock(module.logger).should_receive('warning').once()
 
 

+ 2 - 0
tests/unit/hooks/monitoring/test_ntfy.py

@@ -17,6 +17,7 @@ custom_message_config = {
 }
 }
 
 
 custom_message_headers = {
 custom_message_headers = {
+    'User-Agent': 'borgmatic',
     'X-Title': custom_message_config['title'],
     'X-Title': custom_message_config['title'],
     'X-Message': custom_message_config['message'],
     'X-Message': custom_message_config['message'],
     'X-Priority': custom_message_config['priority'],
     'X-Priority': custom_message_config['priority'],
@@ -26,6 +27,7 @@ custom_message_headers = {
 
 
 def return_default_message_headers(state=Enum):
 def return_default_message_headers(state=Enum):
     return {
     return {
+        'User-Agent': 'borgmatic',
         'X-Title': f'A borgmatic {state.name} event happened',
         'X-Title': f'A borgmatic {state.name} event happened',
         'X-Message': f'A borgmatic {state.name} event happened',
         'X-Message': f'A borgmatic {state.name} event happened',
         'X-Priority': 'default',
         'X-Priority': 'default',

+ 10 - 10
tests/unit/hooks/monitoring/test_pushover.py

@@ -17,7 +17,7 @@ def test_ping_monitor_config_with_minimum_config_fail_state_backup_successfully_
     flexmock(module.logger).should_receive('warning').never()
     flexmock(module.logger).should_receive('warning').never()
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
         'https://api.pushover.net/1/messages.json',
         'https://api.pushover.net/1/messages.json',
-        headers={'Content-type': 'application/x-www-form-urlencoded'},
+        headers={'Content-type': 'application/x-www-form-urlencoded', 'User-Agent': 'borgmatic'},
         data={
         data={
             'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
             'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
             'user': '983hfe0of902lkjfa2amanfgui',
             'user': '983hfe0of902lkjfa2amanfgui',
@@ -76,7 +76,7 @@ def test_ping_monitor_start_state_backup_default_message_successfully_send_to_pu
     flexmock(module.logger).should_receive('warning').never()
     flexmock(module.logger).should_receive('warning').never()
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
         'https://api.pushover.net/1/messages.json',
         'https://api.pushover.net/1/messages.json',
-        headers={'Content-type': 'application/x-www-form-urlencoded'},
+        headers={'Content-type': 'application/x-www-form-urlencoded', 'User-Agent': 'borgmatic'},
         data={
         data={
             'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
             'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
             'user': '983hfe0of902lkjfa2amanfgui',
             'user': '983hfe0of902lkjfa2amanfgui',
@@ -113,7 +113,7 @@ def test_ping_monitor_start_state_backup_custom_message_successfully_send_to_pus
     flexmock(module.logger).should_receive('warning').never()
     flexmock(module.logger).should_receive('warning').never()
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
         'https://api.pushover.net/1/messages.json',
         'https://api.pushover.net/1/messages.json',
-        headers={'Content-type': 'application/x-www-form-urlencoded'},
+        headers={'Content-type': 'application/x-www-form-urlencoded', 'User-Agent': 'borgmatic'},
         data={
         data={
             'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
             'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
             'user': '983hfe0of902lkjfa2amanfgui',
             'user': '983hfe0of902lkjfa2amanfgui',
@@ -149,7 +149,7 @@ def test_ping_monitor_start_state_backup_default_message_with_priority_emergency
     flexmock(module.logger).should_receive('warning').never()
     flexmock(module.logger).should_receive('warning').never()
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
         'https://api.pushover.net/1/messages.json',
         'https://api.pushover.net/1/messages.json',
-        headers={'Content-type': 'application/x-www-form-urlencoded'},
+        headers={'Content-type': 'application/x-www-form-urlencoded', 'User-Agent': 'borgmatic'},
         data={
         data={
             'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
             'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
             'user': '983hfe0of902lkjfa2amanfgui',
             'user': '983hfe0of902lkjfa2amanfgui',
@@ -188,7 +188,7 @@ def test_ping_monitor_start_state_backup_default_message_with_priority_emergency
     flexmock(module.logger).should_receive('warning').never()
     flexmock(module.logger).should_receive('warning').never()
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
         'https://api.pushover.net/1/messages.json',
         'https://api.pushover.net/1/messages.json',
-        headers={'Content-type': 'application/x-www-form-urlencoded'},
+        headers={'Content-type': 'application/x-www-form-urlencoded', 'User-Agent': 'borgmatic'},
         data={
         data={
             'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
             'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
             'user': '983hfe0of902lkjfa2amanfgui',
             'user': '983hfe0of902lkjfa2amanfgui',
@@ -227,7 +227,7 @@ def test_ping_monitor_start_state_backup_default_message_with_priority_emergency
     flexmock(module.logger).should_receive('warning').never()
     flexmock(module.logger).should_receive('warning').never()
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
         'https://api.pushover.net/1/messages.json',
         'https://api.pushover.net/1/messages.json',
-        headers={'Content-type': 'application/x-www-form-urlencoded'},
+        headers={'Content-type': 'application/x-www-form-urlencoded', 'User-Agent': 'borgmatic'},
         data={
         data={
             'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
             'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
             'user': '983hfe0of902lkjfa2amanfgui',
             'user': '983hfe0of902lkjfa2amanfgui',
@@ -324,7 +324,7 @@ def test_ping_monitor_start_state_backup_based_on_documentation_advanced_example
     flexmock(module.logger).should_receive('warning').never()
     flexmock(module.logger).should_receive('warning').never()
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
         'https://api.pushover.net/1/messages.json',
         'https://api.pushover.net/1/messages.json',
-        headers={'Content-type': 'application/x-www-form-urlencoded'},
+        headers={'Content-type': 'application/x-www-form-urlencoded', 'User-Agent': 'borgmatic'},
         data={
         data={
             'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
             'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
             'user': '983hfe0of902lkjfa2amanfgui',
             'user': '983hfe0of902lkjfa2amanfgui',
@@ -391,7 +391,7 @@ def test_ping_monitor_fail_state_backup_based_on_documentation_advanced_example_
     flexmock(module.logger).should_receive('warning').never()
     flexmock(module.logger).should_receive('warning').never()
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
         'https://api.pushover.net/1/messages.json',
         'https://api.pushover.net/1/messages.json',
-        headers={'Content-type': 'application/x-www-form-urlencoded'},
+        headers={'Content-type': 'application/x-www-form-urlencoded', 'User-Agent': 'borgmatic'},
         data={
         data={
             'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
             'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
             'user': '983hfe0of902lkjfa2amanfgui',
             'user': '983hfe0of902lkjfa2amanfgui',
@@ -463,7 +463,7 @@ def test_ping_monitor_finish_state_backup_based_on_documentation_advanced_exampl
     flexmock(module.logger).should_receive('warning').never()
     flexmock(module.logger).should_receive('warning').never()
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
         'https://api.pushover.net/1/messages.json',
         'https://api.pushover.net/1/messages.json',
-        headers={'Content-type': 'application/x-www-form-urlencoded'},
+        headers={'Content-type': 'application/x-www-form-urlencoded', 'User-Agent': 'borgmatic'},
         data={
         data={
             'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
             'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
             'user': '983hfe0of902lkjfa2amanfgui',
             'user': '983hfe0of902lkjfa2amanfgui',
@@ -553,7 +553,7 @@ def test_ping_monitor_push_post_error_bails():
     ).once()
     ).once()
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
         'https://api.pushover.net/1/messages.json',
         'https://api.pushover.net/1/messages.json',
-        headers={'Content-type': 'application/x-www-form-urlencoded'},
+        headers={'Content-type': 'application/x-www-form-urlencoded', 'User-Agent': 'borgmatic'},
         data={
         data={
             'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
             'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
             'user': '983hfe0of902lkjfa2amanfgui',
             'user': '983hfe0of902lkjfa2amanfgui',

+ 2 - 0
tests/unit/hooks/monitoring/test_sentry.py

@@ -31,6 +31,7 @@ def test_ping_monitor_constructs_cron_url_and_pings_it(state, configured_states,
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
         f'https://o294220.ingest.us.sentry.io/api/203069/cron/test/5f80ec/?status={expected_status}',
         f'https://o294220.ingest.us.sentry.io/api/203069/cron/test/5f80ec/?status={expected_status}',
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(flexmock(ok=True)).once()
     ).and_return(flexmock(ok=True)).once()
 
 
     module.ping_monitor(
     module.ping_monitor(
@@ -137,6 +138,7 @@ def test_ping_monitor_with_network_error_does_not_raise():
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
         'https://o294220.ingest.us.sentry.io/api/203069/cron/test/5f80ec/?status=in_progress',
         'https://o294220.ingest.us.sentry.io/api/203069/cron/test/5f80ec/?status=in_progress',
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(response).once()
     ).and_return(response).once()
 
 
     module.ping_monitor(
     module.ping_monitor(

+ 8 - 0
tests/unit/hooks/monitoring/test_uptimekuma.py

@@ -13,6 +13,7 @@ def test_ping_monitor_hits_default_uptimekuma_on_fail():
         f'{DEFAULT_PUSH_URL}?status=down&msg=fail',
         f'{DEFAULT_PUSH_URL}?status=down&msg=fail',
         verify=True,
         verify=True,
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(flexmock(ok=True)).once()
     ).and_return(flexmock(ok=True)).once()
 
 
     module.ping_monitor(
     module.ping_monitor(
@@ -31,6 +32,7 @@ def test_ping_monitor_hits_custom_uptimekuma_on_fail():
         f'{CUSTOM_PUSH_URL}?status=down&msg=fail',
         f'{CUSTOM_PUSH_URL}?status=down&msg=fail',
         verify=True,
         verify=True,
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(flexmock(ok=True)).once()
     ).and_return(flexmock(ok=True)).once()
 
 
     module.ping_monitor(
     module.ping_monitor(
@@ -49,6 +51,7 @@ def test_ping_monitor_custom_uptimekuma_on_start():
         f'{CUSTOM_PUSH_URL}?status=up&msg=start',
         f'{CUSTOM_PUSH_URL}?status=up&msg=start',
         verify=True,
         verify=True,
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(flexmock(ok=True)).once()
     ).and_return(flexmock(ok=True)).once()
 
 
     module.ping_monitor(
     module.ping_monitor(
@@ -67,6 +70,7 @@ def test_ping_monitor_custom_uptimekuma_on_finish():
         f'{CUSTOM_PUSH_URL}?status=up&msg=finish',
         f'{CUSTOM_PUSH_URL}?status=up&msg=finish',
         verify=True,
         verify=True,
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(flexmock(ok=True)).once()
     ).and_return(flexmock(ok=True)).once()
 
 
     module.ping_monitor(
     module.ping_monitor(
@@ -127,6 +131,7 @@ def test_ping_monitor_with_connection_error_logs_warning():
         f'{CUSTOM_PUSH_URL}?status=down&msg=fail',
         f'{CUSTOM_PUSH_URL}?status=down&msg=fail',
         verify=True,
         verify=True,
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_raise(module.requests.exceptions.ConnectionError)
     ).and_raise(module.requests.exceptions.ConnectionError)
     flexmock(module.logger).should_receive('warning').once()
     flexmock(module.logger).should_receive('warning').once()
 
 
@@ -150,6 +155,7 @@ def test_ping_monitor_with_other_error_logs_warning():
         f'{CUSTOM_PUSH_URL}?status=down&msg=fail',
         f'{CUSTOM_PUSH_URL}?status=down&msg=fail',
         verify=True,
         verify=True,
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(response)
     ).and_return(response)
     flexmock(module.logger).should_receive('warning').once()
     flexmock(module.logger).should_receive('warning').once()
 
 
@@ -183,6 +189,7 @@ def test_ping_monitor_skips_ssl_verification_when_verify_tls_false():
         f'{CUSTOM_PUSH_URL}?status=down&msg=fail',
         f'{CUSTOM_PUSH_URL}?status=down&msg=fail',
         verify=False,
         verify=False,
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(flexmock(ok=True)).once()
     ).and_return(flexmock(ok=True)).once()
 
 
     module.ping_monitor(
     module.ping_monitor(
@@ -201,6 +208,7 @@ def test_ping_monitor_executes_ssl_verification_when_verify_tls_true():
         f'{CUSTOM_PUSH_URL}?status=down&msg=fail',
         f'{CUSTOM_PUSH_URL}?status=down&msg=fail',
         verify=True,
         verify=True,
         timeout=int,
         timeout=int,
+        headers={'User-Agent': 'borgmatic'},
     ).and_return(flexmock(ok=True)).once()
     ).and_return(flexmock(ok=True)).once()
 
 
     module.ping_monitor(
     module.ping_monitor(

+ 2 - 0
tests/unit/hooks/monitoring/test_zabbix.py

@@ -56,11 +56,13 @@ DATA_USER_LOGOUT = {
 
 
 AUTH_HEADERS_LOGIN = {
 AUTH_HEADERS_LOGIN = {
     'Content-Type': 'application/json-rpc',
     'Content-Type': 'application/json-rpc',
+    'User-Agent': 'borgmatic',
 }
 }
 
 
 AUTH_HEADERS = {
 AUTH_HEADERS = {
     'Content-Type': 'application/json-rpc',
     'Content-Type': 'application/json-rpc',
     'Authorization': f'Bearer {API_KEY}',
     'Authorization': f'Bearer {API_KEY}',
+    'User-Agent': 'borgmatic',
 }
 }