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

This commit is contained in:
Dan Helfman 2019-10-13 15:58:11 -07:00
parent f3910f49ca
commit 7b3b28616d
7 changed files with 87 additions and 16 deletions

3
NEWS
View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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'])

View File

@ -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 == '[]'