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/).
+
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,
+ )