瀏覽代碼

Add an "only_run_on" option to consistency checks so you can limit a check to running on particular days of the week (#785).

Dan Helfman 11 月之前
父節點
當前提交
593c956d33
共有 5 個文件被更改,包括 196 次插入3 次删除
  1. 3 0
      NEWS
  2. 31 2
      borgmatic/actions/check.py
  3. 42 0
      borgmatic/config/schema.yaml
  4. 51 0
      docs/how-to/deal-with-very-large-backups.md
  5. 69 1
      tests/unit/actions/test_check.py

+ 3 - 0
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.
 

+ 31 - 2
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)'
             )

+ 42 - 0
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: |

+ 51 - 0
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
+
+<span class="minilink minilink-addedin">New in version 1.8.13</span> 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
 
 <span class="minilink minilink-addedin">New in version 1.7.1</span> If you

+ 69 - 1
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',