2
0
Antonio Fernandez 7 сар өмнө
parent
commit
2849f54932

+ 208 - 0
borgmatic/config/schema.yaml

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

+ 2 - 0
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,

+ 1 - 0
borgmatic/hooks/monitor.py

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

+ 82 - 0
borgmatic/hooks/pushover.py

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

+ 62 - 0
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,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>

+ 176 - 0
tests/unit/hooks/test_pushover.py

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