Bladeren bron

Add "break-lock" action for removing any repository and cache locks leftover from Borg aborting (#357).

Dan Helfman 2 jaren geleden
bovenliggende
commit
ba8fbe7a44

+ 2 - 0
NEWS

@@ -1,4 +1,6 @@
 1.7.3.dev0
+ * #357: Add "break-lock" action for removing any repository and cache locks leftover from Borg
+   aborting.
  * #587: When the "read_special" option is true or database hooks are enabled, auto-exclude special
    files for a "create" action to prevent Borg from hanging.
  * #587: Warn when ignoring a configured "read_special" value of false, as true is needed when

+ 31 - 0
borgmatic/borg/break_lock.py

@@ -0,0 +1,31 @@
+import logging
+
+from borgmatic.borg import environment, flags
+from borgmatic.execute import execute_command
+
+logger = logging.getLogger(__name__)
+
+
+def break_lock(
+    repository, storage_config, local_borg_version, local_path='borg', remote_path=None,
+):
+    '''
+    Given a local or remote repository path, a storage configuration dict, the local Borg version,
+    and optional local and remote Borg paths, break any repository and cache locks leftover from Borg
+    aborting.
+    '''
+    umask = storage_config.get('umask', None)
+    lock_wait = storage_config.get('lock_wait', None)
+
+    full_command = (
+        (local_path, 'break-lock')
+        + (('--remote-path', remote_path) if remote_path else ())
+        + (('--umask', str(umask)) if umask else ())
+        + (('--lock-wait', str(lock_wait)) if lock_wait else ())
+        + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+        + (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+        + flags.make_repository_flags(repository, local_borg_version)
+    )
+
+    borg_environment = environment.make_environment(storage_config)
+    execute_command(full_command, borg_local_path=local_path, extra_environment=borg_environment)

+ 1 - 0
borgmatic/borg/create.py

@@ -407,6 +407,7 @@ def create_archive(
 
     # If read_special is enabled, exclude files that might cause Borg to hang.
     if read_special:
+        logger.debug(f'{repository}: Collecting special file paths')
         special_file_paths = collect_special_file_paths(
             create_command,
             local_path,

+ 14 - 0
borgmatic/commands/arguments.py

@@ -19,6 +19,7 @@ SUBPARSER_ALIASES = {
     'rinfo': [],
     'info': ['-i'],
     'transfer': [],
+    'break-lock': [],
     'borg': [],
 }
 
@@ -774,6 +775,19 @@ def make_parsers():
     )
     info_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
 
+    break_lock_parser = subparsers.add_parser(
+        'break-lock',
+        aliases=SUBPARSER_ALIASES['break-lock'],
+        help='Break the repository and cache locks left behind by Borg aborting',
+        description='Break Borg repository and cache locks left behind by Borg aborting',
+        add_help=False,
+    )
+    break_lock_group = break_lock_parser.add_argument_group('break-lock arguments')
+    break_lock_group.add_argument(
+        '--repository',
+        help='Path of repository to break the lock for, defaults to the configured repository if there is only one',
+    )
+
     borg_parser = subparsers.add_parser(
         'borg',
         aliases=SUBPARSER_ALIASES['borg'],

+ 13 - 0
borgmatic/commands/borgmatic.py

@@ -13,6 +13,7 @@ import pkg_resources
 
 import borgmatic.commands.completion
 from borgmatic.borg import borg as borg_borg
+from borgmatic.borg import break_lock as borg_break_lock
 from borgmatic.borg import check as borg_check
 from borgmatic.borg import compact as borg_compact
 from borgmatic.borg import create as borg_create
@@ -731,6 +732,18 @@ def run_actions(
             )
             if json_output:  # pragma: nocover
                 yield json.loads(json_output)
+    if 'break-lock' in arguments:
+        if arguments['break-lock'].repository is None or validate.repositories_match(
+            repository, arguments['break-lock'].repository
+        ):
+            logger.warning(f'{repository}: Breaking repository and cache locks')
+            borg_break_lock.break_lock(
+                repository,
+                storage,
+                local_borg_version,
+                local_path=local_path,
+                remote_path=remote_path,
+            )
     if 'borg' in arguments:
         if arguments['borg'].repository is None or validate.repositories_match(
             repository, arguments['borg'].repository

+ 1 - 1
docs/Dockerfile

@@ -4,7 +4,7 @@ COPY . /app
 RUN apk add --no-cache py3-pip py3-ruamel.yaml py3-ruamel.yaml.clib
 RUN pip install --no-cache /app && generate-borgmatic-config && chmod +r /etc/borgmatic/config.yaml
 RUN borgmatic --help > /command-line.txt \
-    && for action in rcreate transfer prune compact create check extract export-tar mount umount restore rlist list rinfo info borg; do \
+    && for action in rcreate transfer prune compact create check extract export-tar mount umount restore rlist list rinfo info break-lock borg; do \
            echo -e "\n--------------------------------------------------------------------------------\n" >> /command-line.txt \
            && borgmatic "$action" --help >> /command-line.txt; done
 

+ 70 - 0
tests/unit/borg/test_break_lock.py

@@ -0,0 +1,70 @@
+import logging
+
+from flexmock import flexmock
+
+from borgmatic.borg import break_lock as module
+
+from ..test_verbosity import insert_logging_mock
+
+
+def insert_execute_command_mock(command):
+    flexmock(module.environment).should_receive('make_environment')
+    flexmock(module).should_receive('execute_command').with_args(
+        command, borg_local_path='borg', extra_environment=None,
+    ).once()
+
+
+def test_break_lock_calls_borg_with_required_flags():
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+    insert_execute_command_mock(('borg', 'break-lock', 'repo'))
+
+    module.break_lock(
+        repository='repo', storage_config={}, local_borg_version='1.2.3',
+    )
+
+
+def test_break_lock_calls_borg_with_remote_path_flags():
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+    insert_execute_command_mock(('borg', 'break-lock', '--remote-path', 'borg1', 'repo'))
+
+    module.break_lock(
+        repository='repo', storage_config={}, local_borg_version='1.2.3', remote_path='borg1',
+    )
+
+
+def test_break_lock_calls_borg_with_umask_flags():
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+    insert_execute_command_mock(('borg', 'break-lock', '--umask', '0770', 'repo'))
+
+    module.break_lock(
+        repository='repo', storage_config={'umask': '0770'}, local_borg_version='1.2.3',
+    )
+
+
+def test_break_lock_calls_borg_with_lock_wait_flags():
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+    insert_execute_command_mock(('borg', 'break-lock', '--lock-wait', '5', 'repo'))
+
+    module.break_lock(
+        repository='repo', storage_config={'lock_wait': '5'}, local_borg_version='1.2.3',
+    )
+
+
+def test_break_lock_with_log_info_calls_borg_with_info_parameter():
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+    insert_execute_command_mock(('borg', 'break-lock', '--info', 'repo'))
+    insert_logging_mock(logging.INFO)
+
+    module.break_lock(
+        repository='repo', storage_config={}, local_borg_version='1.2.3',
+    )
+
+
+def test_break_lock_with_log_debug_calls_borg_with_debug_flags():
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+    insert_execute_command_mock(('borg', 'break-lock', '--debug', '--show-rc', 'repo'))
+    insert_logging_mock(logging.DEBUG)
+
+    module.break_lock(
+        repository='repo', storage_config={}, local_borg_version='1.2.3',
+    )

+ 25 - 0
tests/unit/commands/test_borgmatic.py

@@ -712,6 +712,31 @@ def test_run_actions_does_not_raise_for_info_action():
     )
 
 
+def test_run_actions_does_not_raise_for_break_lock_action():
+    flexmock(module.validate).should_receive('repositories_match').and_return(True)
+    flexmock(module.borg_break_lock).should_receive('break_lock')
+    arguments = {
+        'global': flexmock(monitoring_verbosity=1, dry_run=False),
+        'break-lock': flexmock(repository=flexmock()),
+    }
+
+    list(
+        module.run_actions(
+            arguments=arguments,
+            config_filename='test.yaml',
+            location={'repositories': ['repo']},
+            storage={},
+            retention={},
+            consistency={},
+            hooks={},
+            local_path=None,
+            remote_path=None,
+            local_borg_version=None,
+            repository_path='repo',
+        )
+    )
+
+
 def test_run_actions_does_not_raise_for_borg_action():
     flexmock(module.validate).should_receive('repositories_match').and_return(True)
     flexmock(module.borg_rlist).should_receive('resolve_archive_name').and_return(flexmock())