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