Selaa lähdekoodia

Add "borgmatic list --successful" flag to only list successful (non-checkpoint) archives (#86).

Dan Helfman 5 vuotta sitten
vanhempi
sitoutus
7b3b28616d

+ 2 - 1
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
  * Add a suggestion form to all documentation pages, so users can submit ideas for improving the
    documentation.
    documentation.
  * Update documentation link to community Arch Linux borgmatic package.
  * Update documentation link to community Arch Linux borgmatic package.

+ 9 - 1
borgmatic/borg/list.py

@@ -6,6 +6,10 @@ from borgmatic.execute import execute_command
 logger = logging.getLogger(__name__)
 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):
 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
     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.
     if an archive name is given, listing the files in that archive.
     '''
     '''
     lock_wait = storage_config.get('lock_wait', None)
     lock_wait = storage_config.get('lock_wait', None)
+    if list_arguments.successful:
+        list_arguments.glob_archives = BORG_EXCLUDE_CHECKPOINTS_GLOB
 
 
     full_command = (
     full_command = (
         (local_path, 'list')
         (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('remote-path', remote_path)
         + make_flags('lock-wait', lock_wait)
         + 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))
             '::'.join((repository, list_arguments.archive))
             if list_arguments.archive
             if list_arguments.archive

+ 9 - 0
borgmatic/commands/arguments.py

@@ -316,6 +316,12 @@ def parse_arguments(*unparsed_arguments):
     list_group.add_argument(
     list_group.add_argument(
         '-a', '--glob-archives', metavar='GLOB', help='Only list archive names matching this glob'
         '-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(
     list_group.add_argument(
         '--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
         '--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:
     if 'init' in arguments and arguments['global'].dry_run:
         raise ValueError('The init action cannot be used with the --dry-run option')
         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 (
     if (
         'list' in arguments
         'list' in arguments
         and 'info' in arguments
         and 'info' in arguments

+ 18 - 1
docs/how-to/inspect-your-backups.md

@@ -32,7 +32,7 @@ borgmatic --stats
 
 
 ## Existing backups
 ## 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
 [list](https://borgbackup.readthedocs.io/en/stable/usage/list.html) and
 [info](https://borgbackup.readthedocs.io/en/stable/usage/info.html)
 [info](https://borgbackup.readthedocs.io/en/stable/usage/info.html)
 functionality:
 functionality:
@@ -46,6 +46,7 @@ borgmatic info
 (No borgmatic `list` or `info` actions? Try the old-style `--list` or
 (No borgmatic `list` or `info` actions? Try the old-style `--list` or
 `--info`. Or upgrade borgmatic!)
 `--info`. Or upgrade borgmatic!)
 
 
+
 ## Logging
 ## Logging
 
 
 By default, borgmatic logs to a local syslog-compatible daemon if one is
 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
 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.
 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
 ## Related documentation
 
 

+ 1 - 1
setup.py

@@ -1,6 +1,6 @@
 from setuptools import find_packages, setup
 from setuptools import find_packages, setup
 
 
-VERSION = '1.3.24.dev0'
+VERSION = '1.3.24'
 
 
 
 
 setup(
 setup(

+ 9 - 0
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():
 def test_parse_arguments_disallows_repository_without_extract_or_list():
     flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
     flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
 
 

+ 39 - 12
tests/unit/borg/test_list.py

@@ -14,7 +14,9 @@ def test_list_archives_calls_borg_with_parameters():
     )
     )
 
 
     module.list_archives(
     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)
     insert_logging_mock(logging.INFO)
 
 
     module.list_archives(
     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)
     insert_logging_mock(logging.INFO)
 
 
     module.list_archives(
     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)
     insert_logging_mock(logging.DEBUG)
 
 
     module.list_archives(
     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)
     insert_logging_mock(logging.DEBUG)
 
 
     module.list_archives(
     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(
     module.list_archives(
         repository='repo',
         repository='repo',
         storage_config=storage_config,
         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(
     module.list_archives(
         repository='repo',
         repository='repo',
         storage_config=storage_config,
         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(
     module.list_archives(
         repository='repo',
         repository='repo',
         storage_config={},
         storage_config={},
-        list_arguments=flexmock(archive=None, json=False),
+        list_arguments=flexmock(archive=None, json=False, successful=False),
         local_path='borg1',
         local_path='borg1',
     )
     )
 
 
@@ -109,7 +119,7 @@ def test_list_archives_with_remote_path_calls_borg_with_remote_path_parameters()
     module.list_archives(
     module.list_archives(
         repository='repo',
         repository='repo',
         storage_config={},
         storage_config={},
-        list_arguments=flexmock(archive=None, json=False),
+        list_arguments=flexmock(archive=None, json=False, successful=False),
         remote_path='borg1',
         remote_path='borg1',
     )
     )
 
 
@@ -122,7 +132,7 @@ def test_list_archives_with_short_calls_borg_with_short_parameter():
     module.list_archives(
     module.list_archives(
         repository='repo',
         repository='repo',
         storage_config={},
         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(
     module.list_archives(
         repository='repo',
         repository='repo',
         storage_config={},
         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('[]')
     ).and_return('[]')
 
 
     json_output = module.list_archives(
     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 == '[]'
     assert json_output == '[]'