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 61f40eab..43a5f384 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -1626,6 +1626,265 @@ properties: example: - start - finish + pushover: + type: object + required: ['token', 'user'] + additionalProperties: false + properties: + token: + type: string + description: | + Your application's API token. + example: 7ms6TXHpTokTou2P6x4SodDeentHRa + user: + type: string + description: | + Your user/group key (or that of your target user), viewable + when logged into your dashboard: often referred to as + USER_KEY in Pushover documentation and code examples. + example: hwRwoWsXMBWwgrSecfa9EfPey55WSN + start: + type: object + properties: + message: + type: string + description: | + Message to be sent to the user or group. If omitted + the default is the name of the state. + example: A backup job has started. + priority: + type: integer + description: | + A value of -2, -1, 0 (default), 1 or 2 that + indicates the message priority. + example: 0 + expire: + type: integer + description: | + How many seconds your notification will continue + to be retried (every retry seconds). Defaults to + 600. This settings only applies to priority 2 + notifications. + example: 600 + retry: + type: integer + description: | + The retry parameter specifies how often + (in seconds) the Pushover servers will send the + same notification to the user. Defaults to 30. This + settings only applies to priority 2 notifications. + example: 30 + device: + type: string + description: | + The name of one of your devices to send just to + that device instead of all devices. + example: pixel8 + html: + type: boolean + description: | + Set to True to enable HTML parsing of the message. + Set to False for plain text. + example: True + 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 + example: bike + title: + type: string + description: | + 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. + value of 2. + example: 3600 + url: + type: string + description: | + A supplementary URL to show with your message. + example: https://pushover.net/apps/xxxxx-borgbackup + url_title: + type: string + description: | + A title for the URL specified as the url parameter, + otherwise just the URL is shown. + example: Pushover Link + finish: + type: object + type: object + properties: + message: + type: string + description: | + Message to be sent to the user or group. If omitted + the default is the name of the state. + example: A backup job has finished. + priority: + type: integer + description: | + A value of -2, -1, 0 (default), 1 or 2 that + indicates the message priority. + example: 0 + expire: + type: integer + description: | + How many seconds your notification will continue + to be retried (every retry seconds). Defaults to + 600. This settings only applies to priority 2 + notifications. + example: 600 + retry: + type: integer + description: | + The retry parameter specifies how often + (in seconds) the Pushover servers will send the + same notification to the user. Defaults to 30. This + settings only applies to priority 2 notifications. + example: 30 + device: + type: string + description: | + The name of one of your devices to send just to + that device instead of all devices. + example: pixel8 + html: + type: boolean + description: | + Set to True to enable HTML parsing of the message. + Set to False for plain text. + example: True + 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 + example: bike + title: + type: string + description: | + 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. + value of 2. + example: 3600 + url: + type: string + description: | + A supplementary URL to show with your message. + example: https://pushover.net/apps/xxxxx-borgbackup + url_title: + type: string + description: | + A title for the URL specified as the url parameter, + otherwise just the URL is shown. + example: Pushover Link + fail: + type: object + properties: + message: + type: string + description: | + Message to be sent to the user or group. If omitted + the default is the name of the state. + example: A backup job has failed. + priority: + type: integer + description: | + A value of -2, -1, 0 (default), 1 or 2 that + indicates the message priority. + example: 0 + expire: + type: integer + description: | + How many seconds your notification will continue + to be retried (every retry seconds). Defaults to + 600. This settings only applies to priority 2 + notifications. + example: 600 + retry: + type: integer + description: | + The retry parameter specifies how often + (in seconds) the Pushover servers will send the + same notification to the user. Defaults to 30. This + settings only applies to priority 2 notifications. + example: 30 + device: + type: string + description: | + The name of one of your devices to send just to + that device instead of all devices. + example: pixel8 + html: + type: boolean + description: | + Set to True to enable HTML parsing of the message. + Set to False for plain text. + example: True + 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 + example: bike + title: + type: string + description: | + 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. + value of 2. + example: 3600 + url: + type: string + description: | + A supplementary URL to show with your message. + example: https://pushover.net/apps/xxxxx-borgbackup + url_title: + type: string + description: | + A title for the URL specified as the url parameter, + otherwise just the URL is shown. + example: Pushover Link + 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 failure + only. + example: + - start + - finish zabbix: type: object additionalProperties: false diff --git a/borgmatic/hooks/dispatch.py b/borgmatic/hooks/dispatch.py index be85af8c..4230ffb9 100644 --- a/borgmatic/hooks/dispatch.py +++ b/borgmatic/hooks/dispatch.py @@ -12,6 +12,7 @@ from borgmatic.hooks import ( ntfy, pagerduty, postgresql, + pushover, sqlite, uptimekuma, zabbix, @@ -31,6 +32,7 @@ HOOK_NAME_TO_MODULE = { 'ntfy': ntfy, 'pagerduty': pagerduty, 'postgresql_databases': postgresql, + 'pushover': pushover, 'sqlite_databases': sqlite, 'uptime_kuma': uptimekuma, 'zabbix': zabbix, diff --git a/borgmatic/hooks/monitor.py b/borgmatic/hooks/monitor.py index de4951b7..db44b567 100644 --- a/borgmatic/hooks/monitor.py +++ b/borgmatic/hooks/monitor.py @@ -8,6 +8,7 @@ MONITOR_HOOK_NAMES = ( 'loki', 'ntfy', 'pagerduty', + 'pushover', 'uptime_kuma', 'zabbix', ) diff --git a/borgmatic/hooks/pushover.py b/borgmatic/hooks/pushover.py new file mode 100644 index 00000000..65cc636d --- /dev/null +++ b/borgmatic/hooks/pushover.py @@ -0,0 +1,86 @@ +import logging + +import requests + +logger = logging.getLogger(__name__) + + +EMERGENCY_PRIORITY = 2 + + +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): + ''' + Post a message to the configured Pushover application. + If this is a dry run, then don't actually update anything. + ''' + + run_states = hook_config.get('states', ['fail']) + + if state.name.lower() not in run_states: + return + + dry_run_label = ' (dry run; not actually updating)' if dry_run else '' + + state_config = hook_config.get(state.name.lower(), {}) + + token = hook_config.get('token') + user = hook_config.get('user') + + logger.info(f'{config_filename}: Updating Pushover{dry_run_label}') + + if state_config.get('priority') == EMERGENCY_PRIORITY: + if 'expire' not in state_config: + logger.info(f'{config_filename}: Setting expire to default (10min).') + state_config['expire'] = 600 + if 'retry' not in state_config: + logger.info(f'{config_filename}: Setting retry to default (30sec).') + state_config['retry'] = 30 + else: + if 'expire' in state_config or 'retry' in state_config: + raise ValueError( + 'The configuration parameters retry and expire should not be set when priority is not equal to 2. Please remove them from the configuration.' + ) + + state_config = { + key: (int(value) if key in 'html' else value) for key, value in state_config.items() + } + + data = dict( + { + 'token': token, + 'user': user, + 'message': state.name.lower(), # default to state name. Can be overwritten in state_config loop below. + }, + **state_config, + ) + + if not dry_run: + logging.getLogger('urllib3').setLevel(logging.ERROR) + try: + response = requests.post( + 'https://api.pushover.net/1/messages.json', + headers={'Content-type': 'application/x-www-form-urlencoded'}, + data=data, + ) + if not response.ok: + response.raise_for_status() + except requests.exceptions.RequestException as error: + logger.warning(f'{config_filename}: Pushover 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 diff --git a/docs/how-to/monitor-your-backups.md b/docs/how-to/monitor-your-backups.md index f5007401..daf1adc6 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: * [Healthchecks](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#healthchecks-hook) * [ntfy](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#ntfy-hook) * [PagerDuty](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pagerduty-hook) + * [Pushover](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pushover-hook) * [Uptime Kuma](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#uptime-kuma-hook) * [Zabbix](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#zabbix-hook) @@ -290,6 +291,76 @@ If you have any issues with the integration, [please contact us](https://torsion.org/borgmatic/#support-and-contributing). +## Pushover hook + +New in version 1.9.2 +[Pushover](https://pushover.net) makes it easy to get real-time notifications +on your Android, iPhone, iPad, and Desktop (Android Wear and Apple Watch, +too!). + +First, create a Pushover account and login on your mobile device. Create an +Application in your Pushover dashboard. + +Then, configure borgmatic with your user's unique "User Key" found in your +Pushover dashboard and the unique "API Token" from the created Application. + +Here's a basic example: + + +```yaml +pushover: + token: 7ms6TXHpTokTou2P6x4SodDeentHRa + user: hwRwoWsXMBWwgrSecfa9EfPey55WSN +``` + + +With this configuration, borgmatic creates a Pushover event for your service +whenever borgmatic fails, but only when any of the `create`, `prune`, `compact`, +or `check` actions are run. Note that borgmatic does not contact Pushover +when a backup starts or when it ends without error by default. + +You can configure Pushover to have custom parameters declared for borgmatic's +`start`, `fail` and `finish` hooks states. + +Here's a more advanced example: + + +```yaml +pushover: + token: 7ms6TXHpTokTou2P6x4SodDeentHRa + user: hwRwoWsXMBWwgrSecfa9EfPey55WSN + start: + message: "Backup Started" + priority: -2 + title: "Backup Started" + html: True + ttl: 10 # Message will be deleted after 10 seconds. + fail: + 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 Failed" + html: True + 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: True + ttl: 60 + url: "https://ticketing-system.example.com/login" + url_title: "Login to ticketing system" + states: + - start + - finish + - fail +``` + + ## ntfy hook New in version 1.6.3 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 new file mode 100644 index 00000000..2654aac2 --- /dev/null +++ b/tests/unit/hooks/test_pushover.py @@ -0,0 +1,537 @@ +import pytest +from flexmock import flexmock + +import borgmatic.hooks.monitor +from borgmatic.hooks import pushover as module + + +def test_ping_monitor_config_with_minimum_config_fail_state_backup_successfully_send_to_pushover(): + ''' + This test should be the minimum working configuration. The "message" + should be auto populated with the default value which is the state name. + ''' + hook_config = {'token': 'ksdjfwoweijfvwoeifvjmwghagy92', 'user': '983hfe0of902lkjfa2amanfgui'} + 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': 'fail', + }, + ).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_config_with_minimum_config_start_state_backup_not_send_to_pushover_exit_early(): + ''' + This test should exit early since the hook config does not specify the + 'start' state. Only the 'fail' state is enabled by default. + ''' + hook_config = {'token': 'ksdjfwoweijfvwoeifvjmwghagy92', 'user': '983hfe0of902lkjfa2amanfgui'} + flexmock(module.logger).should_receive('warning').never() + 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_start_state_backup_default_message_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. + ''' + hook_config = { + 'token': 'ksdjfwoweijfvwoeifvjmwghagy92', + 'user': '983hfe0of902lkjfa2amanfgui', + 'states': {'start', 'fail', 'finish'}, + } + 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', + }, + ).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_custom_message_successfully_send_to_pushover(): + ''' + This test should send a notification to Pushover on backup start + since the state has been configured. It should send a custom + 'message' since it is explicitly declared in the state config. + ''' + hook_config = { + 'token': 'ksdjfwoweijfvwoeifvjmwghagy92', + 'user': '983hfe0of902lkjfa2amanfgui', + 'states': {'start', 'fail', 'finish'}, + 'start': {'message': 'custom start message'}, + } + 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': 'custom start message', + }, + ).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_uses_expire_and_retry_defaults(): + ''' + 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': 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': 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_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, 'retry': 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': 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_high_declared_expire_and_retry_ignored_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 a ValueError is + raised. + ''' + 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').never() + with pytest.raises(ValueError): + 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_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': 600, + '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': 600, + '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': 600, + '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': 600, + '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, + ) + + +def test_ping_monitor_config_with_minimum_config_fail_state_backup_successfully_send_to_pushover_dryrun(): + ''' + This test should be the minimum working configuration. The "message" + should be auto populated with the default value which is the state name. + ''' + hook_config = {'token': 'ksdjfwoweijfvwoeifvjmwghagy92', 'user': '983hfe0of902lkjfa2amanfgui'} + 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': 'fail', + }, + ).and_return(flexmock(ok=True)).never() + + module.ping_monitor( + hook_config, + {}, + 'config.yaml', + borgmatic.hooks.monitor.State.FAIL, + monitoring_log_level=1, + dry_run=True, + ) + + +def test_ping_monitor_config_incorrect_state_exit_early(): + ''' + This test should exit early since the start state is not declared in the configuration. + ''' + hook_config = { + 'token': 'ksdjfwoweijfvwoeifvjmwghagy92', + 'user': '983hfe0of902lkjfa2amanfgui', + } + 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', + }, + ).and_return(flexmock(ok=True)).never() + + module.ping_monitor( + hook_config, + {}, + 'config.yaml', + borgmatic.hooks.monitor.State.START, + monitoring_log_level=1, + dry_run=True, + ) + + +def test_ping_monitor_push_post_error_exits_early(): + ''' + This test simulates the Pushover servers not responding with a 200 OK. We + should raise for status and warn then exit. + ''' + hook_config = hook_config = { + 'token': 'ksdjfwoweijfvwoeifvjmwghagy92', + 'user': '983hfe0of902lkjfa2amanfgui', + } + + push_response = flexmock(ok=False) + push_response.should_receive('raise_for_status').and_raise( + module.requests.ConnectionError + ).once() + flexmock(module.requests).should_receive('post').with_args( + 'https://api.pushover.net/1/messages.json', + headers={'Content-type': 'application/x-www-form-urlencoded'}, + data={ + 'token': 'ksdjfwoweijfvwoeifvjmwghagy92', + 'user': '983hfe0of902lkjfa2amanfgui', + 'message': 'fail', + }, + ).and_return(push_response).once() + + flexmock(module.logger).should_receive('warning').once() + + module.ping_monitor( + hook_config, + {}, + 'config.yaml', + borgmatic.hooks.monitor.State.FAIL, + monitoring_log_level=1, + dry_run=False, + )