Give option to break repository lock. By @samuel-w (#863)

This commit is contained in:
Manu 2021-02-17 09:58:42 +08:00 committed by GitHub
parent 0170512a7e
commit 848bcc57ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 134 additions and 22 deletions

View File

@ -75,7 +75,7 @@ ignore =
max-line-length = 120
exclude =
build,dist,.git,.idea,.cache,.tox,.eggs,
./src/vorta/__init__.py
./src/vorta/__init__.py,.direnv
[tox:tox]
envlist = py36,py37,py38,flake8

View File

@ -9,6 +9,7 @@ from PyQt5.QtWidgets import QMessageBox
from vorta.borg.borg_thread import BorgThread
from vorta.borg.create import BorgCreateThread
from vorta.borg.version import BorgVersionThread
from vorta.borg.break_lock import BorgBreakThread
from vorta.config import TEMP_DIR
from vorta.i18n import init_translations, translate
from vorta.models import BackupProfileModel, SettingsModel, cleanup_db
@ -35,7 +36,7 @@ class VortaApp(QtSingleApplication):
backup_started_event = QtCore.pyqtSignal()
backup_finished_event = QtCore.pyqtSignal(dict)
backup_cancelled_event = QtCore.pyqtSignal()
backup_log_event = QtCore.pyqtSignal(str)
backup_log_event = QtCore.pyqtSignal(str, dict)
backup_progress_event = QtCore.pyqtSignal(str)
def __init__(self, args_raw, single_app=False):
@ -73,6 +74,7 @@ class VortaApp(QtSingleApplication):
self.backup_finished_event.connect(self.backup_finished_event_response)
self.backup_cancelled_event.connect(self.backup_cancelled_event_response)
self.message_received_event.connect(self.message_received_event_response)
self.backup_log_event.connect(self.react_to_log)
self.aboutToQuit.connect(cleanup_db)
self.set_borg_details_action()
self.installEventFilter(self)
@ -171,3 +173,38 @@ class VortaApp(QtSingleApplication):
msg.setInformativeText(self.tr("Vorta was unable to locate a usable Borg Backup binary."))
msg.setStandardButtons(QMessageBox.Ok)
msg.exec()
def react_to_log(self, mgs, context):
"""
Trigger Vorta actions based on Borg logs. E.g. repo lock.
"""
msgid = context.get('msgid')
if msgid == 'LockTimeout':
profile = BackupProfileModel.get(name=context['profile_name'])
repo_url = context.get('repo_url')
msg = QMessageBox()
msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
msg.setText(
self.tr(
f"The repository at {repo_url} might be in use by another computer. Override it and continue?"))
msg.accepted.connect(lambda: self.break_lock(profile))
msg.setWindowTitle(self.tr("Repository In Use"))
self._msg = msg
msg.show()
elif msgid == 'LockFailed':
repo_url = context.get('repo_url')
msg = QMessageBox()
msg.setText(
self.tr(
f"You do not have permission to access the repository at {repo_url}. Gain access and try again.")) # noqa: E501
msg.setWindowTitle(self.tr("No Repository Permissions"))
self._msg = msg
msg.show()
def break_lock(self, profile):
params = BorgBreakThread.prepare(profile)
if not params['ok']:
self.set_progress(params['message'])
return
thread = BorgBreakThread(params['cmd'], params, parent=self)
thread.start()

View File

@ -128,6 +128,12 @@ class BorgThread(QtCore.QThread, BackupProfileMixin):
ret['message'] = trans_late('messages', 'Add a backup repository first.')
return ret
if profile.ssh_key is not None and profile.repo.is_remote_repo() and \
not os.path.isfile(os.path.expanduser(f'~/.ssh/{profile.ssh_key}')):
ret['message'] = trans_late(
'messages', 'Your SSH key {} is missing. Add or change your key and try again.'.format(profile.ssh_key))
return ret
if not borg_compat.check('JSON_LOG'):
ret['message'] = trans_late('messages', 'Your Borg version is too old. >=1.1.0 is required.')
return ret
@ -223,12 +229,20 @@ class BorgThread(QtCore.QThread, BackupProfileMixin):
for line in stderr.split('\n'):
try:
parsed = json.loads(line)
if parsed['type'] == 'log_message':
self.app.backup_log_event.emit(f'{parsed["levelname"]}: {parsed["message"]}')
context = {
'msgid': parsed.get('msgid'),
'repo_url': self.params['repo_url'],
'profile_name': self.params.get('profile_name'),
'cmd': self.params['cmd'][1]
}
self.app.backup_log_event.emit(
f'{parsed["levelname"]}: {parsed["message"]}', context)
level_int = getattr(logging, parsed["levelname"])
logger.log(level_int, parsed["message"])
elif parsed['type'] == 'file_status':
self.app.backup_log_event.emit(f'{parsed["path"]} ({parsed["status"]})')
self.app.backup_log_event.emit(f'{parsed["path"]} ({parsed["status"]})', {})
elif parsed['type'] == 'archive_progress':
msg = (
f"{self.category_label['files']}: {parsed['nfiles']}, "
@ -240,7 +254,7 @@ class BorgThread(QtCore.QThread, BackupProfileMixin):
except json.decoder.JSONDecodeError:
msg = line.strip()
if msg: # Log only if there is something to log.
self.app.backup_log_event.emit(msg)
self.app.backup_log_event.emit(msg, {})
logger.warning(msg)
if p.poll() is not None:

View File

@ -0,0 +1,29 @@
from .borg_thread import BorgThread
class BorgBreakThread(BorgThread):
def started_event(self):
self.app.backup_started_event.emit()
self.app.backup_progress_event.emit(self.tr('Breaking repository lock...'))
def finished_event(self, result):
self.app.backup_finished_event.emit(result)
self.app.backup_progress_event.emit(self.tr('Repository lock broken. Please redo your last action.'))
self.result.emit(result)
@classmethod
def prepare(cls, profile):
ret = super().prepare(profile)
if not ret['ok']:
return ret
else:
ret['ok'] = False # Set back to false, so we can do our own checks here.
cmd = ['borg', 'break-lock', '--info', '--log-json']
cmd.append(f'{profile.repo.url}')
ret['ok'] = True
ret['cmd'] = cmd
return ret

View File

@ -125,8 +125,8 @@ class MainWindow(MainWindowBase, MainWindowUI):
self.repoTab.populate_from_profile()
self.sourceTab.populate_from_profile()
self.scheduleTab.populate_from_profile()
SettingsModel.update({SettingsModel.str_value: self.current_profile.id})\
.where(SettingsModel.key == 'previous_profile_id')\
SettingsModel.update({SettingsModel.str_value: self.current_profile.id}) \
.where(SettingsModel.key == 'previous_profile_id') \
.execute()
def profile_rename_action(self):

View File

@ -0,0 +1 @@
{"type": "log_message", "time": 1605936838.3639696, "message": "Failed to create/acquire the lock /tmp/another/lock.exclusive (timeout).", "levelname": "ERROR", "name": "borg.archiver", "msgid": "LockTimeout"}

View File

@ -0,0 +1,2 @@
{"type": "log_message", "time": 1605936460.5992384, "message": "Failed to create/acquire the lock /tmp/another/lock.exclusive ([Errno 13] Permission denied: '/tmp/another/lock.exclusive').", "levelname": "ERROR", "name": "borg.archiver", "msgid": "LockFailed"}
{"type": "log_message", "time": 1605936460.5994494, "message": "Traceback (most recent call last):\n File \"/usr/lib/python3/dist-packages/borg/archiver.py\", line 4591, in main\n exit_code = archiver.run(args)\n File \"/usr/lib/python3/dist-packages/borg/archiver.py\", line 4523, in run\n return set_ec(func(args))\n File \"/usr/lib/python3/dist-packages/borg/archiver.py\", line 161, in wrapper\n with repository:\n File \"/usr/lib/python3/dist-packages/borg/repository.py\", line 190, in __enter__\n self.open(self.path, bool(self.exclusive), lock_wait=self.lock_wait, lock=self.do_lock)\n File \"/usr/lib/python3/dist-packages/borg/repository.py\", line 421, in open\n self.lock = Lock(os.path.join(path, 'lock'), exclusive, timeout=lock_wait, kill_stale_locks=hostname_is_unique()).acquire()\n File \"/usr/lib/python3/dist-packages/borg/locking.py\", line 350, in acquire\n self._wait_for_readers_finishing(remove, sleep)\n File \"/usr/lib/python3/dist-packages/borg/locking.py\", line 363, in _wait_for_readers_finishing\n self._lock.acquire()\n File \"/usr/lib/python3/dist-packages/borg/locking.py\", line 138, in acquire\n raise LockFailed(self.path, str(err)) from None\nborg.locking.LockFailed: Failed to create/acquire the lock /tmp/another/lock.exclusive ([Errno 13] Permission denied: '/tmp/another/lock.exclusive').\n\nPlatform: Linux github 5.8.0-29-generic #31-Ubuntu SMP Fri Nov 6 12:37:59 UTC 2020 x86_64\nLinux: Unknown Linux \nBorg: 1.1.14 Python: CPython 3.8.6 msgpack: 0.5.6\nPID: 64701 CWD: /home/user/Projects/vorta/tests/borg_json_output\nsys.argv: ['/usr/bin/borg', 'create', '--list', '--progress', '--info', '--log-json', '--json', '--filter=AM', '-C', 'lz4', '/tmp/another::github-asdf-2020-11-17T00:05:49', '/home/user/bashrc']\nSSH_ORIGINAL_COMMAND: None\n", "levelname": "ERROR", "name": "borg.archiver"}

View File

@ -56,7 +56,7 @@ def local_en():
@pytest.fixture(scope='function', autouse=True)
def cleanup(request, qapp, qtbot):
"""
Ensure BorgThread is stopped when new test starts.
Cleanup after each test
"""
def ensure_borg_thread_stopped():
qapp.backup_cancelled_event.emit()
@ -106,14 +106,3 @@ def borg_json_output():
@pytest.fixture
def rootdir():
return os.path.dirname(os.path.abspath(__file__))
def delete_current_profile(qapp):
''' Delete current profile for cleanup '''
main = qapp.main_window
target = BackupProfileModel.get(id=main.profileSelector.currentData())
if qapp.scheduler.get_job(target.id):
qapp.scheduler.remove_job(target.id)
target.delete_instance(recursive=True)
main.profileSelector.removeItem(main.profileSelector.currentIndex())
main.profile_select_action(0)

43
tests/test_lock.py Normal file
View File

@ -0,0 +1,43 @@
from PyQt5 import QtCore
import vorta.borg.borg_thread
import vorta.application
def test_create_perm_error(qapp, borg_json_output, mocker, qtbot):
main = qapp.main_window
mocker.patch.object(vorta.application.QMessageBox, 'show')
stdout, stderr = borg_json_output('create_perm')
popen_result = mocker.MagicMock(stdout=stdout, stderr=stderr, returncode=0)
mocker.patch.object(vorta.borg.borg_thread, 'Popen', return_value=popen_result)
qtbot.mouseClick(main.createStartBtn, QtCore.Qt.LeftButton)
qtbot.waitUntil(lambda: hasattr(qapp, '_msg'), timeout=10000)
assert qapp._msg.text().startswith("You do not have permission")
del qapp._msg
def test_create_lock(qapp, borg_json_output, mocker, qtbot):
main = qapp.main_window
mocker.patch.object(vorta.application.QMessageBox, 'show')
# Trigger locked repo
stdout, stderr = borg_json_output('create_lock')
popen_result = mocker.MagicMock(stdout=stdout, stderr=stderr, returncode=0)
mocker.patch.object(vorta.borg.borg_thread, 'Popen', return_value=popen_result)
qtbot.mouseClick(main.createStartBtn, QtCore.Qt.LeftButton)
qtbot.waitUntil(lambda: hasattr(qapp, '_msg'), timeout=10000)
assert "The repository at" in qapp._msg.text()
# Break locked repo
stdout, stderr = borg_json_output('create_break')
popen_result = mocker.MagicMock(stdout=stdout, stderr=stderr, returncode=0)
mocker.patch.object(vorta.borg.borg_thread, 'Popen', return_value=popen_result)
qtbot.waitUntil(lambda: main.createStartBtn.isEnabled(), timeout=3000) # Prevent thread collision
qapp._msg.accept()
exp_message_text = 'Repository lock broken. Please redo your last action.'
qtbot.waitUntil(lambda: main.progressText.text() == exp_message_text, timeout=5000)

View File

@ -1,6 +1,5 @@
from PyQt5 import QtCore
from PyQt5.QtWidgets import QDialogButtonBox
from .conftest import delete_current_profile
from vorta.models import BackupProfileModel
@ -18,8 +17,6 @@ def test_profile_add(qapp, qtbot):
assert BackupProfileModel.get_or_none(name='Test Profile') is not None
assert main.profileSelector.currentText() == 'Test Profile'
delete_current_profile(qapp)
def test_profile_edit(qapp, qtbot):
main = qapp.main_window