mirror of
https://github.com/borgbase/vorta
synced 2024-12-21 23:33:13 +00:00
Give option to break repository lock. By @samuel-w (#863)
This commit is contained in:
parent
0170512a7e
commit
848bcc57ba
14 changed files with 134 additions and 22 deletions
|
@ -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
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
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 @@ def __init__(self, args_raw, single_app=False):
|
|||
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 @@ def _alert_missing_borg(self):
|
|||
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()
|
||||
|
|
|
@ -128,6 +128,12 @@ def prepare(cls, profile):
|
|||
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 @@ def read_async(fd):
|
|||
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 @@ def read_async(fd):
|
|||
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:
|
||||
|
|
29
src/vorta/borg/break_lock.py
Normal file
29
src/vorta/borg/break_lock.py
Normal 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
|
|
@ -125,8 +125,8 @@ def profile_select_action(self, index):
|
|||
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):
|
||||
|
|
0
tests/borg_json_output/create_break_stderr.json
Normal file
0
tests/borg_json_output/create_break_stderr.json
Normal file
0
tests/borg_json_output/create_break_stdout.json
Normal file
0
tests/borg_json_output/create_break_stdout.json
Normal file
1
tests/borg_json_output/create_lock_stderr.json
Normal file
1
tests/borg_json_output/create_lock_stderr.json
Normal 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"}
|
0
tests/borg_json_output/create_lock_stdout.json
Normal file
0
tests/borg_json_output/create_lock_stdout.json
Normal file
2
tests/borg_json_output/create_perm_stderr.json
Normal file
2
tests/borg_json_output/create_perm_stderr.json
Normal 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"}
|
0
tests/borg_json_output/create_perm_stdout.json
Normal file
0
tests/borg_json_output/create_perm_stdout.json
Normal 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 _read_json(subcommand):
|
|||
@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
43
tests/test_lock.py
Normal 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)
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue