diff --git a/README.md b/README.md index 45b48a4e..01151e70 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ borgmatic is powered by [Borg Backup](https://www.borgbackup.org/). Cronitor Cronhub PagerDuty +Pushover ntfy Loki Apprise diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index d7668124..222a141d 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -1637,40 +1637,48 @@ properties: priority: type: integer description: | - A value of -2, -1, 0 (default), 1 or 2 that - indicates the message priority. - example: "0" + A value of -2, -1, 0 (default), 1 or 2 that indicates the message priority. + example: 0 + expire: + type: integer + description: | + he expire parameter specifies how many seconds + your notification will continue to be retried + for (every retry seconds). + example: 1200 + retry: + type: integer + description: | + The retry parameter specifies how often + (in seconds) the Pushover servers will send the + same notification to the user. + example: 30 device: type: string description: | - The name of one of your devices to send just to that - device instead of all devices. + The name of one of your devices to send just to that device instead of all devices. example: pixel8 html: type: integer description: | - Set to 1 to enable HTML parsing of the message. Set - to 0 for plain text. + Set to 1 to enable HTML parsing of the message. Set to 0 for plain text. example: 1 sound: type: string description: | - The name of a supported sound to override your - default sound choice. All options can be - found here: https://pushover.net/api#sounds + The name of a supported sound to override your default sound choice. All + options can be found here: https://pushover.net/api#sounds example: bike title: type: string description: | - Your message's title, otherwise your app's - name is used. + Your message's title, otherwise your app's name is used. example: A backup job has started. ttl: type: integer description: | - The number of seconds that the message will live, - before being deleted automatically. The ttl - parameter is ignored for messages with a priority + The number of seconds that the message will live, before being deleted + automatically. The ttl parameter is ignored for messages with a priority value of 2. example: 3600 url: @@ -1681,8 +1689,8 @@ properties: url_title: type: string description: | - A title for the URL specified as the url parameter, - otherwise just the URL is shown. + A title for the URL specified as the url parameter, otherwise just + the URL is shown. example: Pushover Link finish: type: object @@ -1696,40 +1704,48 @@ properties: priority: type: integer description: | - A value of -2, -1, 0 (default), 1 or 2 that - indicates the message priority. - example: "0" + A value of -2, -1, 0 (default), 1 or 2 that indicates the message priority. + example: 0 + expire: + type: integer + description: | + he expire parameter specifies how many seconds + your notification will continue to be retried + for (every retry seconds). + example: 1200 + retry: + type: integer + description: | + The retry parameter specifies how often + (in seconds) the Pushover servers will send the + same notification to the user. + example: 30 device: type: string description: | - The name of one of your devices to send just to that - device instead of all devices. + The name of one of your devices to send just to that device instead of all devices. example: pixel8 html: type: integer description: | - Set to 1 to enable HTML parsing of the message. Set - to 0 for plain text. + Set to 1 to enable HTML parsing of the message. Set to 0 for plain text. example: 1 sound: type: string description: | - The name of a supported sound to override your - default sound choice. All options can be - found here: https://pushover.net/api#sounds + The name of a supported sound to override your default sound choice. All + options can be found here: https://pushover.net/api#sounds example: bike title: type: string description: | - Your message's title, otherwise your app's - name is used. + Your message's title, otherwise your app's name is used. example: A backup job has started. ttl: type: integer description: | - The number of seconds that the message will live, - before being deleted automatically. The ttl - parameter is ignored for messages with a priority + The number of seconds that the message will live, before being deleted + automatically. The ttl parameter is ignored for messages with a priority value of 2. example: 3600 url: @@ -1740,8 +1756,8 @@ properties: url_title: type: string description: | - A title for the URL specified as the url parameter, - otherwise just the URL is shown. + A title for the URL specified as the url parameter, otherwise just + the URL is shown. example: Pushover Link fail: type: object @@ -1754,40 +1770,48 @@ properties: priority: type: integer description: | - A value of -2, -1, 0 (default), 1 or 2 that - indicates the message priority. - example: "0" + A value of -2, -1, 0 (default), 1 or 2 that indicates the message priority. + example: 0 + expire: + type: integer + description: | + he expire parameter specifies how many seconds + your notification will continue to be retried + for (every retry seconds). + example: 1200 + retry: + type: integer + description: | + The retry parameter specifies how often + (in seconds) the Pushover servers will send the + same notification to the user. + example: 30 device: type: string description: | - The name of one of your devices to send just to that - device instead of all devices. + The name of one of your devices to send just to that device instead of all devices. example: pixel8 html: type: integer description: | - Set to 1 to enable HTML parsing of the message. Set - to 0 for plain text. + Set to 1 to enable HTML parsing of the message. Set to 0 for plain text. example: 1 sound: type: string description: | - The name of a supported sound to override your - default sound choice. All options can be - found here: https://pushover.net/api#sounds + The name of a supported sound to override your default sound choice. All + options can be found here: https://pushover.net/api#sounds example: bike title: type: string description: | - Your message's title, otherwise your app's - name is used. + Your message's title, otherwise your app's name is used. example: A backup job has started. ttl: type: integer description: | - The number of seconds that the message will live, - before being deleted automatically. The ttl - parameter is ignored for messages with a priority + The number of seconds that the message will live, before being deleted + automatically. The ttl parameter is ignored for messages with a priority value of 2. example: 3600 url: @@ -1798,8 +1822,8 @@ properties: url_title: type: string description: | - A title for the URL specified as the url parameter, - otherwise just the URL is shown. + A title for the URL specified as the url parameter, otherwise just + the URL is shown. example: Pushover Link states: type: array diff --git a/borgmatic/hooks/pushover.py b/borgmatic/hooks/pushover.py index 73a01c89..ecd6ce28 100644 --- a/borgmatic/hooks/pushover.py +++ b/borgmatic/hooks/pushover.py @@ -46,18 +46,25 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev logger.warning(f'{config_filename}: User missing for Pushover') return + if 'priority' in state_config and state_config['priority'] == 2: + if 'expire' not in state_config: + logger.info(f'{config_filename}: Setting expire to default (10min).') + state_config['expire'] = 1200 + if 'retry' not in state_config: + logger.info(f'{config_filename}: Setting retry to default (30sec).') + state_config['retry'] = 30 + else: + state_config.pop('expire', None) + state_config.pop('retry', None) + data = { 'token': token, 'user': user, 'message': state.name.lower(), # default to state name. Can be overwritten in state_config loop below. } - + for key in state_config: data[key] = state_config[key] - if key == 'priority': - if data['priority'] == 2: - data['expire'] = 30 - data['retry'] = 30 if not dry_run: logging.getLogger('urllib3').setLevel(logging.ERROR) diff --git a/docs/how-to/monitor-your-backups.md b/docs/how-to/monitor-your-backups.md index 9069610f..82de8c28 100644 --- a/docs/how-to/monitor-your-backups.md +++ b/docs/how-to/monitor-your-backups.md @@ -331,20 +331,28 @@ pushover: start: message: "Backup Started" priority: -2 - device: "pixel8" title: "Backup Started" html: 1 - sound: "bike" - ttl: 10 + ttl: 10 # Message will be deleted after 10 seconds. fail: - message: "Backup Failed" - priority: -2 + message: "Backup Failed" + priority: 2 # Requests acknowledgement for messages. + expire: 1200 # Used only for priority 2. Default is 1200 seconds. + retry: 30 # Used only for priority 2. Default is 30 seconds. device: "pixel8" - title: "Backup Started" + title: "Backup Failed" html: 1 sound: "siren" url: "https://ticketing-system.example.com/login" url_title: "Login to ticketing system" + finish: + message: "Backup Finished" + priority: 0 + title: "Backup Finished" + html: 1 + ttl: 60 + url: "https://ticketing-system.example.com/login" + url_title: "Login to ticketing system" states: - start - finish diff --git a/docs/static/pushover.png b/docs/static/pushover.png new file mode 100644 index 00000000..e0ef72a9 Binary files /dev/null and b/docs/static/pushover.png differ diff --git a/tests/unit/hooks/test_pushover.py b/tests/unit/hooks/test_pushover.py index 8484f5ab..b709ae4d 100644 --- a/tests/unit/hooks/test_pushover.py +++ b/tests/unit/hooks/test_pushover.py @@ -142,17 +142,115 @@ def test_ping_monitor_start_state_backup_custom_message_successfully_send_to_pus ) -def test_ping_monitor_start_state_backup_default_message_with_priority_declared_successfully_send_to_pushover(): - # This test should send a notification to Pushover on backup start - # since the state has been configured. It should default to sending - # the name of the state as the 'message' since it is not - # explicitly declared in the state config. It should also send - # with a priority of 1 (high). +def test_ping_monitor_start_state_backup_default_message_with_priority_emergency_declared_no_expiry_or_retry_success(): + # This simulates priority level 2 being set but expiry and retry are + # not declared. This should set retry and expiry to their defaults. hook_config = { 'token': 'ksdjfwoweijfvwoeifvjmwghagy92', 'user': '983hfe0of902lkjfa2amanfgui', 'states': {'start', 'fail', 'finish'}, - 'start': {'priority': 1}, + 'start': {'priority': 2}, + } + flexmock(module.logger).should_receive('warning').never() + flexmock(module.requests).should_receive('post').with_args( + 'https://api.pushover.net/1/messages.json', + headers={'Content-type': 'application/x-www-form-urlencoded'}, + data={ + 'token': 'ksdjfwoweijfvwoeifvjmwghagy92', + 'user': '983hfe0of902lkjfa2amanfgui', + 'message': 'start', + 'priority': 2, + 'retry': 30, + 'expire': 1200, + }, + ).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_start_state_backup_default_message_with_priority_emergency_declared_with_expire_no_retry_success(): + # This simulates priority level 2 and expiry being set but retry is + # not declared. This should set retry to the default. + hook_config = { + 'token': 'ksdjfwoweijfvwoeifvjmwghagy92', + 'user': '983hfe0of902lkjfa2amanfgui', + 'states': {'start', 'fail', 'finish'}, + 'start': {'priority': 2, 'expire': 600}, + } + flexmock(module.logger).should_receive('warning').never() + flexmock(module.requests).should_receive('post').with_args( + 'https://api.pushover.net/1/messages.json', + headers={'Content-type': 'application/x-www-form-urlencoded'}, + data={ + 'token': 'ksdjfwoweijfvwoeifvjmwghagy92', + 'user': '983hfe0of902lkjfa2amanfgui', + 'message': 'start', + 'priority': 2, + 'retry': 30, + 'expire': 600, + }, + ).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_start_state_backup_default_message_with_priority_emergency_declared_no_expire_with_retry_success(): + # This simulates priority level 2 and retry being set but expire is + # not declared. This should set expire to the default. + hook_config = { + 'token': 'ksdjfwoweijfvwoeifvjmwghagy92', + 'user': '983hfe0of902lkjfa2amanfgui', + 'states': {'start', 'fail', 'finish'}, + 'start': {'priority': 2, 'expire': 30}, + } + flexmock(module.logger).should_receive('warning').never() + flexmock(module.requests).should_receive('post').with_args( + 'https://api.pushover.net/1/messages.json', + headers={'Content-type': 'application/x-www-form-urlencoded'}, + data={ + 'token': 'ksdjfwoweijfvwoeifvjmwghagy92', + 'user': '983hfe0of902lkjfa2amanfgui', + 'message': 'start', + 'priority': 2, + 'retry': 30, + 'expire': 30, + }, + ).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_start_state_backup_default_message_with_priority_high_declared_expire_and_retry_delared_success(): + # This simulates priority level 1, retry and expiry being set. Since expire + # and retry are only used for priority level 2, they should not be included + # in the request sent to Pushover. This test verifies that those are + # stripped from the request. + hook_config = { + 'token': 'ksdjfwoweijfvwoeifvjmwghagy92', + 'user': '983hfe0of902lkjfa2amanfgui', + 'states': {'start', 'fail', 'finish'}, + 'start': {'priority': 1, 'expire': 30, 'retry': 30}, } flexmock(module.logger).should_receive('warning').never() flexmock(module.requests).should_receive('post').with_args( @@ -174,3 +272,193 @@ def test_ping_monitor_start_state_backup_default_message_with_priority_declared_ monitoring_log_level=1, dry_run=False, ) + + +def test_ping_monitor_start_state_backup_based_on_documentation_advanced_example_success(): + # Here is a test of what is provided in the monitor-your-backups.md file + # as an 'advanced example'. This test runs the start state. + hook_config = { + 'token': 'ksdjfwoweijfvwoeifvjmwghagy92', + 'user': '983hfe0of902lkjfa2amanfgui', + 'states': {'start', 'fail', 'finish'}, + 'start': { + 'message': 'Backup Started', + 'priority': -2, + 'title': 'Backup Started', + 'html': 1, + 'ttl': 10, + }, + 'fail': { + 'message': 'Backup Failed', + 'priority': 2, + 'expire': 1200, + 'retry': 30, + 'device': 'pixel8', + 'title': 'Backup Failed', + 'html': 1, + 'sound': 'siren', + 'url': 'https://ticketing-system.example.com/login', + 'url_title': 'Login to ticketing system', + }, + 'finish': { + 'message': 'Backup Finished', + 'priority': 0, + 'title': 'Backup Finished', + 'html': 1, + 'ttl': 60, + 'url': 'https://ticketing-system.example.com/login', + 'url_title': 'Login to ticketing system', + }, + } + flexmock(module.logger).should_receive('warning').never() + flexmock(module.requests).should_receive('post').with_args( + 'https://api.pushover.net/1/messages.json', + headers={'Content-type': 'application/x-www-form-urlencoded'}, + data={ + 'token': 'ksdjfwoweijfvwoeifvjmwghagy92', + 'user': '983hfe0of902lkjfa2amanfgui', + 'message': 'Backup Started', + 'priority': -2, + 'title': 'Backup Started', + 'html': 1, + 'ttl': 10, + }, + ).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_fail_state_backup_based_on_documentation_advanced_example_success(): + # Here is a test of what is provided in the monitor-your-backups.md file + # as an 'advanced example'. This test runs the fail state. + hook_config = { + 'token': 'ksdjfwoweijfvwoeifvjmwghagy92', + 'user': '983hfe0of902lkjfa2amanfgui', + 'states': {'start', 'fail', 'finish'}, + 'start': { + 'message': 'Backup Started', + 'priority': -2, + 'title': 'Backup Started', + 'html': 1, + 'ttl': 10, + }, + 'fail': { + 'message': 'Backup Failed', + 'priority': 2, + 'expire': 1200, + 'retry': 30, + 'device': 'pixel8', + 'title': 'Backup Failed', + 'html': 1, + 'sound': 'siren', + 'url': 'https://ticketing-system.example.com/login', + 'url_title': 'Login to ticketing system', + }, + 'finish': { + 'message': 'Backup Finished', + 'priority': 0, + 'title': 'Backup Finished', + 'html': 1, + 'ttl': 60, + 'url': 'https://ticketing-system.example.com/login', + 'url_title': 'Login to ticketing system', + }, + } + flexmock(module.logger).should_receive('warning').never() + flexmock(module.requests).should_receive('post').with_args( + 'https://api.pushover.net/1/messages.json', + headers={'Content-type': 'application/x-www-form-urlencoded'}, + data={ + 'token': 'ksdjfwoweijfvwoeifvjmwghagy92', + 'user': '983hfe0of902lkjfa2amanfgui', + 'message': 'Backup Failed', + 'priority': 2, + 'expire': 1200, + 'retry': 30, + 'device': 'pixel8', + 'title': 'Backup Failed', + 'html': 1, + 'sound': 'siren', + 'url': 'https://ticketing-system.example.com/login', + 'url_title': 'Login to ticketing system', + }, + ).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_finish_state_backup_based_on_documentation_advanced_example_success(): + # Here is a test of what is provided in the monitor-your-backups.md file + # as an 'advanced example'. This test runs the finish state. + hook_config = { + 'token': 'ksdjfwoweijfvwoeifvjmwghagy92', + 'user': '983hfe0of902lkjfa2amanfgui', + 'states': {'start', 'fail', 'finish'}, + 'start': { + 'message': 'Backup Started', + 'priority': -2, + 'title': 'Backup Started', + 'html': 1, + 'ttl': 10, + }, + 'fail': { + 'message': 'Backup Failed', + 'priority': 2, + 'expire': 1200, + 'retry': 30, + 'device': 'pixel8', + 'title': 'Backup Failed', + 'html': 1, + 'sound': 'siren', + 'url': 'https://ticketing-system.example.com/login', + 'url_title': 'Login to ticketing system', + }, + 'finish': { + 'message': 'Backup Finished', + 'priority': 0, + 'title': 'Backup Finished', + 'html': 1, + 'ttl': 60, + 'url': 'https://ticketing-system.example.com/login', + 'url_title': 'Login to ticketing system', + }, + } + flexmock(module.logger).should_receive('warning').never() + flexmock(module.requests).should_receive('post').with_args( + 'https://api.pushover.net/1/messages.json', + headers={'Content-type': 'application/x-www-form-urlencoded'}, + data={ + 'token': 'ksdjfwoweijfvwoeifvjmwghagy92', + 'user': '983hfe0of902lkjfa2amanfgui', + 'message': 'Backup Finished', + 'priority': 0, + 'title': 'Backup Finished', + 'html': 1, + 'ttl': 60, + 'url': 'https://ticketing-system.example.com/login', + 'url_title': 'Login to ticketing system', + }, + ).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, + )