Specify "--archive latest" to all actions that accept an archive (#289).
This commit is contained in:
parent
bc02c123e6
commit
55141bda67
6 changed files with 177 additions and 13 deletions
5
NEWS
5
NEWS
|
@ -1,3 +1,8 @@
|
||||||
|
1.5.1.dev0
|
||||||
|
* #289: Tired of looking up the latest successful archive name in order to pass it to borgmatic
|
||||||
|
actions? Me too. Now you can specify "--archive latest" to all actions that accept an archive
|
||||||
|
flag.
|
||||||
|
|
||||||
1.5.0
|
1.5.0
|
||||||
* #245: Monitor backups with PagerDuty hook integration. See the documentation for more
|
* #245: Monitor backups with PagerDuty hook integration. See the documentation for more
|
||||||
information: https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pagerduty-hook
|
information: https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pagerduty-hook
|
||||||
|
|
|
@ -11,6 +11,42 @@ logger = logging.getLogger(__name__)
|
||||||
BORG_EXCLUDE_CHECKPOINTS_GLOB = '*[0123456789]'
|
BORG_EXCLUDE_CHECKPOINTS_GLOB = '*[0123456789]'
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_archive_name(repository, archive, storage_config, local_path='borg', remote_path=None):
|
||||||
|
'''
|
||||||
|
Given a local or remote repository path, an archive name, a storage config dict, a local Borg
|
||||||
|
path, and a remote Borg path, simply return the archive name. But if the archive name is
|
||||||
|
"latest", then instead introspect the repository for the latest successful (non-checkpoint)
|
||||||
|
archive, and return its name.
|
||||||
|
|
||||||
|
Raise ValueError if "latest" is given but there are no archives in the repository.
|
||||||
|
'''
|
||||||
|
if archive != "latest":
|
||||||
|
return archive
|
||||||
|
|
||||||
|
lock_wait = storage_config.get('lock_wait', None)
|
||||||
|
|
||||||
|
full_command = (
|
||||||
|
(local_path, 'list')
|
||||||
|
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||||
|
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||||
|
+ make_flags('remote-path', remote_path)
|
||||||
|
+ make_flags('lock-wait', lock_wait)
|
||||||
|
+ make_flags('glob-archives', BORG_EXCLUDE_CHECKPOINTS_GLOB)
|
||||||
|
+ make_flags('last', 1)
|
||||||
|
+ ('--short', repository)
|
||||||
|
)
|
||||||
|
|
||||||
|
output = execute_command(full_command, output_log_level=None, error_on_warnings=False)
|
||||||
|
try:
|
||||||
|
latest_archive = output.strip().splitlines()[-1]
|
||||||
|
except IndexError:
|
||||||
|
raise ValueError('No archives found in the repository')
|
||||||
|
|
||||||
|
logger.debug('{}: Latest archive is {}'.format(repository, latest_archive))
|
||||||
|
|
||||||
|
return latest_archive
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
|
|
@ -323,7 +323,9 @@ def parse_arguments(*unparsed_arguments):
|
||||||
'--repository',
|
'--repository',
|
||||||
help='Path of repository to extract, defaults to the configured repository if there is only one',
|
help='Path of repository to extract, defaults to the configured repository if there is only one',
|
||||||
)
|
)
|
||||||
extract_group.add_argument('--archive', help='Name of archive to extract', required=True)
|
extract_group.add_argument(
|
||||||
|
'--archive', help='Name of archive to extract (or "latest")', required=True
|
||||||
|
)
|
||||||
extract_group.add_argument(
|
extract_group.add_argument(
|
||||||
'--path',
|
'--path',
|
||||||
'--restore-path',
|
'--restore-path',
|
||||||
|
@ -361,7 +363,7 @@ def parse_arguments(*unparsed_arguments):
|
||||||
'--repository',
|
'--repository',
|
||||||
help='Path of repository to use, defaults to the configured repository if there is only one',
|
help='Path of repository to use, defaults to the configured repository if there is only one',
|
||||||
)
|
)
|
||||||
mount_group.add_argument('--archive', help='Name of archive to mount')
|
mount_group.add_argument('--archive', help='Name of archive to mount (or "latest")')
|
||||||
mount_group.add_argument(
|
mount_group.add_argument(
|
||||||
'--mount-point',
|
'--mount-point',
|
||||||
metavar='PATH',
|
metavar='PATH',
|
||||||
|
@ -415,7 +417,9 @@ def parse_arguments(*unparsed_arguments):
|
||||||
'--repository',
|
'--repository',
|
||||||
help='Path of repository to restore from, defaults to the configured repository if there is only one',
|
help='Path of repository to restore from, defaults to the configured repository if there is only one',
|
||||||
)
|
)
|
||||||
restore_group.add_argument('--archive', help='Name of archive to restore from', required=True)
|
restore_group.add_argument(
|
||||||
|
'--archive', help='Name of archive to restore from (or "latest")', required=True
|
||||||
|
)
|
||||||
restore_group.add_argument(
|
restore_group.add_argument(
|
||||||
'--database',
|
'--database',
|
||||||
metavar='NAME',
|
metavar='NAME',
|
||||||
|
@ -446,7 +450,7 @@ def parse_arguments(*unparsed_arguments):
|
||||||
'--repository',
|
'--repository',
|
||||||
help='Path of repository to list, defaults to the configured repository if there is only one',
|
help='Path of repository to list, defaults to the configured repository if there is only one',
|
||||||
)
|
)
|
||||||
list_group.add_argument('--archive', help='Name of archive to list')
|
list_group.add_argument('--archive', help='Name of archive to list (or "latest")')
|
||||||
list_group.add_argument(
|
list_group.add_argument(
|
||||||
'--path',
|
'--path',
|
||||||
metavar='PATH',
|
metavar='PATH',
|
||||||
|
@ -508,7 +512,7 @@ def parse_arguments(*unparsed_arguments):
|
||||||
'--repository',
|
'--repository',
|
||||||
help='Path of repository to show info for, defaults to the configured repository if there is only one',
|
help='Path of repository to show info for, defaults to the configured repository if there is only one',
|
||||||
)
|
)
|
||||||
info_group.add_argument('--archive', help='Name of archive to show info for')
|
info_group.add_argument('--archive', help='Name of archive to show info for (or "latest")')
|
||||||
info_group.add_argument(
|
info_group.add_argument(
|
||||||
'--json', dest='json', default=False, action='store_true', help='Output results as JSON'
|
'--json', dest='json', default=False, action='store_true', help='Output results as JSON'
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import collections
|
import collections
|
||||||
|
import copy
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
@ -297,7 +298,9 @@ def run_actions(
|
||||||
borg_extract.extract_archive(
|
borg_extract.extract_archive(
|
||||||
global_arguments.dry_run,
|
global_arguments.dry_run,
|
||||||
repository,
|
repository,
|
||||||
arguments['extract'].archive,
|
borg_list.resolve_archive_name(
|
||||||
|
repository, arguments['extract'].archive, storage, local_path, remote_path
|
||||||
|
),
|
||||||
arguments['extract'].paths,
|
arguments['extract'].paths,
|
||||||
location,
|
location,
|
||||||
storage,
|
storage,
|
||||||
|
@ -319,7 +322,9 @@ def run_actions(
|
||||||
|
|
||||||
borg_mount.mount_archive(
|
borg_mount.mount_archive(
|
||||||
repository,
|
repository,
|
||||||
arguments['mount'].archive,
|
borg_list.resolve_archive_name(
|
||||||
|
repository, arguments['mount'].archive, storage, local_path, remote_path
|
||||||
|
),
|
||||||
arguments['mount'].mount_point,
|
arguments['mount'].mount_point,
|
||||||
arguments['mount'].paths,
|
arguments['mount'].paths,
|
||||||
arguments['mount'].foreground,
|
arguments['mount'].foreground,
|
||||||
|
@ -355,7 +360,9 @@ def run_actions(
|
||||||
borg_extract.extract_archive(
|
borg_extract.extract_archive(
|
||||||
global_arguments.dry_run,
|
global_arguments.dry_run,
|
||||||
repository,
|
repository,
|
||||||
arguments['restore'].archive,
|
borg_list.resolve_archive_name(
|
||||||
|
repository, arguments['restore'].archive, storage, local_path, remote_path
|
||||||
|
),
|
||||||
dump.convert_glob_patterns_to_borg_patterns(
|
dump.convert_glob_patterns_to_borg_patterns(
|
||||||
dump.flatten_dump_patterns(dump_patterns, restore_names)
|
dump.flatten_dump_patterns(dump_patterns, restore_names)
|
||||||
),
|
),
|
||||||
|
@ -395,12 +402,16 @@ def run_actions(
|
||||||
if arguments['list'].repository is None or validate.repositories_match(
|
if arguments['list'].repository is None or validate.repositories_match(
|
||||||
repository, arguments['list'].repository
|
repository, arguments['list'].repository
|
||||||
):
|
):
|
||||||
if not arguments['list'].json:
|
list_arguments = copy.copy(arguments['list'])
|
||||||
|
if not list_arguments.json:
|
||||||
logger.warning('{}: Listing archives'.format(repository))
|
logger.warning('{}: Listing archives'.format(repository))
|
||||||
|
list_arguments.archive = borg_list.resolve_archive_name(
|
||||||
|
repository, list_arguments.archive, storage, local_path, remote_path
|
||||||
|
)
|
||||||
json_output = borg_list.list_archives(
|
json_output = borg_list.list_archives(
|
||||||
repository,
|
repository,
|
||||||
storage,
|
storage,
|
||||||
list_arguments=arguments['list'],
|
list_arguments=list_arguments,
|
||||||
local_path=local_path,
|
local_path=local_path,
|
||||||
remote_path=remote_path,
|
remote_path=remote_path,
|
||||||
)
|
)
|
||||||
|
@ -410,12 +421,16 @@ def run_actions(
|
||||||
if arguments['info'].repository is None or validate.repositories_match(
|
if arguments['info'].repository is None or validate.repositories_match(
|
||||||
repository, arguments['info'].repository
|
repository, arguments['info'].repository
|
||||||
):
|
):
|
||||||
if not arguments['info'].json:
|
info_arguments = copy.copy(arguments['info'])
|
||||||
|
if not info_arguments.json:
|
||||||
logger.warning('{}: Displaying summary info for archives'.format(repository))
|
logger.warning('{}: Displaying summary info for archives'.format(repository))
|
||||||
|
info_arguments.archive = borg_list.resolve_archive_name(
|
||||||
|
repository, info_arguments.archive, storage, local_path, remote_path
|
||||||
|
)
|
||||||
json_output = borg_info.display_archives_info(
|
json_output = borg_info.display_archives_info(
|
||||||
repository,
|
repository,
|
||||||
storage,
|
storage,
|
||||||
info_arguments=arguments['info'],
|
info_arguments=info_arguments,
|
||||||
local_path=local_path,
|
local_path=local_path,
|
||||||
remote_path=remote_path,
|
remote_path=remote_path,
|
||||||
)
|
)
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -1,6 +1,6 @@
|
||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
VERSION = '1.5.0'
|
VERSION = '1.5.1.dev0'
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
|
|
|
@ -7,6 +7,110 @@ from borgmatic.borg import list as module
|
||||||
|
|
||||||
from ..test_verbosity import insert_logging_mock
|
from ..test_verbosity import insert_logging_mock
|
||||||
|
|
||||||
|
BORG_LIST_LATEST_ARGUMENTS = (
|
||||||
|
'--glob-archives',
|
||||||
|
module.BORG_EXCLUDE_CHECKPOINTS_GLOB,
|
||||||
|
'--last',
|
||||||
|
'1',
|
||||||
|
'--short',
|
||||||
|
'repo',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_archive_name_passes_through_non_latest_archive_name():
|
||||||
|
archive = 'myhost-2030-01-01T14:41:17.647620'
|
||||||
|
|
||||||
|
assert module.resolve_archive_name('repo', archive, storage_config={}) == archive
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_archive_name_calls_borg_with_parameters():
|
||||||
|
expected_archive = 'archive-name'
|
||||||
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
|
('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
|
||||||
|
output_log_level=None,
|
||||||
|
error_on_warnings=False,
|
||||||
|
).and_return(expected_archive + '\n')
|
||||||
|
|
||||||
|
assert module.resolve_archive_name('repo', 'latest', storage_config={}) == expected_archive
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_archive_name_with_log_info_calls_borg_with_info_parameter():
|
||||||
|
expected_archive = 'archive-name'
|
||||||
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
|
('borg', 'list', '--info') + BORG_LIST_LATEST_ARGUMENTS,
|
||||||
|
output_log_level=None,
|
||||||
|
error_on_warnings=False,
|
||||||
|
).and_return(expected_archive + '\n')
|
||||||
|
insert_logging_mock(logging.INFO)
|
||||||
|
|
||||||
|
assert module.resolve_archive_name('repo', 'latest', storage_config={}) == expected_archive
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_archive_name_with_log_debug_calls_borg_with_debug_parameter():
|
||||||
|
expected_archive = 'archive-name'
|
||||||
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
|
('borg', 'list', '--debug', '--show-rc') + BORG_LIST_LATEST_ARGUMENTS,
|
||||||
|
output_log_level=None,
|
||||||
|
error_on_warnings=False,
|
||||||
|
).and_return(expected_archive + '\n')
|
||||||
|
insert_logging_mock(logging.DEBUG)
|
||||||
|
|
||||||
|
assert module.resolve_archive_name('repo', 'latest', storage_config={}) == expected_archive
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_archive_name_with_local_path_calls_borg_via_local_path():
|
||||||
|
expected_archive = 'archive-name'
|
||||||
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
|
('borg1', 'list') + BORG_LIST_LATEST_ARGUMENTS,
|
||||||
|
output_log_level=None,
|
||||||
|
error_on_warnings=False,
|
||||||
|
).and_return(expected_archive + '\n')
|
||||||
|
|
||||||
|
assert (
|
||||||
|
module.resolve_archive_name('repo', 'latest', storage_config={}, local_path='borg1')
|
||||||
|
== expected_archive
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_archive_name_with_remote_path_calls_borg_with_remote_path_parameters():
|
||||||
|
expected_archive = 'archive-name'
|
||||||
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
|
('borg', 'list', '--remote-path', 'borg1') + BORG_LIST_LATEST_ARGUMENTS,
|
||||||
|
output_log_level=None,
|
||||||
|
error_on_warnings=False,
|
||||||
|
).and_return(expected_archive + '\n')
|
||||||
|
|
||||||
|
assert (
|
||||||
|
module.resolve_archive_name('repo', 'latest', storage_config={}, remote_path='borg1')
|
||||||
|
== expected_archive
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_archive_name_without_archives_raises():
|
||||||
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
|
('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
|
||||||
|
output_log_level=None,
|
||||||
|
error_on_warnings=False,
|
||||||
|
).and_return('')
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
module.resolve_archive_name('repo', 'latest', storage_config={})
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_archive_name_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
||||||
|
expected_archive = 'archive-name'
|
||||||
|
|
||||||
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
|
('borg', 'list', '--lock-wait', 'okay') + BORG_LIST_LATEST_ARGUMENTS,
|
||||||
|
output_log_level=None,
|
||||||
|
error_on_warnings=False,
|
||||||
|
).and_return(expected_archive + '\n')
|
||||||
|
|
||||||
|
assert (
|
||||||
|
module.resolve_archive_name('repo', 'latest', storage_config={'lock_wait': 'okay'})
|
||||||
|
== expected_archive
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_list_archives_calls_borg_with_parameters():
|
def test_list_archives_calls_borg_with_parameters():
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
|
|
Loading…
Reference in a new issue