From 765e343c7146744fe214733dcdd5a403f41b4a1a Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Sat, 26 May 2018 16:19:05 -0700 Subject: [PATCH] Support for Borg --info via borgmatic command-line (#61). --- NEWS | 7 ++- borgmatic/borg/info.py | 29 ++++++++++ borgmatic/borg/list.py | 28 ++++++++++ borgmatic/commands/borgmatic.py | 26 +++++++-- borgmatic/tests/unit/borg/test_info.py | 77 ++++++++++++++++++++++++++ 5 files changed, 159 insertions(+), 8 deletions(-) create mode 100644 borgmatic/borg/info.py create mode 100644 borgmatic/borg/list.py create mode 100644 borgmatic/tests/unit/borg/test_info.py diff --git a/NEWS b/NEWS index 9a596eed5..78e25fab7 100644 --- a/NEWS +++ b/NEWS @@ -1,14 +1,15 @@ 1.1.16.dev0 + * #61: Support for Borg --list option via borgmatic command-line to list all archives. + * #61: Support for Borg --info option via borgmatic command-line to display summary information. + * #62: Update README to mention other ways of installing borgmatic. * Support for Borg --prefix option for consistency checks via "prefix" option in borgmatic's consistency configuration. * Add introductory screencast link to documentation. - * Update tox.ini to only assume Python 3.x instead of Python 3.4 specifically. - * #62: Update README to mention other ways of installing borgmatic. * #59: Ignore "check_last" and consistency "prefix" when "archives" not in consistency checks. * #60: Add "Persistent" flag to systemd timer example. * #63: Support for Borg --nobsdflags option to skip recording bsdflags (e.g. NODUMP, IMMUTABLE) in archive. - * #61: Support for Borg --list option via borgmatic command-line to list all archives. + * Update tox.ini to only assume Python 3.x instead of Python 3.4 specifically. 1.1.15 * Support for Borg BORG_PASSCOMMAND environment variable to read a password from an external file. diff --git a/borgmatic/borg/info.py b/borgmatic/borg/info.py new file mode 100644 index 000000000..c55e3651c --- /dev/null +++ b/borgmatic/borg/info.py @@ -0,0 +1,29 @@ +import logging +import subprocess + +from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS + + +logger = logging.getLogger(__name__) + + +def display_archives_info(verbosity, repository, storage_config, local_path='borg', + remote_path=None): + ''' + Given a verbosity flag, a local or remote repository path, and a storage config dict, + display summary information for Borg archives in the repository. + ''' + lock_wait = storage_config.get('lock_wait', None) + + full_command = ( + (local_path, 'info', repository) + + (('--remote-path', remote_path) if remote_path else ()) + + (('--lock-wait', str(lock_wait)) if lock_wait else ()) + + { + VERBOSITY_SOME: ('--info',), + VERBOSITY_LOTS: ('--debug',), + }.get(verbosity, ()) + ) + + logger.debug(' '.join(full_command)) + subprocess.check_call(full_command) diff --git a/borgmatic/borg/list.py b/borgmatic/borg/list.py new file mode 100644 index 000000000..989f88174 --- /dev/null +++ b/borgmatic/borg/list.py @@ -0,0 +1,28 @@ +import logging +import subprocess + +from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS + + +logger = logging.getLogger(__name__) + + +def list_archives(verbosity, repository, storage_config, local_path='borg', remote_path=None): + ''' + Given a verbosity flag, a local or remote repository path, and a storage config dict, + list Borg archives in the repository. + ''' + lock_wait = storage_config.get('lock_wait', None) + + full_command = ( + (local_path, 'list', repository) + + (('--remote-path', remote_path) if remote_path else ()) + + (('--lock-wait', str(lock_wait)) if lock_wait else ()) + + { + VERBOSITY_SOME: ('--info',), + VERBOSITY_LOTS: ('--debug',), + }.get(verbosity, ()) + ) + + logger.debug(' '.join(full_command)) + subprocess.check_call(full_command) diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index b9678b721..a5e4b7a63 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -6,7 +6,7 @@ from subprocess import CalledProcessError import sys from borgmatic.borg import check as borg_check, create as borg_create, prune as borg_prune, \ - list as borg_list + list as borg_list, info as borg_info from borgmatic.commands import hook from borgmatic.config import collect, convert, validate from borgmatic.signals import configure_signals @@ -68,6 +68,12 @@ def parse_arguments(*arguments): action='store_true', help='List archives', ) + parser.add_argument( + '-i', '--info', + dest='info', + action='store_true', + help='Display summary information on archives', + ) parser.add_argument( '-n', '--dry-run', dest='dry_run', @@ -84,11 +90,12 @@ def parse_arguments(*arguments): # If any of the action flags are explicitly requested, leave them as-is. Otherwise, assume # defaults: Mutate the given arguments to enable the default actions. - if not args.prune and not args.create and not args.check and not args.list: - args.prune = True - args.create = True - args.check = True + if args.prune or args.create or args.check or args.list or args.info: + return args + args.prune = True + args.create = True + args.check = True return args @@ -154,6 +161,15 @@ def run_configuration(config_filename, args): # pragma: no cover local_path=local_path, remote_path=remote_path, ) + if args.info: + logger.info('{}: Displaying summary info for archives'.format(repository)) + borg_info.display_archives_info( + args.verbosity, + repository, + storage, + local_path=local_path, + remote_path=remote_path, + ) hook.execute_hook(hooks.get('after_backup'), config_filename, 'post-backup') except (OSError, CalledProcessError): diff --git a/borgmatic/tests/unit/borg/test_info.py b/borgmatic/tests/unit/borg/test_info.py new file mode 100644 index 000000000..7a1cbbe72 --- /dev/null +++ b/borgmatic/tests/unit/borg/test_info.py @@ -0,0 +1,77 @@ +from collections import OrderedDict + +from flexmock import flexmock + +from borgmatic.borg import info as module +from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS + + +def insert_subprocess_mock(check_call_command, **kwargs): + subprocess = flexmock(module.subprocess) + subprocess.should_receive('check_call').with_args(check_call_command, **kwargs).once() + + +INFO_COMMAND = ('borg', 'info', 'repo') + + +def test_display_archives_info_calls_borg_with_parameters(): + insert_subprocess_mock(INFO_COMMAND) + + module.display_archives_info( + verbosity=None, + repository='repo', + storage_config={}, + ) + + +def test_display_archives_info_with_verbosity_some_calls_borg_with_info_parameter(): + insert_subprocess_mock(INFO_COMMAND + ('--info',)) + + module.display_archives_info( + repository='repo', + storage_config={}, + verbosity=VERBOSITY_SOME, + ) + + +def test_display_archives_info_with_verbosity_lots_calls_borg_with_debug_parameter(): + insert_subprocess_mock(INFO_COMMAND + ('--debug',)) + + module.display_archives_info( + repository='repo', + storage_config={}, + verbosity=VERBOSITY_LOTS, + ) + + +def test_display_archives_info_with_local_path_calls_borg_via_local_path(): + insert_subprocess_mock(('borg1',) + INFO_COMMAND[1:]) + + module.display_archives_info( + verbosity=None, + repository='repo', + storage_config={}, + local_path='borg1', + ) + + +def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_parameters(): + insert_subprocess_mock(INFO_COMMAND + ('--remote-path', 'borg1')) + + module.display_archives_info( + verbosity=None, + repository='repo', + storage_config={}, + remote_path='borg1', + ) + + +def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_parameters(): + storage_config = {'lock_wait': 5} + insert_subprocess_mock(INFO_COMMAND + ('--lock-wait', '5')) + + module.display_archives_info( + verbosity=None, + repository='repo', + storage_config=storage_config, + )