From 593c956d33bafe2d50e7039e2ac17ebf66e7479c Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Wed, 26 Jun 2024 14:57:59 -0700 Subject: [PATCH] Add an "only_run_on" option to consistency checks so you can limit a check to running on particular days of the week (#785). --- NEWS | 3 + borgmatic/actions/check.py | 33 +++++++++- borgmatic/config/schema.yaml | 42 +++++++++++++ docs/how-to/deal-with-very-large-backups.md | 51 +++++++++++++++ tests/unit/actions/test_check.py | 70 ++++++++++++++++++++- 5 files changed, 196 insertions(+), 3 deletions(-) diff --git a/NEWS b/NEWS index 94a14e6..65f73e2 100644 --- a/NEWS +++ b/NEWS @@ -1,4 +1,7 @@ 1.8.13.dev0 + * #785: Add an "only_run_on" option to consistency checks so you can limit a check to running on + particular days of the week. See the documentation for more information: + https://torsion.org/borgmatic/docs/how-to/deal-with-very-large-backups/#check-days * #886: Fix a PagerDuty hook traceback with Python < 3.10. * #889: Fix the Healthchecks ping body size limit, restoring it to the documented 100,000 bytes. diff --git a/borgmatic/actions/check.py b/borgmatic/actions/check.py index bded12a..26df365 100644 --- a/borgmatic/actions/check.py +++ b/borgmatic/actions/check.py @@ -1,3 +1,4 @@ +import calendar import datetime import hashlib import itertools @@ -99,12 +100,17 @@ def parse_frequency(frequency): raise ValueError(f"Could not parse consistency check frequency '{frequency}'") +WEEKDAY_DAYS = calendar.day_name[0:5] +WEEKEND_DAYS = calendar.day_name[5:7] + + def filter_checks_on_frequency( config, borg_repository_id, checks, force, archives_check_id=None, + datetime_now=datetime.datetime.now, ): ''' Given a configuration dict with a "checks" sequence of dicts, a Borg repository ID, a sequence @@ -143,6 +149,29 @@ def filter_checks_on_frequency( if checks and check not in checks: continue + only_run_on = check_config.get('only_run_on') + if only_run_on: + # Use a dict instead of a set to preserve ordering. + days = dict.fromkeys(only_run_on) + + if 'weekday' in days: + days = { + **dict.fromkeys(day for day in days if day != 'weekday'), + **dict.fromkeys(WEEKDAY_DAYS), + } + if 'weekend' in days: + days = { + **dict.fromkeys(day for day in days if day != 'weekend'), + **dict.fromkeys(WEEKEND_DAYS), + } + + if calendar.day_name[datetime_now().weekday()] not in days: + logger.info( + f"Skipping {check} check due to day of the week; check only runs on {'/'.join(days)} (use --force to check anyway)" + ) + filtered_checks.remove(check) + continue + frequency_delta = parse_frequency(check_config.get('frequency')) if not frequency_delta: continue @@ -153,8 +182,8 @@ def filter_checks_on_frequency( # If we've not yet reached the time when the frequency dictates we're ready for another # check, skip this check. - if datetime.datetime.now() < check_time + frequency_delta: - remaining = check_time + frequency_delta - datetime.datetime.now() + if datetime_now() < check_time + frequency_delta: + remaining = check_time + frequency_delta - datetime_now() logger.info( f'Skipping {check} check due to configured frequency; {remaining} until next check (use --force to check anyway)' ) diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index 56a8987..9177cc7 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -546,6 +546,20 @@ properties: "always": running this check every time checks are run. example: 2 weeks + only_run_on: + type: array + items: + type: string + description: | + After the "frequency" duration has elapsed, only + run this check if the current day of the week + matches one of these values (the name of a day of + the week in the current locale). "weekday" and + "weekend" are also accepted. Defaults to running + the check on any day of the week. + example: + - Saturday + - Sunday - required: [name] additionalProperties: false properties: @@ -579,6 +593,20 @@ properties: "always": running this check every time checks are run. example: 2 weeks + only_run_on: + type: array + items: + type: string + description: | + After the "frequency" duration has elapsed, only + run this check if the current day of the week + matches one of these values (the name of a day of + the week in the current locale). "weekday" and + "weekend" are also accepted. Defaults to running + the check on any day of the week. + example: + - Saturday + - Sunday max_duration: type: integer description: | @@ -627,6 +655,20 @@ properties: "always": running this check every time checks are run. example: 2 weeks + only_run_on: + type: array + items: + type: string + description: | + After the "frequency" duration has elapsed, only + run this check if the current day of the week + matches one of these values (the name of a day of + the week in the current locale). "weekday" and + "weekend" are also accepted. Defaults to running + the check on any day of the week. + example: + - Saturday + - Sunday count_tolerance_percentage: type: number description: | diff --git a/docs/how-to/deal-with-very-large-backups.md b/docs/how-to/deal-with-very-large-backups.md index 9a01467..53fe069 100644 --- a/docs/how-to/deal-with-very-large-backups.md +++ b/docs/how-to/deal-with-very-large-backups.md @@ -242,6 +242,57 @@ check --force` runs `check` even if it's specified in the `skip_actions` option. +### Check days + +New in version 1.8.13 You can +optionally configure checks to only run on particular days of the week. For +instance: + +```yaml +checks: + - name: repository + only_run_on: + - Saturday + - Sunday + - name: archives + only_run_on: + - weekday + - name: spot + only_run_on: + - Friday + - weekend +``` + +Each day of the week is specified in the current locale (system +language/country settings). `weekend` and `weekday` are also accepted. + +Just like with `frequency`, borgmatic only makes a best effort to run checks +on the given day of the week. For instance, if you run `borgmatic check` +daily, then every day borgmatic will have an opportunity to determine whether +your checks are configured to run on that day. If they are, then the checks +run. If not, they are skipped. + +For instance, with the above configuration, if borgmatic is run on a Saturday, +the `repository` check will run. But on a Monday? The repository check will +get skipped. And if borgmatic is never run on a Saturday or a Sunday, that +check will never get a chance to run. + +Also, the day of the week configuration applies *after* any configured +`frequency` for a check. So for instance, imagine the following configuration: + +```yaml +checks: + - name: repository + frequency: 2 weeks + only_run_on: + - Monday +``` + +If you run borgmatic daily with that configuration, then borgmatic will first +wait two weeks after the previous check before running the check again—on the +first Monday after the `frequency` duration elapses. + + ### Running only checks New in version 1.7.1 If you diff --git a/tests/unit/actions/test_check.py b/tests/unit/actions/test_check.py index 3a79360..ee81278 100644 --- a/tests/unit/actions/test_check.py +++ b/tests/unit/actions/test_check.py @@ -113,6 +113,74 @@ def test_filter_checks_on_frequency_retains_check_without_frequency(): ) == ('archives',) +def test_filter_checks_on_frequency_retains_check_with_empty_only_run_on(): + flexmock(module).should_receive('parse_frequency').and_return(None) + + assert module.filter_checks_on_frequency( + config={'checks': [{'name': 'archives', 'only_run_on': []}]}, + borg_repository_id='repo', + checks=('archives',), + force=False, + archives_check_id='1234', + datetime_now=flexmock(weekday=lambda: 0), + ) == ('archives',) + + +def test_filter_checks_on_frequency_retains_check_with_only_run_on_matching_today(): + flexmock(module).should_receive('parse_frequency').and_return(None) + + assert module.filter_checks_on_frequency( + config={'checks': [{'name': 'archives', 'only_run_on': [module.calendar.day_name[0]]}]}, + borg_repository_id='repo', + checks=('archives',), + force=False, + archives_check_id='1234', + datetime_now=flexmock(weekday=lambda: 0), + ) == ('archives',) + + +def test_filter_checks_on_frequency_retains_check_with_only_run_on_matching_today_via_weekday_value(): + flexmock(module).should_receive('parse_frequency').and_return(None) + + assert module.filter_checks_on_frequency( + config={'checks': [{'name': 'archives', 'only_run_on': ['weekday']}]}, + borg_repository_id='repo', + checks=('archives',), + force=False, + archives_check_id='1234', + datetime_now=flexmock(weekday=lambda: 0), + ) == ('archives',) + + +def test_filter_checks_on_frequency_retains_check_with_only_run_on_matching_today_via_weekend_value(): + flexmock(module).should_receive('parse_frequency').and_return(None) + + assert module.filter_checks_on_frequency( + config={'checks': [{'name': 'archives', 'only_run_on': ['weekend']}]}, + borg_repository_id='repo', + checks=('archives',), + force=False, + archives_check_id='1234', + datetime_now=flexmock(weekday=lambda: 6), + ) == ('archives',) + + +def test_filter_checks_on_frequency_skips_check_with_only_run_on_not_matching_today(): + flexmock(module).should_receive('parse_frequency').and_return(None) + + assert ( + module.filter_checks_on_frequency( + config={'checks': [{'name': 'archives', 'only_run_on': [module.calendar.day_name[5]]}]}, + borg_repository_id='repo', + checks=('archives',), + force=False, + archives_check_id='1234', + datetime_now=flexmock(weekday=lambda: 0), + ) + == () + ) + + def test_filter_checks_on_frequency_retains_check_with_elapsed_frequency(): flexmock(module).should_receive('parse_frequency').and_return( module.datetime.timedelta(hours=1) @@ -168,7 +236,7 @@ def test_filter_checks_on_frequency_skips_check_with_unelapsed_frequency(): ) -def test_filter_checks_on_frequency_restains_check_with_unelapsed_frequency_and_force(): +def test_filter_checks_on_frequency_retains_check_with_unelapsed_frequency_and_force(): assert module.filter_checks_on_frequency( config={'checks': [{'name': 'archives', 'frequency': '1 hour'}]}, borg_repository_id='repo',