diff --git a/NEWS b/NEWS index 38f441bf..60fee340 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,12 @@ * #485: When running commands (database clients, command hooks, etc.), elevate stderr output to show up as borgmatic error logs. * #858: With the "--log-json" flag, log borgmatic's own logs as JSON, not just Borg's. + * #1092: BREAKING: Treat most Borg warnings as errors by default, so for instance backups now fail + when source directories are missing. You can still override this behavior with the + "borg_exit_codes" option. See the documentation for more information: + https://torsion.org/borgmatic/how-to/customize-warnings-and-errors/ + * #1092: Deprecate the "source_directories_must_exist" option, as borgmatic now treats "backup file + not found" warnings from Borg as errors, which accomplishes the same thing. * #1132: BREAKING/SECURITY: For the Healthchecks, Apprise, Pagerduty, and Loki monitoring hooks, disable log sending when not explicitly enabled. This avoids revealing private log information to third-party services. To send logs anyway, set the monitoring hook's "send_logs" option to diff --git a/borgmatic/borg/create.py b/borgmatic/borg/create.py index 84e7d80f..5cd35c7f 100644 --- a/borgmatic/borg/create.py +++ b/borgmatic/borg/create.py @@ -180,6 +180,9 @@ def make_base_create_command( # noqa: PLR0912 open pattern file handle). ''' if config.get('source_directories_must_exist', False): + logger.warning( + 'The "source_directories_must_exist" option is deprecated and will be removed from a future release; borgmatic now errors on missing files as Borg runs' + ) borgmatic.borg.pattern.check_all_root_patterns_exist(patterns) patterns_file = borgmatic.borg.pattern.write_patterns_file( diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index 1af7cb99..38be8776 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -21,11 +21,7 @@ properties: type: string description: | List of source directories and files to back up. Globs and tildes - are expanded. Do not backslash spaces in path names. Be aware that - by default, Borg treats missing source directories as warnings - rather than errors. If you'd like to change that behavior, see - https://torsion.org/borgmatic/how-to/customize-warnings-and-errors/ - or the "source_directories_must_exist" option. + are expanded. Do not backslash spaces in path names. example: - /home - /etc @@ -34,8 +30,11 @@ properties: source_directories_must_exist: type: boolean description: | - If true, then source directories (and root pattern paths) must - exist. If they don't, an error is raised. Defaults to false. + Deprecated. Replaced by borgmatic treating Borg's "backup file not + found" warning as an error by default. But if + "source_directories_must_exist" is true, then source directories + (and root pattern paths) must exist before a backup begins. If they + don't, borgmatic errors. Defaults to false. example: true repositories: type: array @@ -468,9 +467,9 @@ properties: description: | A list of Borg exit codes that should be elevated to errors or squashed to warnings as indicated. By default, Borg error exit codes - (2 to 99) are treated as errors while warning exit codes (1 and - 100+) are treated as warnings. Exit codes other than 1 and 2 are - only present in Borg 1.4.0+. + (2 to 99) are treated as errors and most warning exit codes (1 and + 100+) are treated as errors as well. Exit codes other than 1 and 2 + are only present in Borg 1.4.0+. example: - code: 13 treat_as: warning diff --git a/borgmatic/execute.py b/borgmatic/execute.py index 87d141cc..ea3ea467 100644 --- a/borgmatic/execute.py +++ b/borgmatic/execute.py @@ -16,6 +16,9 @@ ERROR_OUTPUT_MAX_LINE_COUNT = 25 BORG_ERROR_EXIT_CODE_START = 2 BORG_ERROR_EXIT_CODE_END = 99 +# See https://borgbackup.readthedocs.io/en/stable/internals/frontends.html#message-ids +BORG_WARNING_EXIT_CODES_TREATED_AS_ERRORS = {101, 102, 104, 105, 106, 107} + class Exit_status(enum.Enum): STILL_RUNNING = 1 @@ -24,7 +27,7 @@ class Exit_status(enum.Enum): ERROR = 4 -def interpret_exit_code(command, exit_code, borg_local_path=None, borg_exit_codes=None): +def interpret_exit_code(command, exit_code, borg_local_path=None, borg_exit_codes=None): # noqa: PLR0911 ''' Return an Exit_status value (e.g. SUCCESS, ERROR, or WARNING) based on interpreting the given exit code. If a Borg local path is given and matches the process' command, then interpret the @@ -55,8 +58,15 @@ def interpret_exit_code(command, exit_code, borg_local_path=None, borg_exit_code ) return Exit_status.WARNING - # If the exit code doesn't have explicit configuration, then fall back to the default Borg - # behavior. + # If the exit code doesn't have explicit configuration, then fall back to the default + # behavior of treating Borg errors as errors and some Borg warnings as errors. + if exit_code in BORG_WARNING_EXIT_CODES_TREATED_AS_ERRORS: + logger.error( + f'Treating exit code {exit_code} as an error, as per borgmatic defaults', + ) + + return Exit_status.ERROR + return ( Exit_status.ERROR if ( diff --git a/docs/how-to/customize-warnings-and-errors.md b/docs/how-to/customize-warnings-and-errors.md index 3189a385..72edb3c1 100644 --- a/docs/how-to/customize-warnings-and-errors.md +++ b/docs/how-to/customize-warnings-and-errors.md @@ -5,40 +5,44 @@ eleventyNavigation: parent: How-to guides order: 13 --- -After Borg runs, it indicates whether it succeeded via its exit code, a -numeric ID indicating success, warning, or error. borgmatic consumes this exit -code to decide how to respond. Normally, a Borg error results in a borgmatic -error, while a Borg warning or success doesn't. + +After Borg runs, it indicates whether it succeeded via its exit code, a numeric +ID indicating success, warning, or error. borgmatic consumes this exit code to +decide how to respond. By default, Borg errors (and some warnings) result +in a borgmatic error, while Borg successes don't. + +New in borgmatic version 2.1.0 +borgmatic elevates most Borg warnings to errors by default. For instance, if a +source directory is missing during backup, Borg indicates that with a warning +exit code (`107`). And starting in borgmatic 2.1.0, that exit code is considered +an error, so you'll actually find out about missing files. With Borg version 1.4+ If the default behavior isn't sufficient for your needs, you can customize how borgmatic interprets [Borg's exit -codes](https://borgbackup.readthedocs.io/en/stable/usage/general.html#return-codes). +codes](https://borgbackup.readthedocs.io/en/stable/internals/frontends.html#message-ids). -For instance, this borgmatic configuration elevates all Borg backup file -permission warnings (exit code `105`)—and only those warnings—to errors: +For instance, this borgmatic configuration elevates a Borg warning about source files +changes during backup (exit code `100`)—and only those warnings—to +errors: ```yaml borg_exit_codes: - - code: 105 + - code: 100 treat_as: error ``` -The following configuration does that *and* elevates backup file not found -warnings (exit code `107`) to errors as well: +The following configuration does that *and* treats Borg's backup file not found +(exit code `107`) as a warning: ```yaml borg_exit_codes: - - code: 105 + - code: 100 treat_as: error - code: 107 - treat_as: error + treat_as: warning ``` -See the full list of [Borg 1.4 error and warning exit -codes](https://borgbackup.readthedocs.io/en/stable/internals/frontends.html#message-ids). -The `rc:` numeric value there tells you the exit code for each. - If you don't know the exit code for a particular Borg error or warning you're experiencing, you can usually find it in your borgmatic output when `--verbosity 2` is enabled. For instance, here's a snippet of that output when a backup file @@ -50,8 +54,8 @@ is not found: terminating with warning status, rc 107 ``` -So if you want to configure borgmatic to treat this as an error instead of a -warning, the exit status to use is `107`. +So if you want to configure borgmatic to treat this as an warning instead of an +error, the exit status to use is `107`. With Borg version 1.2 and earlier Older versions of Borg didn't support granular exit codes, but still @@ -67,15 +71,4 @@ borg_exit_codes: Be aware though that Borg exits with a warning code for a variety of benign situations such as files changing while they're being read, so this example -may not meet your needs. - -Here's another Borg 1.2 example that squashes Borg errors to warnings: - -```yaml -borg_exit_codes: - - code: 2 - treat_as: warning -``` - -Be careful with this example though, because it prevents borgmatic from -erroring when Borg errors, which may not be desirable. +may not meet your needs. Upgrading to Borg 1.4+ is recommended. diff --git a/tests/unit/test_execute.py b/tests/unit/test_execute.py index 41203794..f0115894 100644 --- a/tests/unit/test_execute.py +++ b/tests/unit/test_execute.py @@ -40,7 +40,22 @@ from borgmatic import execute as module (['borg'], 2, 'borg', [{'code': 99, 'treat_as': 'warning'}], module.Exit_status.ERROR), (['borg'], 2, 'borg', [{'code': 2, 'treat_as': 'warning'}], module.Exit_status.WARNING), (['borg'], 100, 'borg', [{'code': 1, 'treat_as': 'error'}], module.Exit_status.WARNING), + (['borg'], 100, 'borg', [], module.Exit_status.WARNING), (['borg'], 100, 'borg', [{'code': 100, 'treat_as': 'error'}], module.Exit_status.ERROR), + (['borg'], 101, 'borg', [], module.Exit_status.ERROR), + (['borg'], 101, 'borg', [{'code': 101, 'treat_as': 'warning'}], module.Exit_status.WARNING), + (['borg'], 102, 'borg', [], module.Exit_status.ERROR), + (['borg'], 102, 'borg', [{'code': 102, 'treat_as': 'warning'}], module.Exit_status.WARNING), + (['borg'], 103, 'borg', [], module.Exit_status.WARNING), + (['borg'], 103, 'borg', [{'code': 103, 'treat_as': 'error'}], module.Exit_status.ERROR), + (['borg'], 104, 'borg', [], module.Exit_status.ERROR), + (['borg'], 104, 'borg', [{'code': 104, 'treat_as': 'warning'}], module.Exit_status.WARNING), + (['borg'], 105, 'borg', [], module.Exit_status.ERROR), + (['borg'], 105, 'borg', [{'code': 105, 'treat_as': 'warning'}], module.Exit_status.WARNING), + (['borg'], 106, 'borg', [], module.Exit_status.ERROR), + (['borg'], 106, 'borg', [{'code': 106, 'treat_as': 'warning'}], module.Exit_status.WARNING), + (['borg'], 107, 'borg', [], module.Exit_status.ERROR), + (['borg'], 107, 'borg', [{'code': 107, 'treat_as': 'warning'}], module.Exit_status.WARNING), ), ) def test_interpret_exit_code_respects_exit_code_and_borg_local_path(