WIP: Separate endpoint for each primary action for monitor hooks #660

Closed
jetchirag wants to merge 8 commits from jetchirag/borgmatic:action-based-monitor-endpoints into main
15 changed files with 218 additions and 82 deletions

View File

@ -249,6 +249,12 @@ def make_parsers():
action='store_true',
help='Go through the motions, but do not actually write to any repositories',
)
global_group.add_argument(
'--skip-monitoring',
dest='skip_monitoring',
action='store_true',
help='Skip reporting any data to the configured monitoring services',
)
global_group.add_argument(
'-nc', '--no-color', dest='no_color', action='store_true', help='Disable colored output'
)

View File

@ -68,6 +68,10 @@ def run_configuration(config_filename, config, arguments):
error_repository = ''
using_primary_action = {'create', 'prune', 'compact', 'check'}.intersection(arguments)
jetchirag marked this conversation as resolved Outdated
Outdated
Review

nit(pythonic): next(iter(arguments))

nit(pythonic): `next(iter(arguments))`
Outdated
Review

Also, if we are only using this variable when using_primary_action, it might make sense to rename it to primary_aciton_name and/or move it where it's needed too.

Also, if we are only using this variable when `using_primary_action`, it might make sense to rename it to `primary_aciton_name` and/or move it where it's needed too.

Thanks, that's definitely more effecient.

Thanks, that's definitely more effecient.

If I'm reading this correctly, this will just grab the first action. But there could very well be multiple actions at once, and I'd expect Cronitor to be pinged for each one (if configured for each one).

If I'm reading this correctly, this will just grab the first action. But there could very well be multiple actions at once, and I'd expect Cronitor to be pinged for each one (if configured for each one).

@witten I unexpectedly missed that multiple actions. Will fix it.

@witten I unexpectedly missed that multiple actions. Will fix it.

Why getattr()? Doesn't the attribute always exist now that you've added it to the global arguments? So would just global_arguments.skip_monitoring work? I don't even think you need the False here because action='store_true' above defaults to False if not specified.

Why `getattr()`? Doesn't the attribute always exist now that you've added it to the global arguments? So would just `global_arguments.skip_monitoring` work? I don't even think you need the `False` here because `action='store_true'` above defaults to False if not specified.
monitoring_log_level = verbosity_to_log_level(global_arguments.monitoring_verbosity)
skip_monitoring = global_arguments.skip_monitoring
jetchirag marked this conversation as resolved Outdated

On the isinstance() call: Shouldn't they always be strings? Or is something else sneaking in there?

On the `isinstance()` call: Shouldn't they always be strings? Or is something else sneaking in there?
action_names = [
action for action in arguments.keys() if action != 'global'
]
monitoring_hooks_are_activated = using_primary_action and monitoring_log_level != DISABLED
try:
@ -77,7 +81,7 @@ def run_configuration(config_filename, config, arguments):
return
try:
if monitoring_hooks_are_activated:
if using_primary_action and not skip_monitoring and monitoring_hooks_are_activated:
dispatch.call_hooks(
'initialize_monitor',
config,
@ -87,15 +91,17 @@ def run_configuration(config_filename, config, arguments):
global_arguments.dry_run,
)
dispatch.call_hooks(
'ping_monitor',
config,
config_filename,
monitor.MONITOR_HOOK_NAMES,
monitor.State.START,
monitoring_log_level,
global_arguments.dry_run,
)
for action_name in action_names:
dispatch.call_hooks(
'ping_monitor',
config,
config_filename,
monitor.MONITOR_HOOK_NAMES,
monitor.State.START,
monitoring_log_level,
Review

I know we've going back and forth on the subject of looping over ping_monitor calls, but something occurs to me: Are there cases where this will ping the monitor multiple times even if it's only configured with a single ping_url option? I'm thinking of this code in normalize.py:

        config['hooks']['cronitor'] = {
            'create': cronitor['ping_url'],
            'prune': cronitor['ping_url'],
            'compact': cronitor['ping_url'],
            'check': cronitor['ping_url'],
        }

I think that means with the actions loop in place and ping_url set, Cronitor will get pinged four times for a standard borgmatic run. But shouldn't it ideally only get pinged once on start—unless the user explicitly configures ping endpoints for multiple actions?

So, the question is what to do about this. One idea is to remove the above normalization code so that you can retain the prior behavior when ping_url is set: Only ping once on start. Another idea is to drop support for ping_url entirely. There are probably other options as well. I'm open to discussion on this point of course! I didn't realize this would be such a thorny ticket.

I know we've going back and forth on the subject of looping over `ping_monitor` calls, but something occurs to me: Are there cases where this will ping the monitor multiple times even if it's only configured with a single `ping_url` option? I'm thinking of this code in `normalize.py`: ``` config['hooks']['cronitor'] = { 'create': cronitor['ping_url'], 'prune': cronitor['ping_url'], 'compact': cronitor['ping_url'], 'check': cronitor['ping_url'], } ``` I think that means with the actions loop in place and `ping_url` set, Cronitor will get pinged *four* times for a standard borgmatic run. But shouldn't it ideally only get pinged *once* on start—unless the user explicitly configures ping endpoints for multiple actions? So, the question is what to do about this. One idea is to remove the above normalization code so that you can retain the prior behavior when `ping_url` is set: Only ping *once* on start. Another idea is to drop support for `ping_url` entirely. There are probably other options as well. I'm open to discussion on this point of course! I didn't realize this would be such a thorny ticket.
Review

@witten I had this concern too since I started working on this. I was also wondering if someone wants to receive notification for a failed event in any of 4 actions, he will have to set the endpoints in each of them and same for future update.

And suppose someone wants to get notified for failure in all of these actions, it will send alert 4 times if all of them fail, where probability of other act

So what if we keep the ping_url but add a boolean configuration so user can select which action to receive notification on. I don't think people will use different cronitor endpoint for different actions of same job.

This should still fulfil the problem raised in this PR's issue.

It's been a while since I worked on this so I might be misunderstanding something here. So these are just the thoughts I gathered going over the code again. Let me know what you think.

@witten I had this concern too since I started working on this. I was also wondering if someone wants to receive notification for a failed event in any of 4 actions, he will have to set the endpoints in each of them and same for future update. And suppose someone wants to get notified for failure in all of these actions, it will send alert 4 times if all of them fail, where probability of other act So what if we keep the ping_url but add a boolean configuration so user can select which action to receive notification on. I don't think people will use different cronitor endpoint for different actions of same job. This should still fulfil the problem raised in this PR's issue. It's been a while since I worked on this so I might be misunderstanding something here. So these are just the thoughts I gathered going over the code again. Let me know what you think.
Review

It's also been a while since I've looked at this code! Could you maybe provide a configuration example to illustrate what you're thinking here? For instance, I'm unsure how an additional boolean configuration option would allow a user to select which borgmatic action to receive notifications on. I would think that any action with a Cronitor URL set would receive a notification. Or is the distinction you're drawing that, for a given action, URLs might only be notified for some of begin/start/fail and not all three?

I don't think people will use different cronitor endpoint for different actions of same job.

Looking at #518, I think a common case is probably where the user wants one URL for, say, the create action and a completely different URL if any for, say, the check action. (See #366 as well.) In that case it wouldn't be a problem that each such URL would get pinged once upon failure, as each action is hitting a different Cronitor endpoint.

For the current (released borgmatic) use case of pinging Cronitor only once for begin/start/fail when the action is create, prune, compact, or check.. What do you think of including some logic such that if a URL is specified for multiple different actions (whether explicitly.. or implicitly via ping_url), then borgmatic is smart enough to only ping Cronitor once before/after all the actions and once on failure.

I've given no thought to how that might be implemented, but such de-duplication seems nice from a user perspective—and it lines up with the currently released behavior. Let me know though if this doesn't sound feasible or desirable.

It's also been a while since I've looked at this code! Could you maybe provide a configuration example to illustrate what you're thinking here? For instance, I'm unsure how an additional boolean configuration option would allow a user to select which borgmatic action to receive notifications on. I would think that any action with a Cronitor URL set would receive a notification. Or is the distinction you're drawing that, for a given action, URLs might only be notified for some of begin/start/fail and not all three? > I don't think people will use different cronitor endpoint for different actions of same job. Looking at #518, I think a common case *is* probably where the user wants one URL for, say, the `create` action and a completely different URL if any for, say, the `check` action. (See #366 as well.) In that case it wouldn't be a problem that each such URL would get pinged once upon failure, as each action is hitting a different Cronitor endpoint. For the current (released borgmatic) use case of pinging Cronitor only once for begin/start/fail when the action is create, prune, compact, or check.. What do you think of including some logic such that if a URL is specified for multiple different actions (whether explicitly.. or implicitly via `ping_url`), then borgmatic is smart enough to only ping Cronitor once before/after all the actions and once on failure. I've given no thought to how that might be implemented, but such de-duplication seems nice from a user perspective—and it lines up with the currently released behavior. Let me know though if this doesn't sound feasible or desirable.
Review

It makes sense to include logic to remove duplicate endpoints. I'll add that.

I've fixed the latest merge conflicts and will start working on the rest of suggestions. Development might still be slow unfortunately since classes are starting but I'll keep at it. Since merge conflicts are fixed, I can solve the rest in small pieces.

Thank you for the help and patience!

It makes sense to include logic to remove duplicate endpoints. I'll add that. I've fixed the latest merge conflicts and will start working on the rest of suggestions. Development might still be slow unfortunately since classes are starting but I'll keep at it. Since merge conflicts are fixed, I can solve the rest in small pieces. Thank you for the help and patience!
global_arguments.dry_run,
action_name,
)
except (OSError, CalledProcessError) as error:
if command.considered_soft_failure(config_filename, error):
return
@ -160,17 +166,19 @@ def run_configuration(config_filename, config, arguments):
error_repository = repository['path']
try:
if monitoring_hooks_are_activated:
if using_primary_action and not skip_monitoring and monitoring_hooks_are_activated:
# send logs irrespective of error
dispatch.call_hooks(
'ping_monitor',
config,
config_filename,
monitor.MONITOR_HOOK_NAMES,
monitor.State.LOG,
monitoring_log_level,
global_arguments.dry_run,
)
for action_name in action_names:
dispatch.call_hooks(
'ping_monitor',
config,
config_filename,
monitor.MONITOR_HOOK_NAMES,
monitor.State.LOG,
monitoring_log_level,
global_arguments.dry_run,
action_name,
)
except (OSError, CalledProcessError) as error:
if command.considered_soft_failure(config_filename, error):
return
@ -180,16 +188,18 @@ def run_configuration(config_filename, config, arguments):
if not encountered_error:
try:
if monitoring_hooks_are_activated:
dispatch.call_hooks(
'ping_monitor',
config,
config_filename,
monitor.MONITOR_HOOK_NAMES,
monitor.State.FINISH,
monitoring_log_level,
global_arguments.dry_run,
)
if using_primary_action and not skip_monitoring and monitoring_hooks_are_activated:
for action_name in action_names:
dispatch.call_hooks(
'ping_monitor',
config,
config_filename,
monitor.MONITOR_HOOK_NAMES,
monitor.State.FINISH,
monitoring_log_level,
global_arguments.dry_run,
action_name,
)
dispatch.call_hooks(
'destroy_monitor',
config,
@ -217,15 +227,17 @@ def run_configuration(config_filename, config, arguments):
error=encountered_error,
output=getattr(encountered_error, 'output', ''),
)
dispatch.call_hooks(
'ping_monitor',
config,
config_filename,
monitor.MONITOR_HOOK_NAMES,
monitor.State.FAIL,
monitoring_log_level,
global_arguments.dry_run,
)
for action_name in action_names:
dispatch.call_hooks(
'ping_monitor',
config,
config_filename,
monitor.MONITOR_HOOK_NAMES,
monitor.State.FAIL,
monitoring_log_level,
global_arguments.dry_run,
action_name,
)
dispatch.call_hooks(
'destroy_monitor',
config,

View File

@ -104,6 +104,13 @@ def normalize(config_filename, config):
)
)
config['cronitor'] = {'ping_url': cronitor}
if isinstance(cronitor, dict) and 'ping_url' in cronitor:
config['hooks']['cronitor'] = {
'create': cronitor['ping_url'],
'prune': cronitor['ping_url'],
'compact': cronitor['ping_url'],
'check': cronitor['ping_url'],
}
pagerduty = config.get('pagerduty')
if isinstance(pagerduty, str):

View File

@ -1151,13 +1151,49 @@ properties:
example: incoming_envelope
fail:
type: object
additionalProperties: false
properties:
title:
create:
type: string
description: |
The title of the message.
example: Ping!
message:
Cronitor ping URL to notify when a backup
begins, ends, or errors.
example: https://cronitor.link/d3x0c1
prune:
type: string
description: |
Cronitor ping URL to notify when a prune action
begins, ends, or errors.
example: https://cronitor.link/d3x0c1
compact:
type: string
description: |
Cronitor ping URL to notify when a compact action
begins, ends, or errors.
example: https://cronitor.link/d3x0c1
check:
type: string
description: |
Cronitor ping URL to notify when a check action
begins, ends, or errors.
example: https://cronitor.link/d3x0c1
ping_url:
type: string
description: |
If this is set, other properties will be ignored
and replaced by this value.
example: https://cronitor.link/d3x0c1
description: |
Configuration for a monitoring integration with Cronitor.
Create an account at https://cronitor.io if you'd
like to use this service. See borgmatic monitoring
documentation for details.
pagerduty:
type: object
required: ['integration_key']
additionalProperties: false
properties:
integration_key:
type: string
description: |
The message body to publish.
@ -1242,19 +1278,43 @@ properties:
documentation for details.
cronitor:
type: object
required: ['ping_url']
additionalProperties: false
properties:
create:
type: string
description: |
Cronitor ping URL to notify when a backup
begins, ends, or errors.
example: https://cronitor.link/d3x0c1
prune:
type: string
description: |
Cronitor ping URL to notify when a prune action
begins, ends, or errors.
example: https://cronitor.link/d3x0c1
compact:
type: string
description: |
Cronitor ping URL to notify when a compact action
begins, ends, or errors.
example: https://cronitor.link/d3x0c1
check:
type: string
description: |
Cronitor ping URL to notify when a check action
begins, ends, or errors.
example: https://cronitor.link/d3x0c1
ping_url:
type: string
description: |
Cronitor ping URL to notify when a backup begins,
ends, or errors.
If this is set, other properties will be ignored
and replaced by this value.
example: https://cronitor.link/d3x0c1
description: |
Configuration for a monitoring integration with Cronitor. Create an
account at https://cronitor.io if you'd like to use this service.
See borgmatic monitoring documentation for details.
Configuration for a monitoring integration with Cronitor.
Create an account at https://cronitor.io if you'd
like to use this service. See borgmatic monitoring
documentation for details.
pagerduty:
type: object
required: ['integration_key']

View File

@ -22,7 +22,7 @@ def initialize_monitor(
pass
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run):
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run, action_name):
jetchirag marked this conversation as resolved Outdated

To support the multiple actions thing mentioned above, a couple of options I can think of:

  • Change this to take multiple action names and internally loop accordingly.
  • Or, call ping_monitor() in a loop over the action names.
To support the multiple actions thing mentioned above, a couple of options I can think of: * Change this to take multiple action names and internally loop accordingly. * Or, call `ping_monitor()` in a loop over the action names.

I decided to go with calling ping_monitor in a loop.

I decided to go with calling ping_monitor in a loop.

Sounds good. Please let me know when this is ready for me to have another look!

Sounds good. Please let me know when this is ready for me to have another look!

I think it's ready for a review. Only tests should be missing.

I think it's ready for a review. Only tests should be missing.
'''
Ping the configured Cronhub URL, modified with the monitor.State. Use the given configuration
filename in any log entries. If this is a dry run, then don't actually ping anything.

View File

@ -22,7 +22,7 @@ def initialize_monitor(
pass
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run):
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run, action_name):
'''
Ping the configured Cronitor URL, modified with the monitor.State. Use the given configuration
filename in any log entries. If this is a dry run, then don't actually ping anything.
@ -34,7 +34,13 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
return
dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
ping_url = f"{hook_config['ping_url']}/{MONITOR_STATE_TO_CRONITOR[state]}"
try:
ping_url = f"{hook_config[action_name]}/{MONITOR_STATE_TO_CRONITOR[state]}"
except KeyError:
jetchirag marked this conversation as resolved
Review

Nice, succinct way to skip unconfigured actions. You could optionally logger.debug() in the except so that it's clear what's being skipped and why.

Nice, succinct way to skip unconfigured actions. You could optionally `logger.debug()` in the `except` so that it's clear what's being skipped and why.
logger.debug(
jetchirag marked this conversation as resolved Outdated
Outdated
Review

Missed a debug log here 😄

Missed a debug log here 😄

Thanks! :)

Thanks! :)
f'{config_filename}: Skipping Cronitor {state.name.lower()} ping due to unconfigured action: {action_name}'
)
return
logger.info(f'{config_filename}: Pinging Cronitor {state.name.lower()}{dry_run_label}')
logger.debug(f'{config_filename}: Using Cronitor ping URL {ping_url}')

View File

@ -90,7 +90,7 @@ def initialize_monitor(hook_config, config, config_filename, monitoring_log_leve
)
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run):
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run, action_name):
'''
Ping the configured Healthchecks URL or UUID, modified with the monitor.State. Use the given
configuration filename in any log entries, and log to Healthchecks with the giving log level.

View File

@ -14,7 +14,7 @@ def initialize_monitor(
pass
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run):
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run, action_name):
'''
Ping the configured Ntfy topic. Use the given configuration filename in any log entries.
If this is a dry run, then don't actually ping anything.

View File

@ -21,7 +21,7 @@ def initialize_monitor(
pass
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run):
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run, action_name):
'''
If this is an error state, create a PagerDuty event with the configured integration key. Use
the given configuration filename in any log entries. If this is a dry run, then don't actually

View File

@ -16,7 +16,7 @@ def test_run_configuration_runs_actions_for_each_repository():
expected_results[1:]
)
config = {'repositories': [{'path': 'foo'}, {'path': 'bar'}]}
arguments = {'global': flexmock(monitoring_verbosity=1)}
arguments = {'global': flexmock(monitoring_verbosity=1, skip_monitoring=False)}
results = list(module.run_configuration('test.yaml', config, arguments))
@ -30,7 +30,7 @@ def test_run_configuration_with_invalid_borg_version_errors():
flexmock(module.dispatch).should_receive('call_hooks').never()
flexmock(module).should_receive('run_actions').never()
config = {'repositories': ['foo']}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'prune': flexmock()}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False, skip_monitoring=False), 'prune': flexmock()}
list(module.run_configuration('test.yaml', config, arguments))
@ -45,7 +45,7 @@ def test_run_configuration_logs_monitor_start_error():
flexmock(module).should_receive('log_error_records').and_return(expected_results)
flexmock(module).should_receive('run_actions').never()
config = {'repositories': ['foo']}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False, skip_monitoring=False), 'create': flexmock()}
results = list(module.run_configuration('test.yaml', config, arguments))
@ -60,7 +60,7 @@ def test_run_configuration_bails_for_monitor_start_soft_failure():
flexmock(module).should_receive('log_error_records').never()
flexmock(module).should_receive('run_actions').never()
config = {'repositories': ['foo']}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False, skip_monitoring=False), 'create': flexmock()}
results = list(module.run_configuration('test.yaml', config, arguments))
@ -76,7 +76,7 @@ def test_run_configuration_logs_actions_error():
flexmock(module).should_receive('log_error_records').and_return(expected_results)
flexmock(module).should_receive('run_actions').and_raise(OSError)
config = {'repositories': [{'path': 'foo'}]}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False)}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False, skip_monitoring=False)}
results = list(module.run_configuration('test.yaml', config, arguments))
@ -92,7 +92,7 @@ def test_run_configuration_bails_for_actions_soft_failure():
flexmock(module).should_receive('log_error_records').never()
flexmock(module.command).should_receive('considered_soft_failure').and_return(True)
config = {'repositories': [{'path': 'foo'}]}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False, skip_monitoring=False), 'create': flexmock()}
results = list(module.run_configuration('test.yaml', config, arguments))
@ -109,7 +109,7 @@ def test_run_configuration_logs_monitor_log_error():
flexmock(module).should_receive('log_error_records').and_return(expected_results)
flexmock(module).should_receive('run_actions').and_return([])
config = {'repositories': [{'path': 'foo'}]}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False, skip_monitoring=False), 'create': flexmock()}
results = list(module.run_configuration('test.yaml', config, arguments))
@ -127,7 +127,7 @@ def test_run_configuration_bails_for_monitor_log_soft_failure():
flexmock(module).should_receive('run_actions').and_return([])
flexmock(module.command).should_receive('considered_soft_failure').and_return(True)
config = {'repositories': [{'path': 'foo'}]}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False, skip_monitoring=False), 'create': flexmock()}
results = list(module.run_configuration('test.yaml', config, arguments))
@ -144,7 +144,7 @@ def test_run_configuration_logs_monitor_finish_error():
flexmock(module).should_receive('log_error_records').and_return(expected_results)
flexmock(module).should_receive('run_actions').and_return([])
config = {'repositories': [{'path': 'foo'}]}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False, skip_monitoring=False), 'create': flexmock()}
results = list(module.run_configuration('test.yaml', config, arguments))
@ -162,7 +162,7 @@ def test_run_configuration_bails_for_monitor_finish_soft_failure():
flexmock(module).should_receive('run_actions').and_return([])
flexmock(module.command).should_receive('considered_soft_failure').and_return(True)
config = {'repositories': [{'path': 'foo'}]}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False, skip_monitoring=False), 'create': flexmock()}
results = list(module.run_configuration('test.yaml', config, arguments))
@ -177,7 +177,7 @@ def test_run_configuration_does_not_call_monitoring_hooks_if_monitoring_hooks_ar
flexmock(module).should_receive('run_actions').and_return([])
config = {'repositories': [{'path': 'foo'}]}
arguments = {'global': flexmock(monitoring_verbosity=-2, dry_run=False), 'create': flexmock()}
arguments = {'global': flexmock(monitoring_verbosity=-2, dry_run=False, skip_monitoring=False), 'create': flexmock()}
results = list(module.run_configuration('test.yaml', config, arguments))
assert results == []
@ -192,7 +192,7 @@ def test_run_configuration_logs_on_error_hook_error():
).and_return(expected_results[1:])
flexmock(module).should_receive('run_actions').and_raise(OSError)
config = {'repositories': [{'path': 'foo'}]}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False, skip_monitoring=False), 'create': flexmock()}
results = list(module.run_configuration('test.yaml', config, arguments))
@ -208,7 +208,7 @@ def test_run_configuration_bails_for_on_error_hook_soft_failure():
flexmock(module).should_receive('log_error_records').and_return(expected_results)
flexmock(module).should_receive('run_actions').and_raise(OSError)
config = {'repositories': [{'path': 'foo'}]}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False, skip_monitoring=False), 'create': flexmock()}
results = list(module.run_configuration('test.yaml', config, arguments))
@ -223,7 +223,7 @@ def test_run_configuration_retries_soft_error():
flexmock(module).should_receive('run_actions').and_raise(OSError).and_return([])
flexmock(module).should_receive('log_error_records').and_return([flexmock()]).once()
config = {'repositories': [{'path': 'foo'}], 'retries': 1}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False, skip_monitoring=False), 'create': flexmock()}
results = list(module.run_configuration('test.yaml', config, arguments))
assert results == []
@ -246,7 +246,7 @@ def test_run_configuration_retries_hard_error():
OSError,
).and_return(error_logs)
config = {'repositories': [{'path': 'foo'}], 'retries': 1}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False, skip_monitoring=False), 'create': flexmock()}
results = list(module.run_configuration('test.yaml', config, arguments))
assert results == error_logs
@ -264,7 +264,7 @@ def test_run_configuration_repos_ordered():
'bar: Error running actions for repository', OSError
).and_return(expected_results[1:]).ordered()
config = {'repositories': [{'path': 'foo'}, {'path': 'bar'}]}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False, skip_monitoring=False), 'create': flexmock()}
results = list(module.run_configuration('test.yaml', config, arguments))
assert results == expected_results
@ -298,7 +298,7 @@ def test_run_configuration_retries_round_robin():
'repositories': [{'path': 'foo'}, {'path': 'bar'}],
'retries': 1,
}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False, skip_monitoring=False), 'create': flexmock()}
results = list(module.run_configuration('test.yaml', config, arguments))
assert results == foo_error_logs + bar_error_logs
@ -330,7 +330,7 @@ def test_run_configuration_retries_one_passes():
'repositories': [{'path': 'foo'}, {'path': 'bar'}],
'retries': 1,
}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False, skip_monitoring=False), 'create': flexmock()}
results = list(module.run_configuration('test.yaml', config, arguments))
assert results == error_logs
@ -373,7 +373,7 @@ def test_run_configuration_retry_wait():
'retries': 3,
'retry_wait': 10,
}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False, skip_monitoring=False), 'create': flexmock()}
results = list(module.run_configuration('test.yaml', config, arguments))
assert results == error_logs
@ -412,7 +412,7 @@ def test_run_configuration_retries_timeout_multiple_repos():
'retries': 1,
'retry_wait': 10,
}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False, skip_monitoring=False), 'create': flexmock()}
results = list(module.run_configuration('test.yaml', config, arguments))
assert results == error_logs

View File

@ -16,6 +16,7 @@ def test_ping_monitor_rewrites_ping_url_for_start_state():
module.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
@ -32,6 +33,7 @@ def test_ping_monitor_rewrites_ping_url_and_state_for_start_state():
module.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
@ -48,6 +50,7 @@ def test_ping_monitor_rewrites_ping_url_for_finish_state():
module.monitor.State.FINISH,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
@ -64,6 +67,7 @@ def test_ping_monitor_rewrites_ping_url_for_fail_state():
module.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
@ -78,6 +82,7 @@ def test_ping_monitor_dry_run_does_not_hit_ping_url():
module.monitor.State.START,
monitoring_log_level=1,
dry_run=True,
action_name='create',
)
@ -95,6 +100,7 @@ def test_ping_monitor_with_connection_error_logs_warning():
module.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
@ -116,6 +122,7 @@ def test_ping_monitor_with_other_error_logs_warning():
module.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
@ -130,4 +137,5 @@ def test_ping_monitor_with_unsupported_monitoring_state_bails():
module.monitor.State.LOG,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)

View File

@ -4,7 +4,7 @@ from borgmatic.hooks import cronitor as module
def test_ping_monitor_hits_ping_url_for_start_state():
hook_config = {'ping_url': 'https://example.com'}
hook_config = {'create': 'https://example.com'}
flexmock(module.requests).should_receive('get').with_args('https://example.com/run').and_return(
flexmock(ok=True)
)
@ -16,11 +16,12 @@ def test_ping_monitor_hits_ping_url_for_start_state():
module.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
def test_ping_monitor_hits_ping_url_for_finish_state():
hook_config = {'ping_url': 'https://example.com'}
hook_config = {'create': 'https://example.com'}
flexmock(module.requests).should_receive('get').with_args(
'https://example.com/complete'
).and_return(flexmock(ok=True))
@ -32,11 +33,12 @@ def test_ping_monitor_hits_ping_url_for_finish_state():
module.monitor.State.FINISH,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
def test_ping_monitor_hits_ping_url_for_fail_state():
hook_config = {'ping_url': 'https://example.com'}
hook_config = {'create': 'https://example.com'}
flexmock(module.requests).should_receive('get').with_args(
'https://example.com/fail'
).and_return(flexmock(ok=True))
@ -48,11 +50,12 @@ def test_ping_monitor_hits_ping_url_for_fail_state():
module.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
def test_ping_monitor_dry_run_does_not_hit_ping_url():
hook_config = {'ping_url': 'https://example.com'}
hook_config = {'create': 'https://example.com'}
flexmock(module.requests).should_receive('get').never()
module.ping_monitor(
@ -62,11 +65,12 @@ def test_ping_monitor_dry_run_does_not_hit_ping_url():
module.monitor.State.START,
monitoring_log_level=1,
dry_run=True,
action_name='create',
)
def test_ping_monitor_with_connection_error_logs_warning():
hook_config = {'ping_url': 'https://example.com'}
hook_config = {'create': 'https://example.com'}
flexmock(module.requests).should_receive('get').and_raise(
module.requests.exceptions.ConnectionError
)
@ -79,11 +83,12 @@ def test_ping_monitor_with_connection_error_logs_warning():
module.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
def test_ping_monitor_with_other_error_logs_warning():
hook_config = {'ping_url': 'https://example.com'}
hook_config = {'create': 'https://example.com'}
response = flexmock(ok=False)
response.should_receive('raise_for_status').and_raise(
module.requests.exceptions.RequestException
@ -100,11 +105,12 @@ def test_ping_monitor_with_other_error_logs_warning():
module.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
def test_ping_monitor_with_unsupported_monitoring_state_bails():
hook_config = {'ping_url': 'https://example.com'}
hook_config = {'create': 'https://example.com'}
flexmock(module.requests).should_receive('get').never()
module.ping_monitor(
@ -114,4 +120,5 @@ def test_ping_monitor_with_unsupported_monitoring_state_bails():
module.monitor.State.LOG,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)

View File

@ -148,6 +148,7 @@ def test_ping_monitor_hits_ping_url_for_start_state():
state=module.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
@ -166,6 +167,7 @@ def test_ping_monitor_hits_ping_url_for_finish_state():
state=module.monitor.State.FINISH,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
@ -184,6 +186,7 @@ def test_ping_monitor_hits_ping_url_for_fail_state():
state=module.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
@ -202,6 +205,7 @@ def test_ping_monitor_hits_ping_url_for_log_state():
state=module.monitor.State.LOG,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
@ -222,6 +226,7 @@ def test_ping_monitor_with_ping_uuid_hits_corresponding_url():
state=module.monitor.State.FINISH,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
@ -240,6 +245,7 @@ def test_ping_monitor_skips_ssl_verification_when_verify_tls_false():
state=module.monitor.State.FINISH,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
@ -258,6 +264,7 @@ def test_ping_monitor_executes_ssl_verification_when_verify_tls_true():
state=module.monitor.State.FINISH,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
@ -273,6 +280,7 @@ def test_ping_monitor_dry_run_does_not_hit_ping_url():
state=module.monitor.State.START,
monitoring_log_level=1,
dry_run=True,
action_name='create',
)
@ -288,6 +296,7 @@ def test_ping_monitor_does_not_hit_ping_url_when_states_not_matching():
state=module.monitor.State.START,
monitoring_log_level=1,
dry_run=True,
action_name='create',
)
@ -305,6 +314,7 @@ def test_ping_monitor_hits_ping_url_when_states_matching():
state=module.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
@ -323,6 +333,7 @@ def test_ping_monitor_with_connection_error_logs_warning():
state=module.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
@ -345,4 +356,5 @@ def test_ping_monitor_with_other_error_logs_warning():
state=module.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)

View File

@ -49,6 +49,7 @@ def test_ping_monitor_minimal_config_hits_hosted_ntfy_on_fail():
borgmatic.hooks.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
@ -71,6 +72,7 @@ def test_ping_monitor_with_auth_hits_hosted_ntfy_on_fail():
borgmatic.hooks.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
@ -90,6 +92,7 @@ def test_ping_monitor_auth_with_no_username_warning():
borgmatic.hooks.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
@ -109,6 +112,7 @@ def test_ping_monitor_auth_with_no_password_warning():
borgmatic.hooks.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
@ -123,6 +127,7 @@ def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_start():
borgmatic.hooks.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
@ -137,6 +142,7 @@ def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_finish():
borgmatic.hooks.monitor.State.FINISH,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
@ -155,6 +161,7 @@ def test_ping_monitor_minimal_config_hits_selfhosted_ntfy_on_fail():
borgmatic.hooks.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
@ -169,6 +176,7 @@ def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_fail_dry_run():
borgmatic.hooks.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=True,
action_name='create',
)
@ -185,6 +193,7 @@ def test_ping_monitor_custom_message_hits_hosted_ntfy_on_fail():
borgmatic.hooks.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
@ -203,6 +212,7 @@ def test_ping_monitor_custom_state_hits_hosted_ntfy_on_start():
borgmatic.hooks.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
@ -222,6 +232,7 @@ def test_ping_monitor_with_connection_error_logs_warning():
borgmatic.hooks.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
@ -245,4 +256,5 @@ def test_ping_monitor_with_other_error_logs_warning():
borgmatic.hooks.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)

View File

@ -13,6 +13,7 @@ def test_ping_monitor_ignores_start_state():
module.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
@ -26,6 +27,7 @@ def test_ping_monitor_ignores_finish_state():
module.monitor.State.FINISH,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
@ -39,6 +41,7 @@ def test_ping_monitor_calls_api_for_fail_state():
module.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
@ -52,6 +55,7 @@ def test_ping_monitor_dry_run_does_not_call_api():
module.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=True,
action_name='create',
)
@ -68,6 +72,7 @@ def test_ping_monitor_with_connection_error_logs_warning():
module.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)
@ -86,4 +91,5 @@ def test_ping_monitor_with_other_error_logs_warning():
module.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
action_name='create',
)