initial pushover commit

This commit is contained in:
Antonio Fernandez 2024-10-30 11:25:26 -04:00
parent 129f3e753c
commit 2849f54932
6 changed files with 531 additions and 0 deletions

View File

@ -1609,6 +1609,214 @@ properties:
example:
- start
- finish
pushover:
type: object
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
type: object
properties:
message:
type: string
description: |
Message to be sent to the user or group.
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"
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: integer
description: |
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
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.
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"
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: integer
description: |
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
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.
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"
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: integer
description: |
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
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

View File

@ -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,

View File

@ -8,6 +8,7 @@ MONITOR_HOOK_NAMES = (
'loki',
'ntfy',
'pagerduty',
'pushover',
'uptime_kuma',
'zabbix',
)

View File

@ -0,0 +1,82 @@
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):
'''
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(),
{
'message': state.name.lower(),
},
)
token = hook_config.get('token')
user = hook_config.get('user')
logger.info(f'{config_filename}: Updating Pushover {dry_run_label}')
if token is None:
logger.warning(f'{config_filename}: Token missing for Pushover')
return
if user is None:
logger.warning(f'{config_filename}: User missing for Pushover')
return
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)
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

View File

@ -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,67 @@ If you have any issues with the integration, [please contact
us](https://torsion.org/borgmatic/#support-and-contributing).
## Pushover hook
<span class="minilink minilink-addedin">New in version 1.9.0</span>
[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 backups fail, 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.
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 <b>Started</b>"
priority: -2
device: "pixel8"
title: "Backup Started"
html: 1
sound: "bike"
ttl: 10
fail:
message: "Backup <font color='#ed4337'>Failed</font>"
priority: -2
device: "pixel8"
title: "Backup Started"
html: 1
sound: "siren"
url: "https://ticketing-system.example.com/login"
url_title: "Login to ticketing system"
states:
- start
- finish
- fail
```
## ntfy hook
<span class="minilink minilink-addedin">New in version 1.6.3</span>

View File

@ -0,0 +1,176 @@
from flexmock import flexmock
import borgmatic.hooks.monitor
from borgmatic.hooks import pushover as module
def test_ping_monitor_config_with_token_only_exit_early():
# This test should exit early since only providing a token is not enough
# for the hook to work
hook_config = {'token': 'ksdjfwoweijfvwoeifvjmwghagy92'}
flexmock(module.logger).should_receive('warning').once()
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=False,
)
def test_ping_monitor_config_with_user_only_exit_early():
# This test should exit early since only providing a token is not enough
# for the hook to work
hook_config = {'user': '983hfe0of902lkjfa2amanfgui'}
flexmock(module.logger).should_receive('warning').once()
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=False,
)
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_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).
hook_config = {
'token': 'ksdjfwoweijfvwoeifvjmwghagy92',
'user': '983hfe0of902lkjfa2amanfgui',
'states': {'start', 'fail', 'finish'},
'start': {'priority': 1},
}
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': 1,
},
).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,
)