diff --git a/NEWS b/NEWS index 5c6b5db8..81dffc7f 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,7 @@ +1.2.14.dev0 + * #116: When running multiple configuration files, attempt all configuration files even if one of + them errors. Log a summary of results at the end. + 1.2.13 * #100: Support for --stats command-line flag independent of --verbosity. * #117: With borgmatic --init command-line flag, proceed without erroring if a repository already diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index 69b9ef09..ae430563 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -29,7 +29,7 @@ LEGACY_CONFIG_PATH = '/etc/borgmatic/config' def parse_arguments(*arguments): ''' Given command-line arguments with which this script was invoked, parse the arguments and return - them as an ArgumentParser instance. + them as an argparse.ArgumentParser instance. ''' config_paths = collect.get_default_config_paths() @@ -308,25 +308,53 @@ def _run_commands_on_repository( sys.stdout.write(output) -def main(): # pragma: no cover - try: - configure_signals() - args = parse_arguments(*sys.argv[1:]) - logging.basicConfig(level=verbosity_to_log_level(args.verbosity), format='%(message)s') - - 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) - - if len(config_filenames) == 0: - raise ValueError( - 'Error: No configuration files found in: {}'.format(' '.join(args.config_paths)) - ) - - for config_filename in config_filenames: +def collect_configuration_run_summary_logs(config_filenames, args): + ''' + Given a sequence of configuration filenames and parsed command-line arguments as an + argparse.ArgumentParser instance, run each configuration file and yield a series of + logging.LogRecord instances containing summary information about each run. + ''' + for config_filename in config_filenames: + try: run_configuration(config_filename, args) - except (ValueError, OSError, CalledProcessError) as error: - print(error, file=sys.stderr) - print(file=sys.stderr) - print('Need some help? https://torsion.org/borgmatic/#issues', file=sys.stderr) + 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), + ) + ) + yield logging.makeLogRecord(dict(levelno=logging.CRITICAL, msg=error)) + + if not config_filenames: + yield logging.makeLogRecord( + dict( + levelno=logging.CRITICAL, + msg='{}: No configuration files found'.format(' '.join(args.config_paths)), + ) + ) + + +def main(): # pragma: no cover + configure_signals() + args = parse_arguments(*sys.argv[1:]) + logging.basicConfig(level=verbosity_to_log_level(args.verbosity), format='%(message)s') + + 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) + + summary_logs = tuple(collect_configuration_run_summary_logs(config_filenames, args)) + + logger.info('\nsummary:') + [logger.handle(log) for log in summary_logs if log.levelno >= logger.getEffectiveLevel()] + + if any(log.levelno == logging.CRITICAL for log in summary_logs): + logger.critical('\nNeed some help? https://torsion.org/borgmatic/#issues') sys.exit(1) diff --git a/setup.py b/setup.py index 95ef9bea..9a38bb4c 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages -VERSION = '1.2.13' +VERSION = '1.2.14.dev0' setup( diff --git a/tests/unit/commands/test_borgmatic.py b/tests/unit/commands/test_borgmatic.py index 943f9aae..a7b0810c 100644 --- a/tests/unit/commands/test_borgmatic.py +++ b/tests/unit/commands/test_borgmatic.py @@ -3,12 +3,12 @@ import sys from flexmock import flexmock -from borgmatic.commands import borgmatic +from borgmatic.commands import borgmatic as module def test_run_commands_handles_multiple_json_outputs_in_array(): ( - flexmock(borgmatic) + flexmock(module) .should_receive('_run_commands_on_repository') .times(3) .replace_with( @@ -36,7 +36,7 @@ def test_run_commands_handles_multiple_json_outputs_in_array(): ) ) - borgmatic._run_commands( + module._run_commands( args=flexmock(json=True), consistency=None, local_path=None, @@ -45,3 +45,29 @@ def test_run_commands_handles_multiple_json_outputs_in_array(): retention=None, storage=None, ) + + +def test_collect_configuration_run_summary_logs_info_for_success(): + flexmock(module).should_receive('run_configuration') + + logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=())) + + assert any(log for log in logs if log.levelno == module.logging.INFO) + + +def test_collect_configuration_run_summary_logs_critical_for_error(): + flexmock(module).should_receive('run_configuration').and_raise(ValueError) + + logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=())) + + assert any(log for log in logs if log.levelno == module.logging.CRITICAL) + + +def test_collect_configuration_run_summary_logs_critical_for_missing_configs(): + logs = tuple( + module.collect_configuration_run_summary_logs( + config_filenames=(), args=flexmock(config_paths=()) + ) + ) + + assert any(log for log in logs if log.levelno == module.logging.CRITICAL)