diff --git a/AUTHORS b/AUTHORS index d66be94f..500a9d67 100644 --- a/AUTHORS +++ b/AUTHORS @@ -7,6 +7,6 @@ Johannes Feichtner: Support for user hooks Michele Lazzeri: Custom archive names Nick Whyte: Support prefix filtering for archive consistency checks newtonne: Read encryption password from external file -Robin `ypid` Schneider: Support additional options of Borg +Robin `ypid` Schneider: Support additional options of Borg and add --validate action Scott Squires: Custom archive names Thomas LÉVEIL: Support for a keep_minutely prune option. Support for the --json option diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index 4ab5a75b..20e8f64f 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -47,6 +47,9 @@ def parse_arguments(*arguments): ) actions_group = parser.add_argument_group('actions') + actions_group.add_argument( + '-V', '--validate', dest='validate', action='store_true', help='Validate borgmatic configuration file and exit' + ) actions_group.add_argument( '-I', '--init', dest='init', action='store_true', help='Initialize an empty Borg repository' ) @@ -420,23 +423,24 @@ def collect_configuration_run_summary_logs(config_filenames, args): # Execute the actions corresponding to each configuration file. json_results = [] - for config_filename, config in configs.items(): - try: - json_results.extend(list(run_configuration(config_filename, config, args))) - yield logging.makeLogRecord( - dict( - levelno=logging.INFO, - msg='{}: Successfully ran configuration file'.format(config_filename), + if not args.validate: + for config_filename, config in configs.items(): + try: + json_results.extend(list(run_configuration(config_filename, config, args))) + yield logging.makeLogRecord( + dict( + levelno=logging.INFO, + msg='{}: Successfully ran configuration file'.format(config_filename), + ) ) - ) - except (ValueError, OSError, CalledProcessError) as error: - yield logging.makeLogRecord( - dict( - levelno=logging.CRITICAL, - msg='{}: Error running configuration file'.format(config_filename), + except (ValueError, OSError, CalledProcessError) as error: + yield logging.makeLogRecord( + dict( + levelno=logging.CRITICAL, + msg='{}: Error running configuration file'.format(config_filename), + ) ) - ) - yield logging.makeLogRecord(dict(levelno=logging.CRITICAL, msg=error)) + yield logging.makeLogRecord(dict(levelno=logging.CRITICAL, msg=error)) if json_results: sys.stdout.write(json.dumps(json_results)) @@ -475,6 +479,7 @@ def main(): # pragma: no cover sys.exit(0) config_filenames = tuple(collect.collect_config_filenames(args.config_paths)) + logger.debug('Ensuring legacy configuration is upgraded') convert.guard_configuration_upgraded(LEGACY_CONFIG_PATH, config_filenames) @@ -484,4 +489,9 @@ def main(): # pragma: no cover [logger.handle(log) for log in summary_logs if log.levelno >= logger.getEffectiveLevel()] if any(log.levelno == logging.CRITICAL for log in summary_logs): + if args.validate: + sys.exit(1) exit_with_help_link() + + if args.validate: + print('Configuration files are valid.') diff --git a/tests/integration/commands/test_borgmatic.py b/tests/integration/commands/test_borgmatic.py index 704dc7c9..dae26628 100644 --- a/tests/integration/commands/test_borgmatic.py +++ b/tests/integration/commands/test_borgmatic.py @@ -268,3 +268,19 @@ def test_borgmatic_version_matches_news_version(): news_version = open('NEWS').readline() assert borgmatic_version == news_version + + +def test_borgmatic_validate_error(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + borgmatic_output = subprocess.check_output(('borgmatic', '--validate')).decode('ascii') + + assert borgmatic_output == 'Configuration files are valid.' + + +def test_borgmatic_validate_ok(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + borgmatic_output = subprocess.check_output(('borgmatic', '--validate')).decode('ascii') + + assert borgmatic_output == 'Configuration files are valid.' diff --git a/tests/unit/commands/test_borgmatic.py b/tests/unit/commands/test_borgmatic.py index e5610878..be0af053 100644 --- a/tests/unit/commands/test_borgmatic.py +++ b/tests/unit/commands/test_borgmatic.py @@ -6,7 +6,7 @@ from borgmatic.commands import borgmatic as module def test_collect_configuration_run_summary_logs_info_for_success(): flexmock(module.validate).should_receive('parse_configuration').and_return({'test.yaml': {}}) flexmock(module).should_receive('run_configuration').and_return([]) - args = flexmock(extract=False, list=False) + args = flexmock(validate=False, extract=False, list=False) logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=args)) @@ -17,7 +17,7 @@ def test_collect_configuration_run_summary_logs_info_for_success_with_extract(): flexmock(module.validate).should_receive('parse_configuration').and_return({'test.yaml': {}}) flexmock(module.validate).should_receive('guard_configuration_contains_repository') flexmock(module).should_receive('run_configuration').and_return([]) - args = flexmock(extract=True, list=False, repository='repo') + args = flexmock(validate=False, extract=True, list=False, repository='repo') logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=args)) @@ -51,7 +51,7 @@ def test_collect_configuration_run_summary_logs_critical_for_list_with_archive_a def test_collect_configuration_run_summary_logs_info_for_success_with_list(): flexmock(module.validate).should_receive('parse_configuration').and_return({'test.yaml': {}}) flexmock(module).should_receive('run_configuration').and_return([]) - args = flexmock(extract=False, list=True, repository='repo', archive=None) + args = flexmock(validate=False, extract=False, list=True, repository='repo', archive=None) logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=args)) @@ -60,7 +60,7 @@ def test_collect_configuration_run_summary_logs_info_for_success_with_list(): def test_collect_configuration_run_summary_logs_critical_for_parse_error(): flexmock(module.validate).should_receive('parse_configuration').and_raise(ValueError) - args = flexmock(extract=False, list=False) + args = flexmock(validate=False, extract=False, list=False) logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=args)) @@ -71,7 +71,7 @@ def test_collect_configuration_run_summary_logs_critical_for_run_error(): flexmock(module.validate).should_receive('parse_configuration').and_return({'test.yaml': {}}) flexmock(module.validate).should_receive('guard_configuration_contains_repository') flexmock(module).should_receive('run_configuration').and_raise(ValueError) - args = flexmock(extract=False, list=False) + args = flexmock(validate=False, extract=False, list=False) logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=args)) @@ -81,7 +81,7 @@ def test_collect_configuration_run_summary_logs_critical_for_run_error(): def test_collect_configuration_run_summary_logs_critical_for_missing_configs(): flexmock(module.validate).should_receive('parse_configuration').and_return({'test.yaml': {}}) flexmock(module).should_receive('run_configuration').and_return([]) - args = flexmock(config_paths=(), extract=False, list=False) + args = flexmock(config_paths=(), validate=False, extract=False, list=False) logs = tuple(module.collect_configuration_run_summary_logs(config_filenames=(), args=args)) @@ -96,6 +96,6 @@ def test_collect_configuration_run_summary_logs_outputs_merged_json_results(): ['baz'] ) flexmock(module.sys.stdout).should_receive('write').with_args('["foo", "bar", "baz"]').once() - args = flexmock(extract=False, list=False) + args = flexmock(validate=False, extract=False, list=False) tuple(module.collect_configuration_run_summary_logs(('test.yaml', 'test2.yaml'), args=args))