Compare commits

...
8 Commits
10 changed files with 100 additions and 30 deletions
+5
View File
@@ -1,3 +1,8 @@
1.2.13
* #100: Support for --stats command-line flag independent of --verbosity.
* #117: With borgmatic --init command-line flag, proceed without erroring if a repository already
exists.
1.2.12
* #110: Support for Borg repository initialization via borgmatic --init command-line flag.
* #111: Update Borg create --filter values so a dry run lists files to back up.
+4
View File
@@ -146,6 +146,10 @@ Also, optionally check out the [Borg Quick
Start](https://borgbackup.readthedocs.org/en/latest/quickstart.html) for more
background about repository initialization.
Note that borgmatic skips repository initialization if the repository already
exists. This supports use cases like ensuring a repository exists prior to
performing a backup.
If the repository is on a remote host, make sure that your local user has
key-based SSH access to the desired user account on the remote host.
+2 -1
View File
@@ -91,6 +91,7 @@ def create_archive(
local_path='borg',
remote_path=None,
progress=False,
stats=False,
json=False,
):
'''
@@ -135,7 +136,7 @@ def create_archive(
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
+ (('--list', '--filter', 'AME-') if logger.isEnabledFor(logging.INFO) else ())
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--stats',) if not dry_run and logger.isEnabledFor(logging.INFO) else ())
+ (('--stats',) if not dry_run and (logger.isEnabledFor(logging.INFO) or stats) else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ (('--dry-run',) if dry_run else ())
+ (('--progress',) if progress else ())
+12 -4
View File
@@ -15,9 +15,17 @@ def initialize_repository(
):
'''
Given a local or remote repository path, a Borg encryption mode, whether the repository should
be append-only, and the storage quota to use, initialize the repository.
be append-only, and the storage quota to use, initialize the repository. If the repository
already exists, then log and skip initialization.
'''
full_command = (
info_command = (local_path, 'info', repository)
logger.debug(' '.join(info_command))
if subprocess.call(info_command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0:
logger.info('Repository already exists. Skipping initialization.')
return
init_command = (
(local_path, 'init', repository)
+ (('--encryption', encryption_mode) if encryption_mode else ())
+ (('--append-only',) if append_only else ())
@@ -27,5 +35,5 @@ def initialize_repository(
+ (('--remote-path', remote_path) if remote_path else ())
)
logger.debug(' '.join(full_command))
subprocess.check_call(full_command)
logger.debug(' '.join(init_command))
subprocess.check_call(init_command)
+8 -1
View File
@@ -31,7 +31,13 @@ def _make_prune_flags(retention_config):
def prune_archives(
dry_run, repository, storage_config, retention_config, local_path='borg', remote_path=None
dry_run,
repository,
storage_config,
retention_config,
local_path='borg',
remote_path=None,
stats=False,
):
'''
Given dry-run flag, a local or remote repository path, a storage config dict, and a
@@ -51,6 +57,7 @@ def prune_archives(
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug', '--list', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ (('--dry-run',) if dry_run else ())
+ (('--stats',) if stats else ())
)
logger.debug(' '.join(full_command))
+14 -4
View File
@@ -107,6 +107,13 @@ def parse_arguments(*arguments):
action='store_true',
help='Display progress with --create option for each file as it is backed up',
)
parser.add_argument(
'--stats',
dest='stats',
default=False,
action='store_true',
help='Display statistics of archive with --create or --prune option',
)
parser.add_argument(
'--json',
dest='json',
@@ -142,16 +149,17 @@ def parse_arguments(*arguments):
'The --encryption, --append-only, and --storage-quota options can only be used with the --init option'
)
if args.init and (args.prune or args.create or args.dry_run):
raise ValueError(
'The --init option cannot be used with the --prune, --create, or --dry-run options'
)
if args.init and args.dry_run:
raise ValueError('The --init option cannot be used with the --dry-run option')
if args.init and not args.encryption_mode:
raise ValueError('The --encryption option is required with the --init option')
if args.progress and not args.create:
raise ValueError('The --progress option can only be used with the --create option')
if args.stats and not (args.create or args.prune):
raise ValueError('The --stats option can only be used with the --create or --prune options')
if args.json and not (args.create or args.list or args.info):
raise ValueError(
'The --json option can only be used with the --create, --list, or --info options'
@@ -261,6 +269,7 @@ def _run_commands_on_repository(
retention,
local_path=local_path,
remote_path=remote_path,
stats=args.stats,
)
if args.create:
logger.info('{}: Creating archive{}'.format(repository, dry_run_label))
@@ -272,6 +281,7 @@ def _run_commands_on_repository(
local_path=local_path,
remote_path=remote_path,
progress=args.progress,
stats=args.stats,
)
if args.check and checks.repository_enabled_for_checks(repository, consistency):
logger.info('{}: Running consistency checks'.format(repository))
+1
View File
@@ -41,6 +41,7 @@ for sub_command in prune create check list info; do
| grep -v '^--nobsdflags$' \
| grep -v '^--pattern$' \
| grep -v '^--progress$' \
| grep -v '^--stats$' \
| grep -v '^--read-special$' \
| grep -v '^--repository-only$' \
| grep -v '^--show-rc$' \
+1 -1
View File
@@ -1,7 +1,7 @@
from setuptools import setup, find_packages
VERSION = '1.2.12'
VERSION = '1.2.13'
setup(
+20 -7
View File
@@ -113,25 +113,25 @@ def test_parse_arguments_disallows_storage_quota_without_init():
module.parse_arguments('--config', 'myconfig', '--storage-quota', '5G')
def test_parse_arguments_disallows_init_and_prune():
def test_parse_arguments_allows_init_and_prune():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('--config', 'myconfig', '--init', '--prune')
module.parse_arguments('--config', 'myconfig', '--init', '--encryption', 'repokey', '--prune')
def test_parse_arguments_disallows_init_and_create():
def test_parse_arguments_allows_init_and_create():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('--config', 'myconfig', '--init', '--create')
module.parse_arguments('--config', 'myconfig', '--init', '--encryption', 'repokey', '--create')
def test_parse_arguments_disallows_init_and_dry_run():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('--config', 'myconfig', '--init', '--dry-run')
module.parse_arguments(
'--config', 'myconfig', '--init', '--encryption', 'repokey', '--dry-run'
)
def test_parse_arguments_allows_progress_and_create():
@@ -143,6 +143,19 @@ def test_parse_arguments_disallows_progress_without_create():
module.parse_arguments('--progress', '--list')
def test_parse_arguments_with_stats_and_create_flags_does_not_raise():
module.parse_arguments('--stats', '--create', '--list')
def test_parse_arguments_with_stats_and_prune_flags_does_not_raise():
module.parse_arguments('--stats', '--prune', '--list')
def test_parse_arguments_with_stats_flag_but_no_create_or_prune_flag_raises_value_error():
with pytest.raises(ValueError):
module.parse_arguments('--stats', '--list')
def test_parse_arguments_allows_json_with_list_or_info():
module.parse_arguments('--list', '--json')
module.parse_arguments('--info', '--json')
+33 -12
View File
@@ -6,53 +6,74 @@ from borgmatic.borg import init as module
from ..test_verbosity import insert_logging_mock
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_REPOSITORY_EXISTS_RESPONSE_CODE = 0
INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE = 2
INIT_COMMAND = ('borg', 'init', 'repo', '--encryption', 'repokey')
def insert_info_command_mock(info_response):
subprocess = flexmock(module.subprocess)
subprocess.should_receive('call').and_return(info_response)
def insert_init_command_mock(init_command, **kwargs):
subprocess = flexmock(module.subprocess)
subprocess.should_receive('check_call').with_args(init_command, **kwargs).once()
def test_initialize_repository_calls_borg_with_parameters():
insert_subprocess_mock(INIT_COMMAND)
insert_info_command_mock(INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE)
insert_init_command_mock(INIT_COMMAND)
module.initialize_repository(repository='repo', encryption_mode='repokey')
def test_initialize_repository_skips_initialization_when_repository_already_exists():
insert_info_command_mock(INFO_REPOSITORY_EXISTS_RESPONSE_CODE)
flexmock(module.subprocess).should_receive('check_call').never()
module.initialize_repository(repository='repo', encryption_mode='repokey')
def test_initialize_repository_with_append_only_calls_borg_with_append_only_parameter():
insert_subprocess_mock(INIT_COMMAND + ('--append-only',))
insert_info_command_mock(INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE)
insert_init_command_mock(INIT_COMMAND + ('--append-only',))
module.initialize_repository(repository='repo', encryption_mode='repokey', append_only=True)
def test_initialize_repository_with_storage_quota_calls_borg_with_storage_quota_parameter():
insert_subprocess_mock(INIT_COMMAND + ('--storage-quota', '5G'))
insert_info_command_mock(INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE)
insert_init_command_mock(INIT_COMMAND + ('--storage-quota', '5G'))
module.initialize_repository(repository='repo', encryption_mode='repokey', storage_quota='5G')
def test_initialize_repository_with_log_info_calls_borg_with_info_parameter():
insert_subprocess_mock(INIT_COMMAND + ('--info',))
insert_info_command_mock(INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE)
insert_init_command_mock(INIT_COMMAND + ('--info',))
insert_logging_mock(logging.INFO)
module.initialize_repository(repository='repo', encryption_mode='repokey')
def test_initialize_repository_with_log_debug_calls_borg_with_debug_parameter():
insert_subprocess_mock(INIT_COMMAND + ('--debug',))
insert_info_command_mock(INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE)
insert_init_command_mock(INIT_COMMAND + ('--debug',))
insert_logging_mock(logging.DEBUG)
module.initialize_repository(repository='repo', encryption_mode='repokey')
def test_initialize_repository_with_local_path_calls_borg_via_local_path():
insert_subprocess_mock(('borg1',) + INIT_COMMAND[1:])
insert_info_command_mock(INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE)
insert_init_command_mock(('borg1',) + INIT_COMMAND[1:])
module.initialize_repository(repository='repo', encryption_mode='repokey', local_path='borg1')
def test_initialize_repository_with_remote_path_calls_borg_with_remote_path_parameter():
insert_subprocess_mock(INIT_COMMAND + ('--remote-path', 'borg1'))
insert_info_command_mock(INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE)
insert_init_command_mock(INIT_COMMAND + ('--remote-path', 'borg1'))
module.initialize_repository(repository='repo', encryption_mode='repokey', remote_path='borg1')