Add "borgmatic list --successful" flag to only list successful (non-checkpoint) archives (#86).
This commit is contained in:
parent
f3910f49ca
commit
7b3b28616d
7 changed files with 87 additions and 16 deletions
3
NEWS
3
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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
2
setup.py
2
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(
|
||||||
|
|
|
@ -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'])
|
||||||
|
|
||||||
|
|
|
@ -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 == '[]'
|
||||||
|
|
Loading…
Reference in a new issue