From 38994c78fc65db8d0c2f4f1899f98c1f31ae9184 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Sat, 21 Nov 2015 20:50:53 +0100 Subject: [PATCH] implement borg break-lock REPO command, fixes #157 due to borg's architecture, breaking the repo lock needs first creating a repository object. this would usually try to get a lock and then block if there already is one. thus I added a flag to open without trying to create a lock. --- borg/archiver.py | 30 +++++++++++++++++++++++++++--- borg/cache.py | 5 +++++ borg/remote.py | 12 ++++++++---- borg/repository.py | 14 ++++++++++---- borg/testsuite/archiver.py | 4 ++++ 5 files changed, 54 insertions(+), 11 deletions(-) diff --git a/borg/archiver.py b/borg/archiver.py index 55563d3a9..6e9a94222 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -42,11 +42,11 @@ class Archiver: self.verbose = verbose self.lock_wait = lock_wait - def open_repository(self, location, create=False, exclusive=False): + def open_repository(self, location, create=False, exclusive=False, lock=True): if location.proto == 'ssh': - repository = RemoteRepository(location, create=create, lock_wait=self.lock_wait) + repository = RemoteRepository(location, create=create, lock_wait=self.lock_wait, lock=lock) else: - repository = Repository(location.path, create=create, exclusive=exclusive, lock_wait=self.lock_wait) + repository = Repository(location.path, create=create, exclusive=exclusive, lock_wait=self.lock_wait, lock=lock) repository._location = location return repository @@ -573,6 +573,16 @@ class Archiver: print('Done.') return EXIT_SUCCESS + def do_break_lock(self, args): + """Break the repository lock (e.g. in case it was left by a dead borg.""" + repository = self.open_repository(args.repository, lock=False) + try: + repository.break_lock() + Cache.break_lock(repository) + finally: + repository.close() + return self.exit_code + helptext = {} helptext['patterns'] = ''' Exclude patterns use a variant of shell pattern syntax, with '*' matching any @@ -972,6 +982,20 @@ class Archiver: type=location_validator(archive=True), help='archive to display information about') + break_lock_epilog = textwrap.dedent(""" + This command breaks the repository and cache locks. + Please use carefully and only while no borg process (on any machine) is + trying to access the Cache or the Repository. + """) + subparser = subparsers.add_parser('break-lock', parents=[common_parser], + description=self.do_break_lock.__doc__, + epilog=break_lock_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter) + subparser.set_defaults(func=self.do_break_lock) + subparser.add_argument('repository', metavar='REPOSITORY', + type=location_validator(archive=False), + help='repository for which to break the locks') + prune_epilog = textwrap.dedent(""" The prune command prunes a repository by deleting archives not matching any of the specified retention options. This command is normally used by diff --git a/borg/cache.py b/borg/cache.py index 6116df497..c911283db 100644 --- a/borg/cache.py +++ b/borg/cache.py @@ -32,6 +32,11 @@ class Cache: class EncryptionMethodMismatch(Error): """Repository encryption method changed since last access, refusing to continue""" + @staticmethod + def break_lock(repository, path=None): + path = path or os.path.join(get_cache_dir(), hexlify(repository.id).decode('ascii')) + UpgradableLock(os.path.join(path, 'lock'), exclusive=True).break_lock() + def __init__(self, repository, key, manifest, path=None, sync=True, do_files=False, warn_if_unencrypted=True, lock_wait=None): self.lock = None diff --git a/borg/remote.py b/borg/remote.py index 1d5e9cab6..3a6a5e5ad 100644 --- a/borg/remote.py +++ b/borg/remote.py @@ -50,6 +50,7 @@ class RepositoryServer: # pragma: no cover 'rollback', 'save_key', 'load_key', + 'break_lock', ) def __init__(self, restrict_to_paths): @@ -97,7 +98,7 @@ class RepositoryServer: # pragma: no cover def negotiate(self, versions): return 1 - def open(self, path, create=False, lock_wait=None): + def open(self, path, create=False, lock_wait=None, lock=True): path = os.fsdecode(path) if path.startswith('/~'): path = path[1:] @@ -108,7 +109,7 @@ class RepositoryServer: # pragma: no cover break else: raise PathNotAllowed(path) - self.repository = Repository(path, create, lock_wait=lock_wait) + self.repository = Repository(path, create, lock_wait=lock_wait, lock=lock) return self.repository.id @@ -122,7 +123,7 @@ class RemoteRepository: def __init__(self, name): self.name = name - def __init__(self, location, create=False, lock_wait=None): + def __init__(self, location, create=False, lock_wait=None, lock=True): self.location = location self.preload_ids = [] self.msgid = 0 @@ -154,7 +155,7 @@ class RemoteRepository: raise ConnectionClosedWithHint('Is borg working on the server?') if version != 1: raise Exception('Server insisted on using unsupported protocol version %d' % version) - self.id = self.call('open', location.path, create, lock_wait) + self.id = self.call('open', location.path, create, lock_wait, lock) def __del__(self): self.close() @@ -308,6 +309,9 @@ class RemoteRepository: def load_key(self): return self.call('load_key') + def break_lock(self): + return self.call('break_lock') + def close(self): if self.p: self.p.stdin.close() diff --git a/borg/repository.py b/borg/repository.py index 77d1d234b..f3189bc3b 100644 --- a/borg/repository.py +++ b/borg/repository.py @@ -51,7 +51,7 @@ class Repository: class ObjectNotFound(ErrorWithTraceback): """Object with key {} not found in repository {}.""" - def __init__(self, path, create=False, exclusive=False, lock_wait=None): + def __init__(self, path, create=False, exclusive=False, lock_wait=None, lock=True): self.path = os.path.abspath(path) self.io = None self.lock = None @@ -59,7 +59,7 @@ class Repository: self._active_txn = False if create: self.create(self.path) - self.open(self.path, exclusive, lock_wait=lock_wait) + self.open(self.path, exclusive, lock_wait=lock_wait, lock=lock) def __del__(self): self.close() @@ -129,11 +129,17 @@ class Repository: self.replay_segments(replay_from, segments_transaction_id) return self.get_index_transaction_id() - def open(self, path, exclusive, lock_wait=None): + def break_lock(self): + UpgradableLock(os.path.join(self.path, 'lock')).break_lock() + + def open(self, path, exclusive, lock_wait=None, lock=True): self.path = path if not os.path.isdir(path): raise self.DoesNotExist(path) - self.lock = UpgradableLock(os.path.join(path, 'lock'), exclusive, timeout=lock_wait).acquire() + if lock: + self.lock = UpgradableLock(os.path.join(path, 'lock'), exclusive, timeout=lock_wait).acquire() + else: + self.lock = None self.config = ConfigParser(interpolation=None) self.config.read(os.path.join(self.path, 'config')) if 'repository' not in self.config.sections() or self.config.getint('repository', 'version') != 1: diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index d61834d99..61471586a 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -724,6 +724,10 @@ class ArchiverTestCase(ArchiverTestCaseBase): self.assert_in('test-2', output) self.assert_not_in('something-else', output) + def test_break_lock(self): + self.cmd('init', self.repository_location) + self.cmd('break-lock', self.repository_location) + def test_usage(self): if self.FORK_DEFAULT: self.cmd(exit_code=0)