From 7b3b28616d4c0229806539a0b6ac929113400eae Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Sun, 13 Oct 2019 15:58:11 -0700 Subject: [PATCH] Add "borgmatic list --successful" flag to only list successful (non-checkpoint) archives (#86). --- NEWS | 3 +- borgmatic/borg/list.py | 10 +++- borgmatic/commands/arguments.py | 9 ++++ docs/how-to/inspect-your-backups.md | 19 +++++++- setup.py | 2 +- tests/integration/commands/test_arguments.py | 9 ++++ tests/unit/borg/test_list.py | 51 +++++++++++++++----- 7 files changed, 87 insertions(+), 16 deletions(-) diff --git a/NEWS b/NEWS index a1648873d..b41425073 100644 --- a/NEWS +++ b/NEWS @@ -1,4 +1,5 @@ -1.3.24.dev0 +1.3.24 + * #86: Add "borgmatic list --successful" flag to only list successful (non-checkpoint) archives. * Add a suggestion form to all documentation pages, so users can submit ideas for improving the documentation. * Update documentation link to community Arch Linux borgmatic package. diff --git a/borgmatic/borg/list.py b/borgmatic/borg/list.py index 2b9e84d2b..aa831e522 100644 --- a/borgmatic/borg/list.py +++ b/borgmatic/borg/list.py @@ -6,6 +6,10 @@ from borgmatic.execute import execute_command logger = logging.getLogger(__name__) +# A hack to convince Borg to exclude archives ending in ".checkpoint". +BORG_EXCLUDE_CHECKPOINTS_GLOB = '*[!.][!c][!h][!e][!c][!k][!p][!o][!i][!n][!t]' + + def list_archives(repository, storage_config, list_arguments, local_path='borg', remote_path=None): ''' Given a local or remote repository path, a storage config dict, and the arguments to the list @@ -13,6 +17,8 @@ def list_archives(repository, storage_config, list_arguments, local_path='borg', if an archive name is given, listing the files in that archive. ''' lock_wait = storage_config.get('lock_wait', None) + if list_arguments.successful: + list_arguments.glob_archives = BORG_EXCLUDE_CHECKPOINTS_GLOB full_command = ( (local_path, 'list') @@ -28,7 +34,9 @@ def list_archives(repository, storage_config, list_arguments, local_path='borg', ) + make_flags('remote-path', remote_path) + make_flags('lock-wait', lock_wait) - + make_flags_from_arguments(list_arguments, excludes=('repository', 'archive')) + + make_flags_from_arguments( + list_arguments, excludes=('repository', 'archive', 'successful') + ) + ( '::'.join((repository, list_arguments.archive)) if list_arguments.archive diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index 6f0f1a7b8..aa5bd392e 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -316,6 +316,12 @@ def parse_arguments(*unparsed_arguments): list_group.add_argument( '-a', '--glob-archives', metavar='GLOB', help='Only list archive names matching this glob' ) + list_group.add_argument( + '--successful', + default=False, + action='store_true', + help='Only list archive names of successful (non-checkpoint) backups', + ) list_group.add_argument( '--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys' ) @@ -388,6 +394,9 @@ def parse_arguments(*unparsed_arguments): if 'init' in arguments and arguments['global'].dry_run: raise ValueError('The init action cannot be used with the --dry-run option') + if 'list' in arguments and arguments['list'].glob_archives and arguments['list'].successful: + raise ValueError('The --glob-archives and --successful options cannot be used together') + if ( 'list' in arguments and 'info' in arguments diff --git a/docs/how-to/inspect-your-backups.md b/docs/how-to/inspect-your-backups.md index d06ae124f..73f522bd5 100644 --- a/docs/how-to/inspect-your-backups.md +++ b/docs/how-to/inspect-your-backups.md @@ -32,7 +32,7 @@ borgmatic --stats ## Existing backups -Borgmatic provides convenient actions for Borg's +borgmatic provides convenient actions for Borg's [list](https://borgbackup.readthedocs.io/en/stable/usage/list.html) and [info](https://borgbackup.readthedocs.io/en/stable/usage/info.html) functionality: @@ -46,6 +46,7 @@ borgmatic info (No borgmatic `list` or `info` actions? Try the old-style `--list` or `--info`. Or upgrade borgmatic!) + ## Logging By default, borgmatic logs to a local syslog-compatible daemon if one is @@ -135,6 +136,22 @@ Note that when you specify the `--json` flag, Borg's other non-JSON output is suppressed so as not to interfere with the captured JSON. Also note that JSON output only shows up at the console, and not in syslog. +### Successful backups + +`borgmatic list` includes support for a `--successful` flag that only lists +successful (non-checkpoint) backups. Combined with a built-in Borg flag like +`--last`, you can list the last successful backup for use in your monitoring +scripts. Here's an example combined with `--json`: + +```bash +borgmatic list --successful --last 1 --json +``` + +Note that this particular combination will only work if you've got a single +backup "series" in your repository. If you're instead backing up, say, from +multiple different hosts into a single repository, then you'll need to get +fancier with your archive listing. See `borg list --help` for more flags. + ## Related documentation diff --git a/setup.py b/setup.py index c93ba3ea6..921def49f 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import find_packages, setup -VERSION = '1.3.24.dev0' +VERSION = '1.3.24' setup( diff --git a/tests/integration/commands/test_arguments.py b/tests/integration/commands/test_arguments.py index c13756c30..ff4f645c3 100644 --- a/tests/integration/commands/test_arguments.py +++ b/tests/integration/commands/test_arguments.py @@ -230,6 +230,15 @@ def test_parse_arguments_disallows_init_and_dry_run(): ) +def test_parse_arguments_disallows_glob_archives_with_successful(): + flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) + + with pytest.raises(ValueError): + module.parse_arguments( + '--config', 'myconfig', 'list', '--glob-archives', '*glob*', '--successful' + ) + + def test_parse_arguments_disallows_repository_without_extract_or_list(): flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default']) diff --git a/tests/unit/borg/test_list.py b/tests/unit/borg/test_list.py index 50aa2a5b9..c0a746164 100644 --- a/tests/unit/borg/test_list.py +++ b/tests/unit/borg/test_list.py @@ -14,7 +14,9 @@ def test_list_archives_calls_borg_with_parameters(): ) module.list_archives( - repository='repo', storage_config={}, list_arguments=flexmock(archive=None, json=False) + repository='repo', + storage_config={}, + list_arguments=flexmock(archive=None, json=False, successful=False), ) @@ -25,7 +27,9 @@ def test_list_archives_with_log_info_calls_borg_with_info_parameter(): insert_logging_mock(logging.INFO) module.list_archives( - repository='repo', storage_config={}, list_arguments=flexmock(archive=None, json=False) + repository='repo', + storage_config={}, + list_arguments=flexmock(archive=None, json=False, successful=False), ) @@ -36,7 +40,9 @@ def test_list_archives_with_log_info_and_json_suppresses_most_borg_output(): insert_logging_mock(logging.INFO) module.list_archives( - repository='repo', storage_config={}, list_arguments=flexmock(archive=None, json=True) + repository='repo', + storage_config={}, + list_arguments=flexmock(archive=None, json=True, successful=False), ) @@ -47,7 +53,9 @@ def test_list_archives_with_log_debug_calls_borg_with_debug_parameter(): insert_logging_mock(logging.DEBUG) module.list_archives( - repository='repo', storage_config={}, list_arguments=flexmock(archive=None, json=False) + repository='repo', + storage_config={}, + list_arguments=flexmock(archive=None, json=False, successful=False), ) @@ -58,7 +66,9 @@ def test_list_archives_with_log_debug_and_json_suppresses_most_borg_output(): insert_logging_mock(logging.DEBUG) module.list_archives( - repository='repo', storage_config={}, list_arguments=flexmock(archive=None, json=True) + repository='repo', + storage_config={}, + list_arguments=flexmock(archive=None, json=True, successful=False), ) @@ -71,7 +81,7 @@ def test_list_archives_with_lock_wait_calls_borg_with_lock_wait_parameters(): module.list_archives( repository='repo', storage_config=storage_config, - list_arguments=flexmock(archive=None, json=False), + list_arguments=flexmock(archive=None, json=False, successful=False), ) @@ -84,7 +94,7 @@ def test_list_archives_with_archive_calls_borg_with_archive_parameter(): module.list_archives( repository='repo', storage_config=storage_config, - list_arguments=flexmock(archive='archive', json=False), + list_arguments=flexmock(archive='archive', json=False, successful=False), ) @@ -96,7 +106,7 @@ def test_list_archives_with_local_path_calls_borg_via_local_path(): module.list_archives( repository='repo', storage_config={}, - list_arguments=flexmock(archive=None, json=False), + list_arguments=flexmock(archive=None, json=False, successful=False), local_path='borg1', ) @@ -109,7 +119,7 @@ def test_list_archives_with_remote_path_calls_borg_with_remote_path_parameters() module.list_archives( repository='repo', storage_config={}, - list_arguments=flexmock(archive=None, json=False), + list_arguments=flexmock(archive=None, json=False, successful=False), remote_path='borg1', ) @@ -122,7 +132,7 @@ def test_list_archives_with_short_calls_borg_with_short_parameter(): module.list_archives( repository='repo', storage_config={}, - list_arguments=flexmock(archive=None, json=False, short=True), + list_arguments=flexmock(archive=None, json=False, successful=False, short=True), ) @@ -149,7 +159,22 @@ def test_list_archives_passes_through_arguments_to_borg(argument_name): module.list_archives( repository='repo', storage_config={}, - list_arguments=flexmock(archive=None, json=False, **{argument_name: 'value'}), + list_arguments=flexmock( + archive=None, json=False, successful=False, **{argument_name: 'value'} + ), + ) + + +def test_list_archives_with_successful_calls_borg_to_exclude_checkpoints(): + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'list', '--glob-archives', module.BORG_EXCLUDE_CHECKPOINTS_GLOB, 'repo'), + output_log_level=logging.WARNING, + ).and_return('[]') + + module.list_archives( + repository='repo', + storage_config={}, + list_arguments=flexmock(archive=None, json=False, successful=True), ) @@ -159,7 +184,9 @@ def test_list_archives_with_json_calls_borg_with_json_parameter(): ).and_return('[]') json_output = module.list_archives( - repository='repo', storage_config={}, list_arguments=flexmock(archive=None, json=True) + repository='repo', + storage_config={}, + list_arguments=flexmock(archive=None, json=True, successful=False), ) assert json_output == '[]'