From 4db09a73b3e5fb7c16b155166336ca811797ed88 Mon Sep 17 00:00:00 2001 From: Paul Wilde Date: Fri, 21 Jun 2024 16:47:51 +0100 Subject: [PATCH 01/22] WIP implement Uptime Kuma hook --- borgmatic/hooks/uptimekuma.py | 53 +++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 borgmatic/hooks/uptimekuma.py diff --git a/borgmatic/hooks/uptimekuma.py b/borgmatic/hooks/uptimekuma.py new file mode 100644 index 0000000..5990f60 --- /dev/null +++ b/borgmatic/hooks/uptimekuma.py @@ -0,0 +1,53 @@ +import logging + +import requests + +logger = logging.getLogger(__name__) + + +def initialize_monitor( + ping_url, config, config_filename, monitoring_log_level, dry_run +): # pragma: no cover + ''' + No initialization is necessary for this monitor. + ''' + pass + + +def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run): + ''' + Ping the configured Uptime Kuma push_code. Use the given configuration filename in any log entries. + If this is a dry run, then don't actually ping anything. + ''' + + run_states = hook_config.get('states', ['success','fail']) + + if state.name.lower() in run_states: + dry_run_label = ' (dry run; not actually pinging)' if dry_run else '' + + status = state.name.lower() == "fail" ? "down" : "up" + + base_url = hook_config.get('server', 'https://example.uptime.kuma') & "/api/push" + push_code = hook_config.get('push_code') + + logger.info(f'{config_filename}: Pinging Uptime Kuma push_code {push_code}{dry_run_label}') + logger.debug(f'{config_filename}: Using Uptime Kuma ping URL {base_url}/{push_code}') + logger.debug(f'{config_filename}: Full Uptime Kuma state URL {base_url}/{push_code}?status={status}&msg={state.name}&ping=') + + if not dry_run: + logging.getLogger('urllib3').setLevel(logging.ERROR) + try: + response = requests.post(f'{base_url}/{push_code}?status={status}&msg={state.name}&ping=') + if not response.ok: + response.raise_for_status() + except requests.exceptions.RequestException as error: + logger.warning(f'{config_filename}: ntfy error: {error}') + + +def destroy_monitor( + ping_url_or_uuid, config, config_filename, monitoring_log_level, dry_run +): # pragma: no cover + ''' + No destruction is necessary for this monitor. + ''' + pass From 83bcea98dcec33b18cceb1c3077df9cd1f4f7725 Mon Sep 17 00:00:00 2001 From: Paul Wilde Date: Fri, 21 Jun 2024 16:57:20 +0100 Subject: [PATCH 02/22] WIP added some schema info for uptime kuma --- borgmatic/config/schema.yaml | 37 +++++++++++++++++++++++++++++++++++ borgmatic/hooks/uptimekuma.py | 7 ++++--- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index d8a3548..0acffbf 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -1676,6 +1676,43 @@ properties: an account at https://healthchecks.io (or self-host Healthchecks) if you'd like to use this service. See borgmatic monitoring documentation for details. + uptimekuma: + type: object + required: ['ping_url', 'push_code'] + additionalProperties: false + properties: + ping_url: + type: string + description: | + Uptime Kuma base URL or UUID to notify when a backup + begins, ends, or errors + example: https://example.uptime.kuma + push_code: + type: string + description: | + Uptime Kuma "Push Code" from the push URL you have been given. + For example, the push code for: + 'https://uptime.kuma/api/push/0evpM0MIdE?status=up&msg=OK&ping=' + would be '0evpM0MIdE' + states: + type: array + items: + type: string + enum: + - start + - finish + - fail + uniqueItems: true + description: | + List of one or more monitoring states to ping for: "start", + "finish", and/or "fail". Defaults to pinging for all + states. + example: + - start, finish, fail + description: | + Configuration for a monitoring integration with Uptime Kuma using + the 'Push' monitor type. + See more information here: https://uptime.kuma.pet cronitor: type: object required: ['ping_url'] diff --git a/borgmatic/hooks/uptimekuma.py b/borgmatic/hooks/uptimekuma.py index 5990f60..02317ef 100644 --- a/borgmatic/hooks/uptimekuma.py +++ b/borgmatic/hooks/uptimekuma.py @@ -20,13 +20,14 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev If this is a dry run, then don't actually ping anything. ''' - run_states = hook_config.get('states', ['success','fail']) + run_states = hook_config.get('states', ['start','finish','fail']) if state.name.lower() in run_states: + dry_run_label = ' (dry run; not actually pinging)' if dry_run else '' status = state.name.lower() == "fail" ? "down" : "up" - + base_url = hook_config.get('server', 'https://example.uptime.kuma') & "/api/push" push_code = hook_config.get('push_code') @@ -41,7 +42,7 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev if not response.ok: response.raise_for_status() except requests.exceptions.RequestException as error: - logger.warning(f'{config_filename}: ntfy error: {error}') + logger.warning(f'{config_filename}: Uptime Kuma error: {error}') def destroy_monitor( From 6eb76454bb0742fcdec8840339027ab571f1b90d Mon Sep 17 00:00:00 2001 From: Paul Wilde Date: Fri, 21 Jun 2024 17:00:44 +0100 Subject: [PATCH 03/22] WIP added some schema info for uptime kuma --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 53889ff..82684d8 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ borgmatic is powered by [Borg Backup](https://www.borgbackup.org/). MongoDB SQLite Healthchecks +Uptime Kuma Cronitor Cronhub PagerDuty From 27c90b7cf14d5b97a1ef7de36116cf198a672588 Mon Sep 17 00:00:00 2001 From: Paul Wilde Date: Fri, 21 Jun 2024 21:10:14 +0100 Subject: [PATCH 04/22] Added Uptime Kuma hook --- borgmatic/config/schema.yaml | 17 ++++++++++------- borgmatic/hooks/dispatch.py | 2 ++ borgmatic/hooks/monitor.py | 2 +- borgmatic/hooks/uptimekuma.py | 11 +++++++---- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index 0acffbf..f2dc4d3 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -1678,10 +1678,10 @@ properties: documentation for details. uptimekuma: type: object - required: ['ping_url', 'push_code'] + required: ['server', 'push_code'] additionalProperties: false properties: - ping_url: + server: type: string description: | Uptime Kuma base URL or UUID to notify when a backup @@ -1691,9 +1691,10 @@ properties: type: string description: | Uptime Kuma "Push Code" from the push URL you have been given. - For example, the push code for: - 'https://uptime.kuma/api/push/0evpM0MIdE?status=up&msg=OK&ping=' - would be '0evpM0MIdE' + For example, the push code for + https://uptime.kuma/api/push/12345678?status=up&msg=OK&ping= + would be 12345678 + example: 12345678 states: type: array items: @@ -1708,10 +1709,12 @@ properties: "finish", and/or "fail". Defaults to pinging for all states. example: - - start, finish, fail + - start + - finish + - fail description: | Configuration for a monitoring integration with Uptime Kuma using - the 'Push' monitor type. + the Push monitor type. See more information here: https://uptime.kuma.pet cronitor: type: object diff --git a/borgmatic/hooks/dispatch.py b/borgmatic/hooks/dispatch.py index d437c98..0740cdb 100644 --- a/borgmatic/hooks/dispatch.py +++ b/borgmatic/hooks/dispatch.py @@ -13,6 +13,7 @@ from borgmatic.hooks import ( pagerduty, postgresql, sqlite, + uptimekuma ) logger = logging.getLogger(__name__) @@ -30,6 +31,7 @@ HOOK_NAME_TO_MODULE = { 'postgresql_databases': postgresql, 'sqlite_databases': sqlite, 'loki': loki, + 'uptimekuma': uptimekuma, } diff --git a/borgmatic/hooks/monitor.py b/borgmatic/hooks/monitor.py index 0cbfef4..c6edb68 100644 --- a/borgmatic/hooks/monitor.py +++ b/borgmatic/hooks/monitor.py @@ -1,6 +1,6 @@ from enum import Enum -MONITOR_HOOK_NAMES = ('apprise', 'healthchecks', 'cronitor', 'cronhub', 'pagerduty', 'ntfy', 'loki') +MONITOR_HOOK_NAMES = ('apprise', 'healthchecks', 'cronitor', 'cronhub', 'pagerduty', 'ntfy', 'loki', 'uptimekuma') class State(Enum): diff --git a/borgmatic/hooks/uptimekuma.py b/borgmatic/hooks/uptimekuma.py index 02317ef..42eb335 100644 --- a/borgmatic/hooks/uptimekuma.py +++ b/borgmatic/hooks/uptimekuma.py @@ -26,19 +26,22 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev dry_run_label = ' (dry run; not actually pinging)' if dry_run else '' - status = state.name.lower() == "fail" ? "down" : "up" + + status = "up" + if state.name.lower() == "fail": + status = "down" - base_url = hook_config.get('server', 'https://example.uptime.kuma') & "/api/push" + base_url = hook_config.get('server', 'https://example.uptime.kuma') + "/api/push" push_code = hook_config.get('push_code') logger.info(f'{config_filename}: Pinging Uptime Kuma push_code {push_code}{dry_run_label}') logger.debug(f'{config_filename}: Using Uptime Kuma ping URL {base_url}/{push_code}') - logger.debug(f'{config_filename}: Full Uptime Kuma state URL {base_url}/{push_code}?status={status}&msg={state.name}&ping=') + logger.debug(f'{config_filename}: Full Uptime Kuma state URL {base_url}/{push_code}?status={status}&msg={state.name.lower()}&ping=') if not dry_run: logging.getLogger('urllib3').setLevel(logging.ERROR) try: - response = requests.post(f'{base_url}/{push_code}?status={status}&msg={state.name}&ping=') + response = requests.get(f'{base_url}/{push_code}?status={status}&msg={state.name.lower()}&ping=') if not response.ok: response.raise_for_status() except requests.exceptions.RequestException as error: From 5ab99b4cc01156731dc22490bf452b05aa545d41 Mon Sep 17 00:00:00 2001 From: Paul Wilde Date: Fri, 21 Jun 2024 21:21:04 +0100 Subject: [PATCH 05/22] Added Uptime Kuma image to readme --- docs/static/uptimekuma.png | Bin 0 -> 15541 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/static/uptimekuma.png diff --git a/docs/static/uptimekuma.png b/docs/static/uptimekuma.png new file mode 100644 index 0000000000000000000000000000000000000000..fe7d171cc9626fda22e45233af683226cf8c565b GIT binary patch literal 15541 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 Qd5lL^mRFOjk}(hcKN({nm;e9( literal 0 HcmV?d00001 From dcbc30b164621f06538286aed206ed868f0e3be1 Mon Sep 17 00:00:00 2001 From: Paul Wilde Date: Sat, 22 Jun 2024 10:19:34 +0100 Subject: [PATCH 06/22] WIP add uptime kuma tests --- borgmatic/config/schema.yaml | 6 +- tests/unit/hooks/test_uptimekuma.py | 266 ++++++++++++++++++++++++++++ 2 files changed, 269 insertions(+), 3 deletions(-) create mode 100644 tests/unit/hooks/test_uptimekuma.py diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index f2dc4d3..e180fcd 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -1690,9 +1690,9 @@ properties: push_code: type: string description: | - Uptime Kuma "Push Code" from the push URL you have been given. - For example, the push code for - https://uptime.kuma/api/push/12345678?status=up&msg=OK&ping= + Uptime Kuma "Push Code" from the push URL you have been + given. For example, the push code for + https://base.url/api/push/12345678?status=up&msg=OK&ping= would be 12345678 example: 12345678 states: diff --git a/tests/unit/hooks/test_uptimekuma.py b/tests/unit/hooks/test_uptimekuma.py new file mode 100644 index 0000000..3a58aa7 --- /dev/null +++ b/tests/unit/hooks/test_uptimekuma.py @@ -0,0 +1,266 @@ +from enum import Enum + +from flexmock import flexmock + +import borgmatic.hooks.monitor +from borgmatic.hooks import uptimekuma as module + +default_base_url = 'https://example.uptime.kuma' +custom_base_url = 'https://uptime.example.com' +push_code = 'abcd1234' + +def test_ping_monitor_minimal_config_hits_hosted_ntfy_on_fail(): + hook_config = {'push_code': push_code} + flexmock(module.requests).should_receive('get').with_args( + f'{default_base_url}/api/push/{push_code}' + ).and_return(flexmock(ok=True)).once() + + module.ping_monitor( + hook_config, + {}, + 'config.yaml', + borgmatic.hooks.monitor.State.FAIL, + monitoring_log_level=1, + dry_run=True, + ) + + +def test_ping_monitor_with_access_token_hits_hosted_ntfy_on_fail(): + hook_config = { + 'topic': topic, + 'access_token': 'abc123', + } + flexmock(module.requests).should_receive('post').with_args( + f'{default_base_url}/{topic}', + headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL), + auth=module.requests.auth.HTTPBasicAuth('', 'abc123'), + ).and_return(flexmock(ok=True)).once() + + module.ping_monitor( + hook_config, + {}, + 'config.yaml', + borgmatic.hooks.monitor.State.FAIL, + monitoring_log_level=1, + dry_run=False, + ) + + +def test_ping_monitor_with_username_password_and_access_token_ignores_username_password(): + hook_config = { + 'topic': topic, + 'username': 'testuser', + 'password': 'fakepassword', + 'access_token': 'abc123', + } + flexmock(module.requests).should_receive('post').with_args( + f'{default_base_url}/{topic}', + headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL), + auth=module.requests.auth.HTTPBasicAuth('', 'abc123'), + ).and_return(flexmock(ok=True)).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, + ) + + +def test_ping_monitor_with_username_password_hits_hosted_ntfy_on_fail(): + hook_config = { + 'topic': topic, + 'username': 'testuser', + 'password': 'fakepassword', + } + flexmock(module.requests).should_receive('post').with_args( + f'{default_base_url}/{topic}', + headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL), + auth=module.requests.auth.HTTPBasicAuth('testuser', 'fakepassword'), + ).and_return(flexmock(ok=True)).once() + + module.ping_monitor( + hook_config, + {}, + 'config.yaml', + borgmatic.hooks.monitor.State.FAIL, + monitoring_log_level=1, + dry_run=False, + ) + + +def test_ping_monitor_with_password_but_no_username_warns(): + hook_config = {'topic': topic, 'password': 'fakepassword'} + flexmock(module.requests).should_receive('post').with_args( + f'{default_base_url}/{topic}', + headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL), + auth=None, + ).and_return(flexmock(ok=True)).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, + ) + + +def test_ping_monitor_with_username_but_no_password_warns(): + hook_config = {'topic': topic, 'username': 'testuser'} + flexmock(module.requests).should_receive('post').with_args( + f'{default_base_url}/{topic}', + headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL), + auth=None, + ).and_return(flexmock(ok=True)).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, + ) + + +def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_start(): + hook_config = {'topic': topic} + 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_minimal_config_does_not_hit_hosted_ntfy_on_finish(): + hook_config = {'topic': topic} + flexmock(module.requests).should_receive('post').never() + + module.ping_monitor( + hook_config, + {}, + 'config.yaml', + borgmatic.hooks.monitor.State.FINISH, + monitoring_log_level=1, + dry_run=False, + ) + + +def test_ping_monitor_minimal_config_hits_selfhosted_ntfy_on_fail(): + hook_config = {'topic': topic, 'server': custom_base_url} + flexmock(module.requests).should_receive('post').with_args( + f'{custom_base_url}/{topic}', + headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL), + auth=None, + ).and_return(flexmock(ok=True)).once() + + module.ping_monitor( + hook_config, + {}, + 'config.yaml', + borgmatic.hooks.monitor.State.FAIL, + monitoring_log_level=1, + dry_run=False, + ) + + +def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_fail_dry_run(): + hook_config = {'topic': topic} + 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=True, + ) + + +def test_ping_monitor_custom_message_hits_hosted_ntfy_on_fail(): + hook_config = {'topic': topic, 'fail': custom_message_config} + flexmock(module.requests).should_receive('post').with_args( + f'{default_base_url}/{topic}', headers=custom_message_headers, auth=None + ).and_return(flexmock(ok=True)).once() + + module.ping_monitor( + hook_config, + {}, + 'config.yaml', + borgmatic.hooks.monitor.State.FAIL, + monitoring_log_level=1, + dry_run=False, + ) + + +def test_ping_monitor_custom_state_hits_hosted_ntfy_on_start(): + hook_config = {'topic': topic, 'states': ['start', 'fail']} + flexmock(module.requests).should_receive('post').with_args( + f'{default_base_url}/{topic}', + headers=return_default_message_headers(borgmatic.hooks.monitor.State.START), + auth=None, + ).and_return(flexmock(ok=True)).once() + + module.ping_monitor( + hook_config, + {}, + 'config.yaml', + borgmatic.hooks.monitor.State.START, + monitoring_log_level=1, + dry_run=False, + ) + + +def test_ping_monitor_with_connection_error_logs_warning(): + hook_config = {'topic': topic} + flexmock(module.requests).should_receive('post').with_args( + f'{default_base_url}/{topic}', + headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL), + auth=None, + ).and_raise(module.requests.exceptions.ConnectionError) + flexmock(module.logger).should_receive('warning').once() + + module.ping_monitor( + hook_config, + {}, + 'config.yaml', + borgmatic.hooks.monitor.State.FAIL, + monitoring_log_level=1, + dry_run=False, + ) + + +def test_ping_monitor_with_other_error_logs_warning(): + hook_config = {'topic': topic} + response = flexmock(ok=False) + response.should_receive('raise_for_status').and_raise( + module.requests.exceptions.RequestException + ) + flexmock(module.requests).should_receive('post').with_args( + f'{default_base_url}/{topic}', + headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL), + auth=None, + ).and_return(response) + 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, + ) From a509cdedd5bd58bd0d0b6028333ad457a3d265eb Mon Sep 17 00:00:00 2001 From: Paul Wilde Date: Sat, 22 Jun 2024 10:46:17 +0100 Subject: [PATCH 07/22] Added Uptime Kuma tests --- borgmatic/hooks/dispatch.py | 2 +- borgmatic/hooks/monitor.py | 11 +- borgmatic/hooks/uptimekuma.py | 21 +-- tests/unit/hooks/test_uptimekuma.py | 230 ++++++++++++---------------- 4 files changed, 118 insertions(+), 146 deletions(-) diff --git a/borgmatic/hooks/dispatch.py b/borgmatic/hooks/dispatch.py index 0740cdb..93cae9c 100644 --- a/borgmatic/hooks/dispatch.py +++ b/borgmatic/hooks/dispatch.py @@ -13,7 +13,7 @@ from borgmatic.hooks import ( pagerduty, postgresql, sqlite, - uptimekuma + uptimekuma, ) logger = logging.getLogger(__name__) diff --git a/borgmatic/hooks/monitor.py b/borgmatic/hooks/monitor.py index c6edb68..abe28c5 100644 --- a/borgmatic/hooks/monitor.py +++ b/borgmatic/hooks/monitor.py @@ -1,6 +1,15 @@ from enum import Enum -MONITOR_HOOK_NAMES = ('apprise', 'healthchecks', 'cronitor', 'cronhub', 'pagerduty', 'ntfy', 'loki', 'uptimekuma') +MONITOR_HOOK_NAMES = ( + 'apprise', + 'healthchecks', + 'cronitor', + 'cronhub', + 'pagerduty', + 'ntfy', + 'loki', + 'uptimekuma', +) class State(Enum): diff --git a/borgmatic/hooks/uptimekuma.py b/borgmatic/hooks/uptimekuma.py index 42eb335..fcd171d 100644 --- a/borgmatic/hooks/uptimekuma.py +++ b/borgmatic/hooks/uptimekuma.py @@ -20,28 +20,31 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev If this is a dry run, then don't actually ping anything. ''' - run_states = hook_config.get('states', ['start','finish','fail']) + run_states = hook_config.get('states', ['start', 'finish', 'fail']) if state.name.lower() in run_states: - + dry_run_label = ' (dry run; not actually pinging)' if dry_run else '' + status = 'up' + if state.name.lower() == 'fail': + status = 'down' - status = "up" - if state.name.lower() == "fail": - status = "down" - - base_url = hook_config.get('server', 'https://example.uptime.kuma') + "/api/push" + base_url = hook_config.get('server', 'https://example.uptime.kuma') + '/api/push' push_code = hook_config.get('push_code') logger.info(f'{config_filename}: Pinging Uptime Kuma push_code {push_code}{dry_run_label}') logger.debug(f'{config_filename}: Using Uptime Kuma ping URL {base_url}/{push_code}') - logger.debug(f'{config_filename}: Full Uptime Kuma state URL {base_url}/{push_code}?status={status}&msg={state.name.lower()}&ping=') + logger.debug( + f'{config_filename}: Full Uptime Kuma state URL {base_url}/{push_code}?status={status}&msg={state.name.lower()}&ping=' + ) if not dry_run: logging.getLogger('urllib3').setLevel(logging.ERROR) try: - response = requests.get(f'{base_url}/{push_code}?status={status}&msg={state.name.lower()}&ping=') + response = requests.get( + f'{base_url}/{push_code}?status={status}&msg={state.name.lower()}&ping=' + ) if not response.ok: response.raise_for_status() except requests.exceptions.RequestException as error: diff --git a/tests/unit/hooks/test_uptimekuma.py b/tests/unit/hooks/test_uptimekuma.py index 3a58aa7..547a6f2 100644 --- a/tests/unit/hooks/test_uptimekuma.py +++ b/tests/unit/hooks/test_uptimekuma.py @@ -1,5 +1,3 @@ -from enum import Enum - from flexmock import flexmock import borgmatic.hooks.monitor @@ -9,31 +7,11 @@ default_base_url = 'https://example.uptime.kuma' custom_base_url = 'https://uptime.example.com' push_code = 'abcd1234' -def test_ping_monitor_minimal_config_hits_hosted_ntfy_on_fail(): + +def test_ping_monitor_hits_default_uptimekuma_on_fail(): hook_config = {'push_code': push_code} flexmock(module.requests).should_receive('get').with_args( - f'{default_base_url}/api/push/{push_code}' - ).and_return(flexmock(ok=True)).once() - - module.ping_monitor( - hook_config, - {}, - 'config.yaml', - borgmatic.hooks.monitor.State.FAIL, - monitoring_log_level=1, - dry_run=True, - ) - - -def test_ping_monitor_with_access_token_hits_hosted_ntfy_on_fail(): - hook_config = { - 'topic': topic, - 'access_token': 'abc123', - } - flexmock(module.requests).should_receive('post').with_args( - f'{default_base_url}/{topic}', - headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL), - auth=module.requests.auth.HTTPBasicAuth('', 'abc123'), + f'{default_base_url}/api/push/{push_code}?status=down&msg=fail&ping=' ).and_return(flexmock(ok=True)).once() module.ping_monitor( @@ -46,40 +24,10 @@ def test_ping_monitor_with_access_token_hits_hosted_ntfy_on_fail(): ) -def test_ping_monitor_with_username_password_and_access_token_ignores_username_password(): - hook_config = { - 'topic': topic, - 'username': 'testuser', - 'password': 'fakepassword', - 'access_token': 'abc123', - } - flexmock(module.requests).should_receive('post').with_args( - f'{default_base_url}/{topic}', - headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL), - auth=module.requests.auth.HTTPBasicAuth('', 'abc123'), - ).and_return(flexmock(ok=True)).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, - ) - - -def test_ping_monitor_with_username_password_hits_hosted_ntfy_on_fail(): - hook_config = { - 'topic': topic, - 'username': 'testuser', - 'password': 'fakepassword', - } - flexmock(module.requests).should_receive('post').with_args( - f'{default_base_url}/{topic}', - headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL), - auth=module.requests.auth.HTTPBasicAuth('testuser', 'fakepassword'), +def test_ping_monitor_hits_custom_uptimekuma_on_fail(): + hook_config = {'server': custom_base_url, 'push_code': push_code} + flexmock(module.requests).should_receive('get').with_args( + f'{custom_base_url}/api/push/{push_code}?status=down&msg=fail&ping=' ).and_return(flexmock(ok=True)).once() module.ping_monitor( @@ -92,47 +40,11 @@ def test_ping_monitor_with_username_password_hits_hosted_ntfy_on_fail(): ) -def test_ping_monitor_with_password_but_no_username_warns(): - hook_config = {'topic': topic, 'password': 'fakepassword'} - flexmock(module.requests).should_receive('post').with_args( - f'{default_base_url}/{topic}', - headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL), - auth=None, +def test_ping_monitor_hits_default_uptimekuma_on_start(): + hook_config = {'push_code': push_code} + flexmock(module.requests).should_receive('get').with_args( + f'{default_base_url}/api/push/{push_code}?status=up&msg=start&ping=' ).and_return(flexmock(ok=True)).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, - ) - - -def test_ping_monitor_with_username_but_no_password_warns(): - hook_config = {'topic': topic, 'username': 'testuser'} - flexmock(module.requests).should_receive('post').with_args( - f'{default_base_url}/{topic}', - headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL), - auth=None, - ).and_return(flexmock(ok=True)).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, - ) - - -def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_start(): - hook_config = {'topic': topic} - flexmock(module.requests).should_receive('post').never() module.ping_monitor( hook_config, @@ -144,9 +56,27 @@ def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_start(): ) -def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_finish(): - hook_config = {'topic': topic} - flexmock(module.requests).should_receive('post').never() +def test_ping_monitor_custom_uptimekuma_on_start(): + hook_config = {'server': custom_base_url, 'push_code': push_code} + flexmock(module.requests).should_receive('get').with_args( + f'{custom_base_url}/api/push/{push_code}?status=up&msg=start&ping=' + ).and_return(flexmock(ok=True)).once() + + module.ping_monitor( + hook_config, + {}, + 'config.yaml', + borgmatic.hooks.monitor.State.START, + monitoring_log_level=1, + dry_run=False, + ) + + +def test_ping_monitor_hits_default_uptimekuma_on_finish(): + hook_config = {'push_code': push_code} + flexmock(module.requests).should_receive('get').with_args( + f'{default_base_url}/api/push/{push_code}?status=up&msg=finish&ping=' + ).and_return(flexmock(ok=True)).once() module.ping_monitor( hook_config, @@ -158,27 +88,25 @@ def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_finish(): ) -def test_ping_monitor_minimal_config_hits_selfhosted_ntfy_on_fail(): - hook_config = {'topic': topic, 'server': custom_base_url} - flexmock(module.requests).should_receive('post').with_args( - f'{custom_base_url}/{topic}', - headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL), - auth=None, +def test_ping_monitor_custom_uptimekuma_on_finish(): + hook_config = {'server': custom_base_url, 'push_code': push_code} + flexmock(module.requests).should_receive('get').with_args( + f'{custom_base_url}/api/push/{push_code}?status=up&msg=finish&ping=' ).and_return(flexmock(ok=True)).once() module.ping_monitor( hook_config, {}, 'config.yaml', - borgmatic.hooks.monitor.State.FAIL, + borgmatic.hooks.monitor.State.FINISH, monitoring_log_level=1, dry_run=False, ) -def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_fail_dry_run(): - hook_config = {'topic': topic} - flexmock(module.requests).should_receive('post').never() +def test_ping_monitor_does_not_hit_default_uptimekuma_on_fail_dry_run(): + hook_config = {'push_code': push_code} + flexmock(module.requests).should_receive('get').never() module.ping_monitor( hook_config, @@ -190,11 +118,9 @@ def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_fail_dry_run(): ) -def test_ping_monitor_custom_message_hits_hosted_ntfy_on_fail(): - hook_config = {'topic': topic, 'fail': custom_message_config} - flexmock(module.requests).should_receive('post').with_args( - f'{default_base_url}/{topic}', headers=custom_message_headers, auth=None - ).and_return(flexmock(ok=True)).once() +def test_ping_monitor_does_not_hit_custom_uptimekuma_on_fail_dry_run(): + hook_config = {'server': custom_base_url, 'push_code': push_code} + flexmock(module.requests).should_receive('get').never() module.ping_monitor( hook_config, @@ -202,17 +128,13 @@ def test_ping_monitor_custom_message_hits_hosted_ntfy_on_fail(): 'config.yaml', borgmatic.hooks.monitor.State.FAIL, monitoring_log_level=1, - dry_run=False, + dry_run=True, ) -def test_ping_monitor_custom_state_hits_hosted_ntfy_on_start(): - hook_config = {'topic': topic, 'states': ['start', 'fail']} - flexmock(module.requests).should_receive('post').with_args( - f'{default_base_url}/{topic}', - headers=return_default_message_headers(borgmatic.hooks.monitor.State.START), - auth=None, - ).and_return(flexmock(ok=True)).once() +def test_ping_monitor_does_not_hit_default_uptimekuma_on_start_dry_run(): + hook_config = {'push_code': push_code} + flexmock(module.requests).should_receive('get').never() module.ping_monitor( hook_config, @@ -220,16 +142,56 @@ def test_ping_monitor_custom_state_hits_hosted_ntfy_on_start(): 'config.yaml', borgmatic.hooks.monitor.State.START, monitoring_log_level=1, - dry_run=False, + dry_run=True, + ) + + +def test_ping_monitor_does_not_hit_custom_uptimekuma_on_start_dry_run(): + hook_config = {'server': custom_base_url, 'push_code': push_code} + flexmock(module.requests).should_receive('get').never() + + module.ping_monitor( + hook_config, + {}, + 'config.yaml', + borgmatic.hooks.monitor.State.START, + monitoring_log_level=1, + dry_run=True, + ) + + +def test_ping_monitor_does_not_hit_default_uptimekuma_on_finish_dry_run(): + hook_config = {'push_code': push_code} + flexmock(module.requests).should_receive('get').never() + + module.ping_monitor( + hook_config, + {}, + 'config.yaml', + borgmatic.hooks.monitor.State.FINISH, + monitoring_log_level=1, + dry_run=True, + ) + + +def test_ping_monitor_does_not_hit_custom_uptimekuma_on_finish_dry_run(): + hook_config = {'server': custom_base_url, 'push_code': push_code} + flexmock(module.requests).should_receive('get').never() + + module.ping_monitor( + hook_config, + {}, + 'config.yaml', + borgmatic.hooks.monitor.State.FINISH, + monitoring_log_level=1, + dry_run=True, ) def test_ping_monitor_with_connection_error_logs_warning(): - hook_config = {'topic': topic} - flexmock(module.requests).should_receive('post').with_args( - f'{default_base_url}/{topic}', - headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL), - auth=None, + hook_config = {'push_code': push_code} + flexmock(module.requests).should_receive('get').with_args( + f'{default_base_url}/api/push/{push_code}?status=down&msg=fail&ping=' ).and_raise(module.requests.exceptions.ConnectionError) flexmock(module.logger).should_receive('warning').once() @@ -244,15 +206,13 @@ def test_ping_monitor_with_connection_error_logs_warning(): def test_ping_monitor_with_other_error_logs_warning(): - hook_config = {'topic': topic} + hook_config = {'push_code': push_code} response = flexmock(ok=False) response.should_receive('raise_for_status').and_raise( module.requests.exceptions.RequestException ) flexmock(module.requests).should_receive('post').with_args( - f'{default_base_url}/{topic}', - headers=return_default_message_headers(borgmatic.hooks.monitor.State.FAIL), - auth=None, + f'{default_base_url}/api/push/{push_code}?status=down&msg=fail&ping=' ).and_return(response) flexmock(module.logger).should_receive('warning').once() From 52aa7c5d21ab151a6baef087d336777f3a2da8ec Mon Sep 17 00:00:00 2001 From: Paul Wilde Date: Mon, 24 Jun 2024 10:20:55 +0100 Subject: [PATCH 08/22] switched to using full 'push_url' instead of separate 'server' and 'push_code' --- borgmatic/config/schema.yaml | 18 +++------- borgmatic/hooks/uptimekuma.py | 51 ++++++++++++-------------- tests/unit/hooks/test_uptimekuma.py | 55 ++++++++++++++++------------- 3 files changed, 57 insertions(+), 67 deletions(-) diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index e180fcd..05520da 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -1678,23 +1678,15 @@ properties: documentation for details. uptimekuma: type: object - required: ['server', 'push_code'] + required: ['push_url'] additionalProperties: false properties: - server: + push_url: type: string description: | - Uptime Kuma base URL or UUID to notify when a backup - begins, ends, or errors - example: https://example.uptime.kuma - push_code: - type: string - description: | - Uptime Kuma "Push Code" from the push URL you have been - given. For example, the push code for - https://base.url/api/push/12345678?status=up&msg=OK&ping= - would be 12345678 - example: 12345678 + Uptime Kuma push URL without query string (do not include the + question mark or anything after it). + example: https://example.uptime.kuma/api/push/abcd1234 states: type: array items: diff --git a/borgmatic/hooks/uptimekuma.py b/borgmatic/hooks/uptimekuma.py index fcd171d..59f20df 100644 --- a/borgmatic/hooks/uptimekuma.py +++ b/borgmatic/hooks/uptimekuma.py @@ -16,40 +16,33 @@ def initialize_monitor( def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run): ''' - Ping the configured Uptime Kuma push_code. Use the given configuration filename in any log entries. + Ping the configured Uptime Kuma push_url. + Use the given configuration filename in any log entries. If this is a dry run, then don't actually ping anything. ''' - run_states = hook_config.get('states', ['start', 'finish', 'fail']) + if state.name.lower() not in run_states: + return + dry_run_label = ' (dry run; not actually pinging)' if dry_run else '' + status = 'down' if state.name.lower() == 'fail' else 'up' + push_url = hook_config.get('push_url', 'https://example.uptime.kuma/api/push/abcd1234') + query = f'status={status}&msg={state.name.lower()}' - if state.name.lower() in run_states: - - dry_run_label = ' (dry run; not actually pinging)' if dry_run else '' - - status = 'up' - if state.name.lower() == 'fail': - status = 'down' - - base_url = hook_config.get('server', 'https://example.uptime.kuma') + '/api/push' - push_code = hook_config.get('push_code') - - logger.info(f'{config_filename}: Pinging Uptime Kuma push_code {push_code}{dry_run_label}') - logger.debug(f'{config_filename}: Using Uptime Kuma ping URL {base_url}/{push_code}') - logger.debug( - f'{config_filename}: Full Uptime Kuma state URL {base_url}/{push_code}?status={status}&msg={state.name.lower()}&ping=' - ) - - if not dry_run: - logging.getLogger('urllib3').setLevel(logging.ERROR) - try: - response = requests.get( - f'{base_url}/{push_code}?status={status}&msg={state.name.lower()}&ping=' - ) - if not response.ok: - response.raise_for_status() - except requests.exceptions.RequestException as error: - logger.warning(f'{config_filename}: Uptime Kuma error: {error}') + logger.info(f'{config_filename}: Pinging Uptime Kuma push_url {push_url}?{query} {dry_run_label}') + logger.debug( + f'{config_filename}: Full Uptime Kuma state URL {push_url}?{query}' + ) + if not dry_run: + logging.getLogger('urllib3').setLevel(logging.ERROR) + try: + response = requests.get( + f'{push_url}?{query}' + ) + if not response.ok: + response.raise_for_status() + except requests.exceptions.RequestException as error: + logger.warning(f'{config_filename}: Uptime Kuma error: {error}') def destroy_monitor( ping_url_or_uuid, config, config_filename, monitoring_log_level, dry_run diff --git a/tests/unit/hooks/test_uptimekuma.py b/tests/unit/hooks/test_uptimekuma.py index 547a6f2..1d57444 100644 --- a/tests/unit/hooks/test_uptimekuma.py +++ b/tests/unit/hooks/test_uptimekuma.py @@ -3,15 +3,13 @@ from flexmock import flexmock import borgmatic.hooks.monitor from borgmatic.hooks import uptimekuma as module -default_base_url = 'https://example.uptime.kuma' -custom_base_url = 'https://uptime.example.com' -push_code = 'abcd1234' +DEFAULT_BASE_URL = 'https://example.uptime.kuma/api/push/abcd1234' +CUSTOM_BASE_URL = 'https://uptime.example.com/api/push/efgh5678' def test_ping_monitor_hits_default_uptimekuma_on_fail(): - hook_config = {'push_code': push_code} flexmock(module.requests).should_receive('get').with_args( - f'{default_base_url}/api/push/{push_code}?status=down&msg=fail&ping=' + f'{DEFAULT_BASE_URL}?status=down&msg=fail' ).and_return(flexmock(ok=True)).once() module.ping_monitor( @@ -25,9 +23,9 @@ def test_ping_monitor_hits_default_uptimekuma_on_fail(): def test_ping_monitor_hits_custom_uptimekuma_on_fail(): - hook_config = {'server': custom_base_url, 'push_code': push_code} + hook_config = {'push_url': push_url} flexmock(module.requests).should_receive('get').with_args( - f'{custom_base_url}/api/push/{push_code}?status=down&msg=fail&ping=' + f'{CUSTOM_BASE_URL}?status=down&msg=fail' ).and_return(flexmock(ok=True)).once() module.ping_monitor( @@ -41,9 +39,8 @@ def test_ping_monitor_hits_custom_uptimekuma_on_fail(): def test_ping_monitor_hits_default_uptimekuma_on_start(): - hook_config = {'push_code': push_code} flexmock(module.requests).should_receive('get').with_args( - f'{default_base_url}/api/push/{push_code}?status=up&msg=start&ping=' + f'{DEFAULT_BASE_URL}?status=up&msg=start' ).and_return(flexmock(ok=True)).once() module.ping_monitor( @@ -57,9 +54,9 @@ def test_ping_monitor_hits_default_uptimekuma_on_start(): def test_ping_monitor_custom_uptimekuma_on_start(): - hook_config = {'server': custom_base_url, 'push_code': push_code} + hook_config = {'push_url': push_url} flexmock(module.requests).should_receive('get').with_args( - f'{custom_base_url}/api/push/{push_code}?status=up&msg=start&ping=' + f'{CUSTOM_BASE_URL}?status=up&msg=start' ).and_return(flexmock(ok=True)).once() module.ping_monitor( @@ -73,9 +70,8 @@ def test_ping_monitor_custom_uptimekuma_on_start(): def test_ping_monitor_hits_default_uptimekuma_on_finish(): - hook_config = {'push_code': push_code} flexmock(module.requests).should_receive('get').with_args( - f'{default_base_url}/api/push/{push_code}?status=up&msg=finish&ping=' + f'{DEFAULT_BASE_URL}?status=up&msg=finish' ).and_return(flexmock(ok=True)).once() module.ping_monitor( @@ -89,9 +85,9 @@ def test_ping_monitor_hits_default_uptimekuma_on_finish(): def test_ping_monitor_custom_uptimekuma_on_finish(): - hook_config = {'server': custom_base_url, 'push_code': push_code} + hook_config = {'push_url': CUSTOM_BASE_URL} flexmock(module.requests).should_receive('get').with_args( - f'{custom_base_url}/api/push/{push_code}?status=up&msg=finish&ping=' + f'{CUSTOM_BASE_URL}?status=up&msg=finish' ).and_return(flexmock(ok=True)).once() module.ping_monitor( @@ -105,7 +101,6 @@ def test_ping_monitor_custom_uptimekuma_on_finish(): def test_ping_monitor_does_not_hit_default_uptimekuma_on_fail_dry_run(): - hook_config = {'push_code': push_code} flexmock(module.requests).should_receive('get').never() module.ping_monitor( @@ -119,7 +114,7 @@ def test_ping_monitor_does_not_hit_default_uptimekuma_on_fail_dry_run(): def test_ping_monitor_does_not_hit_custom_uptimekuma_on_fail_dry_run(): - hook_config = {'server': custom_base_url, 'push_code': push_code} + hook_config = {'push_url': CUSTOM_BASE_URL} flexmock(module.requests).should_receive('get').never() module.ping_monitor( @@ -133,7 +128,6 @@ def test_ping_monitor_does_not_hit_custom_uptimekuma_on_fail_dry_run(): def test_ping_monitor_does_not_hit_default_uptimekuma_on_start_dry_run(): - hook_config = {'push_code': push_code} flexmock(module.requests).should_receive('get').never() module.ping_monitor( @@ -147,7 +141,7 @@ def test_ping_monitor_does_not_hit_default_uptimekuma_on_start_dry_run(): def test_ping_monitor_does_not_hit_custom_uptimekuma_on_start_dry_run(): - hook_config = {'server': custom_base_url, 'push_code': push_code} + hook_config = {'push_url': CUSTOM_BASE_URL} flexmock(module.requests).should_receive('get').never() module.ping_monitor( @@ -161,7 +155,6 @@ def test_ping_monitor_does_not_hit_custom_uptimekuma_on_start_dry_run(): def test_ping_monitor_does_not_hit_default_uptimekuma_on_finish_dry_run(): - hook_config = {'push_code': push_code} flexmock(module.requests).should_receive('get').never() module.ping_monitor( @@ -175,7 +168,7 @@ def test_ping_monitor_does_not_hit_default_uptimekuma_on_finish_dry_run(): def test_ping_monitor_does_not_hit_custom_uptimekuma_on_finish_dry_run(): - hook_config = {'server': custom_base_url, 'push_code': push_code} + hook_config = {'push_url': CUSTOM_BASE_URL} flexmock(module.requests).should_receive('get').never() module.ping_monitor( @@ -189,9 +182,8 @@ def test_ping_monitor_does_not_hit_custom_uptimekuma_on_finish_dry_run(): def test_ping_monitor_with_connection_error_logs_warning(): - hook_config = {'push_code': push_code} flexmock(module.requests).should_receive('get').with_args( - f'{default_base_url}/api/push/{push_code}?status=down&msg=fail&ping=' + f'{DEFAULT_BASE_URL}?status=down&msg=fail' ).and_raise(module.requests.exceptions.ConnectionError) flexmock(module.logger).should_receive('warning').once() @@ -206,13 +198,12 @@ def test_ping_monitor_with_connection_error_logs_warning(): def test_ping_monitor_with_other_error_logs_warning(): - hook_config = {'push_code': push_code} response = flexmock(ok=False) response.should_receive('raise_for_status').and_raise( module.requests.exceptions.RequestException ) flexmock(module.requests).should_receive('post').with_args( - f'{default_base_url}/api/push/{push_code}?status=down&msg=fail&ping=' + f'{DEFAULT_BASE_URL}?status=down&msg=fail' ).and_return(response) flexmock(module.logger).should_receive('warning').once() @@ -224,3 +215,17 @@ def test_ping_monitor_with_other_error_logs_warning(): monitoring_log_level=1, dry_run=False, ) + +def test_ping_monitor_with_invalid_run_state(): + hook_config = {'push_url': CUSTOM_BASE_URL} + flexmock(module.requests).should_receive('get').never() + + module.ping_monitor( + hook_config, + {}, + 'config.yaml', + borgmatic.hooks.monitor.State.LOG, + monitoring_log_level=1, + dry_run=True, + ) + From 4bd798f0ad5eda67a309d3b2df95145b6243e571 Mon Sep 17 00:00:00 2001 From: Paul Wilde Date: Mon, 24 Jun 2024 10:22:27 +0100 Subject: [PATCH 09/22] alpha ordered dispatch monitor hook names (including loki) --- borgmatic/hooks/dispatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/borgmatic/hooks/dispatch.py b/borgmatic/hooks/dispatch.py index 93cae9c..d41ebab 100644 --- a/borgmatic/hooks/dispatch.py +++ b/borgmatic/hooks/dispatch.py @@ -23,6 +23,7 @@ HOOK_NAME_TO_MODULE = { 'cronhub': cronhub, 'cronitor': cronitor, 'healthchecks': healthchecks, + 'loki': loki, 'mariadb_databases': mariadb, 'mongodb_databases': mongodb, 'mysql_databases': mysql, @@ -30,7 +31,6 @@ HOOK_NAME_TO_MODULE = { 'pagerduty': pagerduty, 'postgresql_databases': postgresql, 'sqlite_databases': sqlite, - 'loki': loki, 'uptimekuma': uptimekuma, } From bf7b163ccd4484a93b3dd1d8551314e0ff84a7d8 Mon Sep 17 00:00:00 2001 From: Paul Wilde Date: Mon, 24 Jun 2024 10:25:08 +0100 Subject: [PATCH 10/22] added early out for dry run --- borgmatic/hooks/uptimekuma.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/borgmatic/hooks/uptimekuma.py b/borgmatic/hooks/uptimekuma.py index 59f20df..8b3e6e6 100644 --- a/borgmatic/hooks/uptimekuma.py +++ b/borgmatic/hooks/uptimekuma.py @@ -27,22 +27,21 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev status = 'down' if state.name.lower() == 'fail' else 'up' push_url = hook_config.get('push_url', 'https://example.uptime.kuma/api/push/abcd1234') query = f'status={status}&msg={state.name.lower()}' - logger.info(f'{config_filename}: Pinging Uptime Kuma push_url {push_url}?{query} {dry_run_label}') logger.debug( f'{config_filename}: Full Uptime Kuma state URL {push_url}?{query}' ) - - if not dry_run: - logging.getLogger('urllib3').setLevel(logging.ERROR) - try: - response = requests.get( - f'{push_url}?{query}' - ) - if not response.ok: - response.raise_for_status() - except requests.exceptions.RequestException as error: - logger.warning(f'{config_filename}: Uptime Kuma error: {error}') + if dry_run: + return + logging.getLogger('urllib3').setLevel(logging.ERROR) + try: + response = requests.get( + f'{push_url}?{query}' + ) + if not response.ok: + response.raise_for_status() + except requests.exceptions.RequestException as error: + logger.warning(f'{config_filename}: Uptime Kuma error: {error}') def destroy_monitor( ping_url_or_uuid, config, config_filename, monitoring_log_level, dry_run From 303d6609e473e2acf6d2c53c7122910f6cf8f6ab Mon Sep 17 00:00:00 2001 From: Paul Wilde Date: Mon, 24 Jun 2024 10:27:23 +0100 Subject: [PATCH 11/22] removed unecessary tests for 'default urls' as these would never really be used anyway --- tests/unit/hooks/test_uptimekuma.py | 76 ----------------------------- 1 file changed, 76 deletions(-) diff --git a/tests/unit/hooks/test_uptimekuma.py b/tests/unit/hooks/test_uptimekuma.py index 1d57444..2ebdecc 100644 --- a/tests/unit/hooks/test_uptimekuma.py +++ b/tests/unit/hooks/test_uptimekuma.py @@ -37,22 +37,6 @@ def test_ping_monitor_hits_custom_uptimekuma_on_fail(): dry_run=False, ) - -def test_ping_monitor_hits_default_uptimekuma_on_start(): - flexmock(module.requests).should_receive('get').with_args( - f'{DEFAULT_BASE_URL}?status=up&msg=start' - ).and_return(flexmock(ok=True)).once() - - module.ping_monitor( - hook_config, - {}, - 'config.yaml', - borgmatic.hooks.monitor.State.START, - monitoring_log_level=1, - dry_run=False, - ) - - def test_ping_monitor_custom_uptimekuma_on_start(): hook_config = {'push_url': push_url} flexmock(module.requests).should_receive('get').with_args( @@ -68,22 +52,6 @@ def test_ping_monitor_custom_uptimekuma_on_start(): dry_run=False, ) - -def test_ping_monitor_hits_default_uptimekuma_on_finish(): - flexmock(module.requests).should_receive('get').with_args( - f'{DEFAULT_BASE_URL}?status=up&msg=finish' - ).and_return(flexmock(ok=True)).once() - - module.ping_monitor( - hook_config, - {}, - 'config.yaml', - borgmatic.hooks.monitor.State.FINISH, - monitoring_log_level=1, - dry_run=False, - ) - - def test_ping_monitor_custom_uptimekuma_on_finish(): hook_config = {'push_url': CUSTOM_BASE_URL} flexmock(module.requests).should_receive('get').with_args( @@ -99,20 +67,6 @@ def test_ping_monitor_custom_uptimekuma_on_finish(): dry_run=False, ) - -def test_ping_monitor_does_not_hit_default_uptimekuma_on_fail_dry_run(): - flexmock(module.requests).should_receive('get').never() - - module.ping_monitor( - hook_config, - {}, - 'config.yaml', - borgmatic.hooks.monitor.State.FAIL, - monitoring_log_level=1, - dry_run=True, - ) - - def test_ping_monitor_does_not_hit_custom_uptimekuma_on_fail_dry_run(): hook_config = {'push_url': CUSTOM_BASE_URL} flexmock(module.requests).should_receive('get').never() @@ -126,20 +80,6 @@ def test_ping_monitor_does_not_hit_custom_uptimekuma_on_fail_dry_run(): dry_run=True, ) - -def test_ping_monitor_does_not_hit_default_uptimekuma_on_start_dry_run(): - flexmock(module.requests).should_receive('get').never() - - module.ping_monitor( - hook_config, - {}, - 'config.yaml', - borgmatic.hooks.monitor.State.START, - monitoring_log_level=1, - dry_run=True, - ) - - def test_ping_monitor_does_not_hit_custom_uptimekuma_on_start_dry_run(): hook_config = {'push_url': CUSTOM_BASE_URL} flexmock(module.requests).should_receive('get').never() @@ -153,20 +93,6 @@ def test_ping_monitor_does_not_hit_custom_uptimekuma_on_start_dry_run(): dry_run=True, ) - -def test_ping_monitor_does_not_hit_default_uptimekuma_on_finish_dry_run(): - flexmock(module.requests).should_receive('get').never() - - module.ping_monitor( - hook_config, - {}, - 'config.yaml', - borgmatic.hooks.monitor.State.FINISH, - monitoring_log_level=1, - dry_run=True, - ) - - def test_ping_monitor_does_not_hit_custom_uptimekuma_on_finish_dry_run(): hook_config = {'push_url': CUSTOM_BASE_URL} flexmock(module.requests).should_receive('get').never() @@ -196,7 +122,6 @@ def test_ping_monitor_with_connection_error_logs_warning(): dry_run=False, ) - def test_ping_monitor_with_other_error_logs_warning(): response = flexmock(ok=False) response.should_receive('raise_for_status').and_raise( @@ -228,4 +153,3 @@ def test_ping_monitor_with_invalid_run_state(): monitoring_log_level=1, dry_run=True, ) - From f97968b72df8726488e62e63f931638e2f19387b Mon Sep 17 00:00:00 2001 From: Paul Wilde Date: Mon, 24 Jun 2024 10:34:52 +0100 Subject: [PATCH 12/22] variable renaming --- tests/unit/hooks/test_uptimekuma.py | 33 ++++++++++++++++------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/tests/unit/hooks/test_uptimekuma.py b/tests/unit/hooks/test_uptimekuma.py index 2ebdecc..a5c3ef5 100644 --- a/tests/unit/hooks/test_uptimekuma.py +++ b/tests/unit/hooks/test_uptimekuma.py @@ -3,13 +3,14 @@ from flexmock import flexmock import borgmatic.hooks.monitor from borgmatic.hooks import uptimekuma as module -DEFAULT_BASE_URL = 'https://example.uptime.kuma/api/push/abcd1234' -CUSTOM_BASE_URL = 'https://uptime.example.com/api/push/efgh5678' +DEFAULT_PUSH_URL = 'https://example.uptime.kuma/api/push/abcd1234' +CUSTOM_PUSH_URL = 'https://uptime.example.com/api/push/efgh5678' def test_ping_monitor_hits_default_uptimekuma_on_fail(): + hook_config = {} flexmock(module.requests).should_receive('get').with_args( - f'{DEFAULT_BASE_URL}?status=down&msg=fail' + f'{DEFAULT_PUSH_URL}?status=down&msg=fail' ).and_return(flexmock(ok=True)).once() module.ping_monitor( @@ -23,9 +24,9 @@ def test_ping_monitor_hits_default_uptimekuma_on_fail(): def test_ping_monitor_hits_custom_uptimekuma_on_fail(): - hook_config = {'push_url': push_url} + hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').with_args( - f'{CUSTOM_BASE_URL}?status=down&msg=fail' + f'{CUSTOM_PUSH_URL}?status=down&msg=fail' ).and_return(flexmock(ok=True)).once() module.ping_monitor( @@ -38,9 +39,9 @@ def test_ping_monitor_hits_custom_uptimekuma_on_fail(): ) def test_ping_monitor_custom_uptimekuma_on_start(): - hook_config = {'push_url': push_url} + hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').with_args( - f'{CUSTOM_BASE_URL}?status=up&msg=start' + f'{CUSTOM_PUSH_URL}?status=up&msg=start' ).and_return(flexmock(ok=True)).once() module.ping_monitor( @@ -53,9 +54,9 @@ def test_ping_monitor_custom_uptimekuma_on_start(): ) def test_ping_monitor_custom_uptimekuma_on_finish(): - hook_config = {'push_url': CUSTOM_BASE_URL} + hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').with_args( - f'{CUSTOM_BASE_URL}?status=up&msg=finish' + f'{CUSTOM_PUSH_URL}?status=up&msg=finish' ).and_return(flexmock(ok=True)).once() module.ping_monitor( @@ -68,7 +69,7 @@ def test_ping_monitor_custom_uptimekuma_on_finish(): ) def test_ping_monitor_does_not_hit_custom_uptimekuma_on_fail_dry_run(): - hook_config = {'push_url': CUSTOM_BASE_URL} + hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').never() module.ping_monitor( @@ -81,7 +82,7 @@ def test_ping_monitor_does_not_hit_custom_uptimekuma_on_fail_dry_run(): ) def test_ping_monitor_does_not_hit_custom_uptimekuma_on_start_dry_run(): - hook_config = {'push_url': CUSTOM_BASE_URL} + hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').never() module.ping_monitor( @@ -94,7 +95,7 @@ def test_ping_monitor_does_not_hit_custom_uptimekuma_on_start_dry_run(): ) def test_ping_monitor_does_not_hit_custom_uptimekuma_on_finish_dry_run(): - hook_config = {'push_url': CUSTOM_BASE_URL} + hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').never() module.ping_monitor( @@ -108,8 +109,9 @@ def test_ping_monitor_does_not_hit_custom_uptimekuma_on_finish_dry_run(): def test_ping_monitor_with_connection_error_logs_warning(): + hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').with_args( - f'{DEFAULT_BASE_URL}?status=down&msg=fail' + f'{CUSTOM_PUSH_URL}?status=down&msg=fail' ).and_raise(module.requests.exceptions.ConnectionError) flexmock(module.logger).should_receive('warning').once() @@ -123,12 +125,13 @@ def test_ping_monitor_with_connection_error_logs_warning(): ) def test_ping_monitor_with_other_error_logs_warning(): + hook_config = {'push_url': CUSTOM_PUSH_URL} response = flexmock(ok=False) response.should_receive('raise_for_status').and_raise( module.requests.exceptions.RequestException ) flexmock(module.requests).should_receive('post').with_args( - f'{DEFAULT_BASE_URL}?status=down&msg=fail' + f'{CUSTOM_PUSH_URL}?status=down&msg=fail' ).and_return(response) flexmock(module.logger).should_receive('warning').once() @@ -142,7 +145,7 @@ def test_ping_monitor_with_other_error_logs_warning(): ) def test_ping_monitor_with_invalid_run_state(): - hook_config = {'push_url': CUSTOM_BASE_URL} + hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').never() module.ping_monitor( From 14ce88e04ba2762156142185d9dfeaccb64aef5a Mon Sep 17 00:00:00 2001 From: Paul Wilde Date: Mon, 24 Jun 2024 10:37:44 +0100 Subject: [PATCH 13/22] black formatting on test_uptimekuma.py --- tests/unit/hooks/test_uptimekuma.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/unit/hooks/test_uptimekuma.py b/tests/unit/hooks/test_uptimekuma.py index a5c3ef5..05d5802 100644 --- a/tests/unit/hooks/test_uptimekuma.py +++ b/tests/unit/hooks/test_uptimekuma.py @@ -38,6 +38,7 @@ def test_ping_monitor_hits_custom_uptimekuma_on_fail(): dry_run=False, ) + def test_ping_monitor_custom_uptimekuma_on_start(): hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').with_args( @@ -53,6 +54,7 @@ def test_ping_monitor_custom_uptimekuma_on_start(): dry_run=False, ) + def test_ping_monitor_custom_uptimekuma_on_finish(): hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').with_args( @@ -68,6 +70,7 @@ def test_ping_monitor_custom_uptimekuma_on_finish(): dry_run=False, ) + def test_ping_monitor_does_not_hit_custom_uptimekuma_on_fail_dry_run(): hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').never() @@ -81,6 +84,7 @@ def test_ping_monitor_does_not_hit_custom_uptimekuma_on_fail_dry_run(): dry_run=True, ) + def test_ping_monitor_does_not_hit_custom_uptimekuma_on_start_dry_run(): hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').never() @@ -94,6 +98,7 @@ def test_ping_monitor_does_not_hit_custom_uptimekuma_on_start_dry_run(): dry_run=True, ) + def test_ping_monitor_does_not_hit_custom_uptimekuma_on_finish_dry_run(): hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').never() @@ -124,6 +129,7 @@ def test_ping_monitor_with_connection_error_logs_warning(): dry_run=False, ) + def test_ping_monitor_with_other_error_logs_warning(): hook_config = {'push_url': CUSTOM_PUSH_URL} response = flexmock(ok=False) @@ -144,6 +150,7 @@ def test_ping_monitor_with_other_error_logs_warning(): dry_run=False, ) + def test_ping_monitor_with_invalid_run_state(): hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').never() From 8f423c72939ff435129e97b2931c8d71b44d2eca Mon Sep 17 00:00:00 2001 From: Paul Wilde Date: Mon, 24 Jun 2024 10:46:04 +0100 Subject: [PATCH 14/22] black formatting on uptimekuma hook --- borgmatic/hooks/uptimekuma.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/borgmatic/hooks/uptimekuma.py b/borgmatic/hooks/uptimekuma.py index 8b3e6e6..0ef1c0d 100644 --- a/borgmatic/hooks/uptimekuma.py +++ b/borgmatic/hooks/uptimekuma.py @@ -27,22 +27,21 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev status = 'down' if state.name.lower() == 'fail' else 'up' push_url = hook_config.get('push_url', 'https://example.uptime.kuma/api/push/abcd1234') query = f'status={status}&msg={state.name.lower()}' - logger.info(f'{config_filename}: Pinging Uptime Kuma push_url {push_url}?{query} {dry_run_label}') - logger.debug( - f'{config_filename}: Full Uptime Kuma state URL {push_url}?{query}' + logger.info( + f'{config_filename}: Pinging Uptime Kuma push_url {push_url}?{query} {dry_run_label}' ) + logger.debug(f'{config_filename}: Full Uptime Kuma state URL {push_url}?{query}') if dry_run: return logging.getLogger('urllib3').setLevel(logging.ERROR) try: - response = requests.get( - f'{push_url}?{query}' - ) + response = requests.get(f'{push_url}?{query}') if not response.ok: response.raise_for_status() except requests.exceptions.RequestException as error: logger.warning(f'{config_filename}: Uptime Kuma error: {error}') + def destroy_monitor( ping_url_or_uuid, config, config_filename, monitoring_log_level, dry_run ): # pragma: no cover From b50996b8649bffb71cadb7511382f5a2c5093bb6 Mon Sep 17 00:00:00 2001 From: Paul Wilde Date: Mon, 24 Jun 2024 11:07:09 +0100 Subject: [PATCH 15/22] change uptimekuma method names to 'push_*' instead of 'ping' --- borgmatic/config/schema.yaml | 4 +-- borgmatic/hooks/uptimekuma.py | 14 +++++----- tests/unit/hooks/test_uptimekuma.py | 40 ++++++++++++++--------------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index 5135ce2..1a1c8cf 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -1745,8 +1745,8 @@ properties: - fail uniqueItems: true description: | - List of one or more monitoring states to ping for: "start", - "finish", and/or "fail". Defaults to pinging for all + List of one or more monitoring states to push for: "start", + "finish", and/or "fail". Defaults to pushing for all states. example: - start diff --git a/borgmatic/hooks/uptimekuma.py b/borgmatic/hooks/uptimekuma.py index 0ef1c0d..ca9f4ca 100644 --- a/borgmatic/hooks/uptimekuma.py +++ b/borgmatic/hooks/uptimekuma.py @@ -6,7 +6,7 @@ logger = logging.getLogger(__name__) def initialize_monitor( - ping_url, config, config_filename, monitoring_log_level, dry_run + push_url, config, config_filename, monitoring_log_level, dry_run ): # pragma: no cover ''' No initialization is necessary for this monitor. @@ -14,21 +14,21 @@ def initialize_monitor( pass -def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run): +def push_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run): ''' - Ping the configured Uptime Kuma push_url. + Make a get request to the configured Uptime Kuma push_url. Use the given configuration filename in any log entries. - If this is a dry run, then don't actually ping anything. + If this is a dry run, then don't actually push anything. ''' run_states = hook_config.get('states', ['start', 'finish', 'fail']) if state.name.lower() not in run_states: return - dry_run_label = ' (dry run; not actually pinging)' if dry_run else '' + dry_run_label = ' (dry run; not actually pushing)' if dry_run else '' status = 'down' if state.name.lower() == 'fail' else 'up' push_url = hook_config.get('push_url', 'https://example.uptime.kuma/api/push/abcd1234') query = f'status={status}&msg={state.name.lower()}' logger.info( - f'{config_filename}: Pinging Uptime Kuma push_url {push_url}?{query} {dry_run_label}' + f'{config_filename}: Pushing Uptime Kuma push_url {push_url}?{query} {dry_run_label}' ) logger.debug(f'{config_filename}: Full Uptime Kuma state URL {push_url}?{query}') if dry_run: @@ -43,7 +43,7 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev def destroy_monitor( - ping_url_or_uuid, config, config_filename, monitoring_log_level, dry_run + push_url_or_uuid, config, config_filename, monitoring_log_level, dry_run ): # pragma: no cover ''' No destruction is necessary for this monitor. diff --git a/tests/unit/hooks/test_uptimekuma.py b/tests/unit/hooks/test_uptimekuma.py index 05d5802..fa77b92 100644 --- a/tests/unit/hooks/test_uptimekuma.py +++ b/tests/unit/hooks/test_uptimekuma.py @@ -7,13 +7,13 @@ DEFAULT_PUSH_URL = 'https://example.uptime.kuma/api/push/abcd1234' CUSTOM_PUSH_URL = 'https://uptime.example.com/api/push/efgh5678' -def test_ping_monitor_hits_default_uptimekuma_on_fail(): +def test_push_monitor_hits_default_uptimekuma_on_fail(): hook_config = {} flexmock(module.requests).should_receive('get').with_args( f'{DEFAULT_PUSH_URL}?status=down&msg=fail' ).and_return(flexmock(ok=True)).once() - module.ping_monitor( + module.push_monitor( hook_config, {}, 'config.yaml', @@ -23,13 +23,13 @@ def test_ping_monitor_hits_default_uptimekuma_on_fail(): ) -def test_ping_monitor_hits_custom_uptimekuma_on_fail(): +def test_push_monitor_hits_custom_uptimekuma_on_fail(): hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').with_args( f'{CUSTOM_PUSH_URL}?status=down&msg=fail' ).and_return(flexmock(ok=True)).once() - module.ping_monitor( + module.push_monitor( hook_config, {}, 'config.yaml', @@ -39,13 +39,13 @@ def test_ping_monitor_hits_custom_uptimekuma_on_fail(): ) -def test_ping_monitor_custom_uptimekuma_on_start(): +def test_push_monitor_custom_uptimekuma_on_start(): hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').with_args( f'{CUSTOM_PUSH_URL}?status=up&msg=start' ).and_return(flexmock(ok=True)).once() - module.ping_monitor( + module.push_monitor( hook_config, {}, 'config.yaml', @@ -55,13 +55,13 @@ def test_ping_monitor_custom_uptimekuma_on_start(): ) -def test_ping_monitor_custom_uptimekuma_on_finish(): +def test_push_monitor_custom_uptimekuma_on_finish(): hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').with_args( f'{CUSTOM_PUSH_URL}?status=up&msg=finish' ).and_return(flexmock(ok=True)).once() - module.ping_monitor( + module.push_monitor( hook_config, {}, 'config.yaml', @@ -71,11 +71,11 @@ def test_ping_monitor_custom_uptimekuma_on_finish(): ) -def test_ping_monitor_does_not_hit_custom_uptimekuma_on_fail_dry_run(): +def test_push_monitor_does_not_hit_custom_uptimekuma_on_fail_dry_run(): hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').never() - module.ping_monitor( + module.push_monitor( hook_config, {}, 'config.yaml', @@ -85,11 +85,11 @@ def test_ping_monitor_does_not_hit_custom_uptimekuma_on_fail_dry_run(): ) -def test_ping_monitor_does_not_hit_custom_uptimekuma_on_start_dry_run(): +def test_push_monitor_does_not_hit_custom_uptimekuma_on_start_dry_run(): hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').never() - module.ping_monitor( + module.push_monitor( hook_config, {}, 'config.yaml', @@ -99,11 +99,11 @@ def test_ping_monitor_does_not_hit_custom_uptimekuma_on_start_dry_run(): ) -def test_ping_monitor_does_not_hit_custom_uptimekuma_on_finish_dry_run(): +def test_push_monitor_does_not_hit_custom_uptimekuma_on_finish_dry_run(): hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').never() - module.ping_monitor( + module.push_monitor( hook_config, {}, 'config.yaml', @@ -113,14 +113,14 @@ def test_ping_monitor_does_not_hit_custom_uptimekuma_on_finish_dry_run(): ) -def test_ping_monitor_with_connection_error_logs_warning(): +def test_push_monitor_with_connection_error_logs_warning(): hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').with_args( f'{CUSTOM_PUSH_URL}?status=down&msg=fail' ).and_raise(module.requests.exceptions.ConnectionError) flexmock(module.logger).should_receive('warning').once() - module.ping_monitor( + module.push_monitor( hook_config, {}, 'config.yaml', @@ -130,7 +130,7 @@ def test_ping_monitor_with_connection_error_logs_warning(): ) -def test_ping_monitor_with_other_error_logs_warning(): +def test_push_monitor_with_other_error_logs_warning(): hook_config = {'push_url': CUSTOM_PUSH_URL} response = flexmock(ok=False) response.should_receive('raise_for_status').and_raise( @@ -141,7 +141,7 @@ def test_ping_monitor_with_other_error_logs_warning(): ).and_return(response) flexmock(module.logger).should_receive('warning').once() - module.ping_monitor( + module.push_monitor( hook_config, {}, 'config.yaml', @@ -151,11 +151,11 @@ def test_ping_monitor_with_other_error_logs_warning(): ) -def test_ping_monitor_with_invalid_run_state(): +def test_push_monitor_with_invalid_run_state(): hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').never() - module.ping_monitor( + module.push_monitor( hook_config, {}, 'config.yaml', From 0ee166fdf02cc67dca8bf456ca77a64c2b41ac8c Mon Sep 17 00:00:00 2001 From: Paul Wilde Date: Mon, 24 Jun 2024 11:46:38 +0100 Subject: [PATCH 16/22] added Uptime Kuma how-to docs --- docs/how-to/monitor-your-backups.md | 54 +++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/docs/how-to/monitor-your-backups.md b/docs/how-to/monitor-your-backups.md index 8699b7b..a1a3731 100644 --- a/docs/how-to/monitor-your-backups.md +++ b/docs/how-to/monitor-your-backups.md @@ -46,6 +46,7 @@ them as backups happen: * [ntfy](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#ntfy-hook) * [Grafana Loki](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#loki-hook) * [Apprise](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#apprise-hook) + * [Uptime Kuma](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#uptimekuma-hook) The idea is that you'll receive an alert when something goes wrong or when the service doesn't hear from borgmatic for a configured interval (if supported). @@ -505,6 +506,59 @@ See the [configuration reference](https://torsion.org/borgmatic/docs/reference/configuration/) for details. +## Uptime Kuma hook + +[Uptime Kuma](https://uptime.kuma.pet) is an easy-to-use self-hosted +monitoring tool and can provide a Push monitor type to accept +HTTP `GET` requests from a service instead of contacting it +directly. + +Uptime Kuma allows you to see a history of monitor states and +can in turn alert via Ntfy, Gotify, Matrix, Apprise, Email, and many more. + +An example configuration is shown here with all the available options, + +```yaml +uptimekuma: + push_url: https://kuma.my-domain.com/api/push/abcd1234 + states: + - start + - finish + - fail +``` +The `push_url` is provided to your from your Uptime Kuma service and +includes a query string, the text including and after the question mark ('?'). +Please do not include the query string in the `push_url` configuration, +borgmatic will add this automatically depending on the state of your backup. + +Using `start`, `finish` and `fail` states means you will get two 'up beats' in +Uptime Kuma for successful backups and the ability to see on failures if +and when the backup started (was there a `start` beat?). + +A reasonable base-level configuration for Uptime Kuma Monitor configuration +for a backup is below: + +``` +# These are to be entered into Uptime Kuma and not into your +# borgmatic configuration. + +Monitor Type = Push +# Push monitors wait for the client to contact instead of the reverse +# which is perfect for backup monitoring. + +Heartbeat Interval = 90000 # = 25 hours = 1 day + 1 hour + +# Wait 6 times the heartbeat retry before heartbeat missed +Retries = 6 + +# Multiplied by the "Retries", gives a grace period within which +# the monitor goes into the "Pending" state +Heartbeat Retry = 360 # = 10 minutes + +# For each Heartbeat Interval the backup fails, a notification is sent +# if configured. +Resend Notification every X times = 1 +``` ## Scripting borgmatic From 0837059e212e4dc9d53c5296a37b8b226a6fe54e Mon Sep 17 00:00:00 2001 From: Paul Wilde Date: Mon, 24 Jun 2024 11:51:41 +0100 Subject: [PATCH 17/22] some minor corrections in how to uptime kuma docs --- docs/how-to/monitor-your-backups.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/how-to/monitor-your-backups.md b/docs/how-to/monitor-your-backups.md index a1a3731..9ae4e71 100644 --- a/docs/how-to/monitor-your-backups.md +++ b/docs/how-to/monitor-your-backups.md @@ -516,7 +516,7 @@ directly. Uptime Kuma allows you to see a history of monitor states and can in turn alert via Ntfy, Gotify, Matrix, Apprise, Email, and many more. -An example configuration is shown here with all the available options, +An example configuration is shown here with all the available options: ```yaml uptimekuma: @@ -527,7 +527,7 @@ uptimekuma: - fail ``` The `push_url` is provided to your from your Uptime Kuma service and -includes a query string, the text including and after the question mark ('?'). +includes a query string; the text including and after the question mark ('?'). Please do not include the query string in the `push_url` configuration, borgmatic will add this automatically depending on the state of your backup. @@ -548,15 +548,15 @@ Monitor Type = Push Heartbeat Interval = 90000 # = 25 hours = 1 day + 1 hour -# Wait 6 times the heartbeat retry before heartbeat missed +# Wait 6 times the Heartbeat Retry (below) before logging a heartbeat missed Retries = 6 -# Multiplied by the "Retries", gives a grace period within which +# Multiplied by Retries this gives a grace period within which # the monitor goes into the "Pending" state Heartbeat Retry = 360 # = 10 minutes -# For each Heartbeat Interval the backup fails, a notification is sent -# if configured. +# For each Heartbeat Interval if the backup fails repeatedly, +# a notification is sent each time. Resend Notification every X times = 1 ``` From 939c2f6718da75093cc4a5fa383fc5a2dcc5af5c Mon Sep 17 00:00:00 2001 From: Paul Wilde Date: Mon, 24 Jun 2024 11:54:12 +0100 Subject: [PATCH 18/22] some minor corrections in how to uptime kuma docs --- docs/how-to/monitor-your-backups.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/how-to/monitor-your-backups.md b/docs/how-to/monitor-your-backups.md index 9ae4e71..2017145 100644 --- a/docs/how-to/monitor-your-backups.md +++ b/docs/how-to/monitor-your-backups.md @@ -538,13 +538,14 @@ and when the backup started (was there a `start` beat?). A reasonable base-level configuration for Uptime Kuma Monitor configuration for a backup is below: -``` +```sh # These are to be entered into Uptime Kuma and not into your # borgmatic configuration. Monitor Type = Push -# Push monitors wait for the client to contact instead of the reverse -# which is perfect for backup monitoring. +# Push monitors wait for the client to contact Uptime Kuma +# instead of Uptime Kuma contacting the client. +# This is perfect for backup monitoring. Heartbeat Interval = 90000 # = 25 hours = 1 day + 1 hour From a8d691169a1735bc232c9548df142cb1bd350d54 Mon Sep 17 00:00:00 2001 From: Paul Wilde Date: Mon, 24 Jun 2024 11:54:34 +0100 Subject: [PATCH 19/22] some minor corrections in how to uptime kuma docs --- docs/how-to/monitor-your-backups.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how-to/monitor-your-backups.md b/docs/how-to/monitor-your-backups.md index 2017145..cbebd08 100644 --- a/docs/how-to/monitor-your-backups.md +++ b/docs/how-to/monitor-your-backups.md @@ -538,7 +538,7 @@ and when the backup started (was there a `start` beat?). A reasonable base-level configuration for Uptime Kuma Monitor configuration for a backup is below: -```sh +```ini # These are to be entered into Uptime Kuma and not into your # borgmatic configuration. From 3e6004363237534400636641fbe036851af34a43 Mon Sep 17 00:00:00 2001 From: Paul Wilde Date: Mon, 24 Jun 2024 11:57:12 +0100 Subject: [PATCH 20/22] some minor corrections in how to uptime kuma docs --- docs/how-to/monitor-your-backups.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/how-to/monitor-your-backups.md b/docs/how-to/monitor-your-backups.md index cbebd08..574e905 100644 --- a/docs/how-to/monitor-your-backups.md +++ b/docs/how-to/monitor-your-backups.md @@ -547,14 +547,14 @@ Monitor Type = Push # instead of Uptime Kuma contacting the client. # This is perfect for backup monitoring. -Heartbeat Interval = 90000 # = 25 hours = 1 day + 1 hour +Heartbeat Interval = 90000 # = 25 hours = 1 day + 1 hour # Wait 6 times the Heartbeat Retry (below) before logging a heartbeat missed Retries = 6 # Multiplied by Retries this gives a grace period within which # the monitor goes into the "Pending" state -Heartbeat Retry = 360 # = 10 minutes +Heartbeat Retry = 360 # = 10 minutes # For each Heartbeat Interval if the backup fails repeatedly, # a notification is sent each time. From d108e6102b9595ed04342c1be85ee4cc92b294e6 Mon Sep 17 00:00:00 2001 From: Paul Wilde Date: Mon, 24 Jun 2024 12:00:22 +0100 Subject: [PATCH 21/22] some minor corrections in how to uptime kuma docs --- docs/how-to/monitor-your-backups.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how-to/monitor-your-backups.md b/docs/how-to/monitor-your-backups.md index 574e905..3786d93 100644 --- a/docs/how-to/monitor-your-backups.md +++ b/docs/how-to/monitor-your-backups.md @@ -535,7 +535,7 @@ Using `start`, `finish` and `fail` states means you will get two 'up beats' in Uptime Kuma for successful backups and the ability to see on failures if and when the backup started (was there a `start` beat?). -A reasonable base-level configuration for Uptime Kuma Monitor configuration +A reasonable base-level configuration for an Uptime Kuma Monitor for a backup is below: ```ini From 067c79c606ebffa4fba698a5eef6461f592b523a Mon Sep 17 00:00:00 2001 From: Paul Wilde Date: Wed, 26 Jun 2024 20:57:37 +0100 Subject: [PATCH 22/22] renamed push_monitor back to ping_monitor in uptime kuma hook --- borgmatic/hooks/uptimekuma.py | 2 +- tests/unit/hooks/test_uptimekuma.py | 40 ++++++++++++++--------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/borgmatic/hooks/uptimekuma.py b/borgmatic/hooks/uptimekuma.py index ca9f4ca..75731be 100644 --- a/borgmatic/hooks/uptimekuma.py +++ b/borgmatic/hooks/uptimekuma.py @@ -14,7 +14,7 @@ def initialize_monitor( pass -def push_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run): +def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run): ''' Make a get request to the configured Uptime Kuma push_url. Use the given configuration filename in any log entries. diff --git a/tests/unit/hooks/test_uptimekuma.py b/tests/unit/hooks/test_uptimekuma.py index fa77b92..05d5802 100644 --- a/tests/unit/hooks/test_uptimekuma.py +++ b/tests/unit/hooks/test_uptimekuma.py @@ -7,13 +7,13 @@ DEFAULT_PUSH_URL = 'https://example.uptime.kuma/api/push/abcd1234' CUSTOM_PUSH_URL = 'https://uptime.example.com/api/push/efgh5678' -def test_push_monitor_hits_default_uptimekuma_on_fail(): +def test_ping_monitor_hits_default_uptimekuma_on_fail(): hook_config = {} flexmock(module.requests).should_receive('get').with_args( f'{DEFAULT_PUSH_URL}?status=down&msg=fail' ).and_return(flexmock(ok=True)).once() - module.push_monitor( + module.ping_monitor( hook_config, {}, 'config.yaml', @@ -23,13 +23,13 @@ def test_push_monitor_hits_default_uptimekuma_on_fail(): ) -def test_push_monitor_hits_custom_uptimekuma_on_fail(): +def test_ping_monitor_hits_custom_uptimekuma_on_fail(): hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').with_args( f'{CUSTOM_PUSH_URL}?status=down&msg=fail' ).and_return(flexmock(ok=True)).once() - module.push_monitor( + module.ping_monitor( hook_config, {}, 'config.yaml', @@ -39,13 +39,13 @@ def test_push_monitor_hits_custom_uptimekuma_on_fail(): ) -def test_push_monitor_custom_uptimekuma_on_start(): +def test_ping_monitor_custom_uptimekuma_on_start(): hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').with_args( f'{CUSTOM_PUSH_URL}?status=up&msg=start' ).and_return(flexmock(ok=True)).once() - module.push_monitor( + module.ping_monitor( hook_config, {}, 'config.yaml', @@ -55,13 +55,13 @@ def test_push_monitor_custom_uptimekuma_on_start(): ) -def test_push_monitor_custom_uptimekuma_on_finish(): +def test_ping_monitor_custom_uptimekuma_on_finish(): hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').with_args( f'{CUSTOM_PUSH_URL}?status=up&msg=finish' ).and_return(flexmock(ok=True)).once() - module.push_monitor( + module.ping_monitor( hook_config, {}, 'config.yaml', @@ -71,11 +71,11 @@ def test_push_monitor_custom_uptimekuma_on_finish(): ) -def test_push_monitor_does_not_hit_custom_uptimekuma_on_fail_dry_run(): +def test_ping_monitor_does_not_hit_custom_uptimekuma_on_fail_dry_run(): hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').never() - module.push_monitor( + module.ping_monitor( hook_config, {}, 'config.yaml', @@ -85,11 +85,11 @@ def test_push_monitor_does_not_hit_custom_uptimekuma_on_fail_dry_run(): ) -def test_push_monitor_does_not_hit_custom_uptimekuma_on_start_dry_run(): +def test_ping_monitor_does_not_hit_custom_uptimekuma_on_start_dry_run(): hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').never() - module.push_monitor( + module.ping_monitor( hook_config, {}, 'config.yaml', @@ -99,11 +99,11 @@ def test_push_monitor_does_not_hit_custom_uptimekuma_on_start_dry_run(): ) -def test_push_monitor_does_not_hit_custom_uptimekuma_on_finish_dry_run(): +def test_ping_monitor_does_not_hit_custom_uptimekuma_on_finish_dry_run(): hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').never() - module.push_monitor( + module.ping_monitor( hook_config, {}, 'config.yaml', @@ -113,14 +113,14 @@ def test_push_monitor_does_not_hit_custom_uptimekuma_on_finish_dry_run(): ) -def test_push_monitor_with_connection_error_logs_warning(): +def test_ping_monitor_with_connection_error_logs_warning(): hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').with_args( f'{CUSTOM_PUSH_URL}?status=down&msg=fail' ).and_raise(module.requests.exceptions.ConnectionError) flexmock(module.logger).should_receive('warning').once() - module.push_monitor( + module.ping_monitor( hook_config, {}, 'config.yaml', @@ -130,7 +130,7 @@ def test_push_monitor_with_connection_error_logs_warning(): ) -def test_push_monitor_with_other_error_logs_warning(): +def test_ping_monitor_with_other_error_logs_warning(): hook_config = {'push_url': CUSTOM_PUSH_URL} response = flexmock(ok=False) response.should_receive('raise_for_status').and_raise( @@ -141,7 +141,7 @@ def test_push_monitor_with_other_error_logs_warning(): ).and_return(response) flexmock(module.logger).should_receive('warning').once() - module.push_monitor( + module.ping_monitor( hook_config, {}, 'config.yaml', @@ -151,11 +151,11 @@ def test_push_monitor_with_other_error_logs_warning(): ) -def test_push_monitor_with_invalid_run_state(): +def test_ping_monitor_with_invalid_run_state(): hook_config = {'push_url': CUSTOM_PUSH_URL} flexmock(module.requests).should_receive('get').never() - module.push_monitor( + module.ping_monitor( hook_config, {}, 'config.yaml',