Initial backup-and-restore GUI restoration

Initial restoration: basic backup-and-restore works, but
- progress bar in backup does not work
- selecting only some VMs to restore does not work
- various miscellaneous enhancements are not yet in place
This commit is contained in:
Marta Marczykowska-Górecka 2017-12-10 21:14:14 +01:00
parent 05d393035b
commit 7a4e4b35d5
No known key found for this signature in database
GPG Key ID: 9A752C30B26FD04B
6 changed files with 392 additions and 486 deletions

View File

@ -1,4 +1,4 @@
#!/usr/bin/python2 #!/usr/bin/python3
# pylint: skip-file # pylint: skip-file
# #
# The Qubes OS Project, http://www.qubes-os.org # The Qubes OS Project, http://www.qubes-os.org
@ -21,78 +21,68 @@
# #
# #
import sys import traceback
import os
import signal import signal
import shutil import shutil
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from qubes.qubes import QubesVmCollection from qubesadmin import Qubes, events, exc
from qubes.qubes import QubesException from qubesadmin import utils as admin_utils
from qubes.qubes import QubesDaemonPidfile from qubes.storage.file import get_disk_usage
from qubes.qubes import QubesHost
from qubes import backup
from qubes import qubesutils
import qubesmanager.resources_rc from PyQt4 import QtCore, QtGui # pylint: disable=import-error
from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent
import time
from .thread_monitor import *
from operator import itemgetter
from datetime import datetime
from string import replace
from .ui_backupdlg import * from .ui_backupdlg import *
from .multiselectwidget import * from .multiselectwidget import *
from .backup_utils import * from .backup_utils import *
import main from . import utils
import grp,pwd import grp
import pwd
import sys
import os
from .thread_monitor import *
import time
class BackupVMsWindow(Ui_Backup, QWizard): class BackupVMsWindow(Ui_Backup, QWizard):
__pyqtSignals__ = ("backup_progress(int)",) __pyqtSignals__ = ("backup_progress(int)",)
def __init__(self, app, qvm_collection, blk_manager, shutdown_vm_func, parent=None): def __init__(self, app, qvm_collection, parent=None):
super(BackupVMsWindow, self).__init__(parent) super(BackupVMsWindow, self).__init__(parent)
self.app = app self.app = app
self.qvm_collection = qvm_collection self.qvm_collection = qvm_collection
self.blk_manager = blk_manager self.backup_settings = QtCore.QSettings()
self.shutdown_vm_func = shutdown_vm_func
self.func_output = [] self.func_output = []
self.selected_vms = [] self.selected_vms = []
self.tmpdir_to_remove = None self.tmpdir_to_remove = None
self.canceled = False self.canceled = False
self.vm = self.qvm_collection[0]
self.files_to_backup = None self.files_to_backup = None
assert self.vm != None
self.setupUi(self) self.setupUi(self)
self.progress_status.text = self.tr("Backup in progress...") self.progress_status.text = self.tr("Backup in progress...")
self.show_running_vms_warning(False)
self.dir_line_edit.setReadOnly(False) self.dir_line_edit.setReadOnly(False)
self.select_vms_widget = MultiSelectWidget(self) self.select_vms_widget = MultiSelectWidget(self)
self.verticalLayout.insertWidget(1, self.select_vms_widget) self.verticalLayout.insertWidget(1, self.select_vms_widget)
self.connect(self, SIGNAL("currentIdChanged(int)"), self.current_page_changed) self.connect(self, SIGNAL("currentIdChanged(int)"),
self.connect(self.select_vms_widget, SIGNAL("selected_changed()"), self.check_running) self.current_page_changed)
self.connect(self.select_vms_widget, SIGNAL("items_removed(PyQt_PyObject)"), self.vms_removed) self.connect(self.select_vms_widget,
self.connect(self.select_vms_widget, SIGNAL("items_added(PyQt_PyObject)"), self.vms_added) SIGNAL("items_removed(PyQt_PyObject)"),
self.refresh_button.clicked.connect(self.check_running) self.vms_removed)
self.shutdown_running_vms_button.clicked.connect(self.shutdown_all_running_selected) self.connect(self.select_vms_widget,
self.connect(self, SIGNAL("backup_progress(int)"), self.progress_bar.setValue) SIGNAL("items_added(PyQt_PyObject)"),
self.dir_line_edit.connect(self.dir_line_edit, SIGNAL("textChanged(QString)"), self.backup_location_changed) self.vms_added)
self.connect(self, SIGNAL("backup_progress(int)"),
self.progress_bar.setValue)
self.dir_line_edit.connect(self.dir_line_edit,
SIGNAL("textChanged(QString)"),
self.backup_location_changed)
self.select_vms_page.isComplete = self.has_selected_vms self.select_vms_page.isComplete = self.has_selected_vms
self.select_dir_page.isComplete = self.has_selected_dir_and_pass self.select_dir_page.isComplete = self.has_selected_dir_and_pass
@ -112,40 +102,62 @@ class BackupVMsWindow(Ui_Backup, QWizard):
self.backup_location_changed) self.backup_location_changed)
self.total_size = 0 self.total_size = 0
self.__fill_vms_list__()
fill_appvms_list(self) # TODO: is the is_running criterion really necessary? It's designed to
self.load_settings() # avoid backuping a VM into itself or surprising the user with starting
# a VM when they didn't plan to.
# TODO: inform the user only running VMs are listed?
self.target_vm_list, self.target_vm_idx = utils.prepare_vm_choice(
self.appvm_combobox,
self.qvm_collection,
None,
self.qvm_collection.domains['dom0'],
(lambda vm: vm.klass != 'TemplateVM' and vm.is_running()),
allow_internal=False,
allow_default=False,
allow_none=False
)
selected = self.load_settings()
self.__fill_vms_list__(selected)
def load_settings(self): def load_settings(self):
dest_vm_name = main.manager_window.manager_settings.value( try:
'backup/vmname', defaultValue="") profile_data = load_backup_profile()
dest_vm_idx = self.appvm_combobox.findText(dest_vm_name.toString()) except Exception as ex: # TODO: fix just for file not found
return
if not profile_data:
return
if 'destination_vm' in profile_data:
dest_vm_name = profile_data['destination_vm']
dest_vm_idx = self.appvm_combobox.findText(dest_vm_name)
if dest_vm_idx > -1: if dest_vm_idx > -1:
self.appvm_combobox.setCurrentIndex(dest_vm_idx) self.appvm_combobox.setCurrentIndex(dest_vm_idx)
if main.manager_window.manager_settings.contains('backup/path'): if 'destination_path' in profile_data:
dest_path = main.manager_window.manager_settings.value( dest_path = profile_data['destination_path']
'backup/path', defaultValue=None) self.dir_line_edit.setText(dest_path)
self.dir_line_edit.setText(dest_path.toString())
if main.manager_window.manager_settings.contains('backup/encrypt'): if 'passphrase_text' in profile_data:
encrypt = main.manager_window.manager_settings.value( self.passphrase_line_edit.setText(profile_data['passphrase_text'])
'backup/encrypt', defaultValue=None) self.passphrase_line_edit_verify.setText(
self.encryption_checkbox.setChecked(encrypt.toBool()) profile_data['passphrase_text'])
# TODO: make a checkbox for saving the profile
# TODO: warn that unknown data will be overwritten
if 'include' in profile_data:
return profile_data['include']
return None
def save_settings(self): def save_settings(self):
main.manager_window.manager_settings.setValue( settings = {'destination_vm': self.appvm_combobox.currentText(),
'backup/vmname', self.appvm_combobox.currentText()) 'destination_path': self.dir_line_edit.text(),
main.manager_window.manager_settings.setValue( 'include': [vm.name for vm in self.selected_vms],
'backup/path', self.dir_line_edit.text()) 'passphrase_text': self.passphrase_line_edit.text()}
main.manager_window.manager_settings.setValue( # TODO: add compression when it is added
'backup/encrypt', self.encryption_checkbox.isChecked()) write_backup_profile(settings)
def show_running_vms_warning(self, show):
self.running_vms_warning.setVisible(show)
self.shutdown_running_vms_button.setVisible(show)
self.refresh_button.setVisible(show)
class VmListItem(QListWidgetItem): class VmListItem(QListWidgetItem):
def __init__(self, vm): def __init__(self, vm):
@ -153,114 +165,45 @@ class BackupVMsWindow(Ui_Backup, QWizard):
if vm.qid == 0: if vm.qid == 0:
local_user = grp.getgrnam('qubes').gr_mem[0] local_user = grp.getgrnam('qubes').gr_mem[0]
home_dir = pwd.getpwnam(local_user).pw_dir home_dir = pwd.getpwnam(local_user).pw_dir
self.size = qubesutils.get_disk_usage(home_dir) self.size = get_disk_usage(home_dir)
else: else:
self.size = self.get_vm_size(vm) self.size = vm.get_disk_utilization()
super(BackupVMsWindow.VmListItem, self).__init__(vm.name+ " (" + qubesutils.size_to_human(self.size) + ")") super(BackupVMsWindow.VmListItem, self).__init__(
vm.name + " (" + admin_utils.size_to_human(self.size) + ")")
def get_vm_size(self, vm): def __fill_vms_list__(self, selected=None):
size = 0 for vm in self.qvm_collection.domains:
if vm.private_img is not None: if vm.features.get('internal', False):
size += qubesutils.get_disk_usage (vm.private_img)
if vm.updateable:
size += qubesutils.get_disk_usage(vm.root_img)
return size
def __fill_vms_list__(self):
for vm in self.qvm_collection.values():
if vm.internal:
continue continue
item = BackupVMsWindow.VmListItem(vm) item = BackupVMsWindow.VmListItem(vm)
if vm.include_in_backups == True: if (selected is None and
getattr(vm, 'include_in_backups', True)) \
or (selected and vm.name in selected):
self.select_vms_widget.selected_list.addItem(item) self.select_vms_widget.selected_list.addItem(item)
self.total_size += item.size self.total_size += item.size
else: else:
self.select_vms_widget.available_list.addItem(item) self.select_vms_widget.available_list.addItem(item)
self.select_vms_widget.available_list.sortItems() self.select_vms_widget.available_list.sortItems()
self.select_vms_widget.selected_list.sortItems() self.select_vms_widget.selected_list.sortItems()
self.check_running()
self.total_size_label.setText(qubesutils.size_to_human(self.total_size)) self.unrecognized_config_label.setVisible(
selected is not None and
len(selected) != len(self.select_vms_widget.selected_list))
self.total_size_label.setText(
admin_utils.size_to_human(self.total_size))
def vms_added(self, items): def vms_added(self, items):
for i in items: for i in items:
self.total_size += i.size self.total_size += i.size
self.total_size_label.setText(qubesutils.size_to_human(self.total_size)) self.total_size_label.setText(
admin_utils.size_to_human(self.total_size))
def vms_removed(self, items): def vms_removed(self, items):
for i in items: for i in items:
self.total_size -= i.size self.total_size -= i.size
self.total_size_label.setText(qubesutils.size_to_human(self.total_size)) self.total_size_label.setText(
admin_utils.size_to_human(self.total_size))
def check_running(self):
some_selected_vms_running = False
for i in range(self.select_vms_widget.selected_list.count()):
item = self.select_vms_widget.selected_list.item(i)
if item.vm.is_running() and item.vm.qid != 0:
item.setForeground(QBrush(QColor(255, 0, 0)))
some_selected_vms_running = True
else:
item.setForeground(QBrush(QColor(0, 0, 0)))
self.show_running_vms_warning(some_selected_vms_running)
for i in range(self.select_vms_widget.available_list.count()):
item = self.select_vms_widget.available_list.item(i)
if item.vm.is_running() and item.vm.qid != 0:
item.setForeground(QBrush(QColor(255, 0, 0)))
else:
item.setForeground(QBrush(QColor(0, 0, 0)))
return some_selected_vms_running
def shutdown_all_running_selected(self):
(names, vms) = self.get_running_vms()
if len(vms) == 0:
return
for vm in vms:
self.blk_manager.check_if_serves_as_backend(vm)
reply = QMessageBox.question(None, self.tr("VM Shutdown Confirmation"),
self.tr(
"Are you sure you want to power down the following VMs: "
"<b>{0}</b>?<br/>"
"<small>This will shutdown all the running applications "
"within them.</small>").format(', '.join(names)),
QMessageBox.Yes | QMessageBox.Cancel)
self.app.processEvents()
if reply == QMessageBox.Yes:
wait_time = 60.0
for vm in vms:
self.shutdown_vm_func(vm, wait_time*1000)
progress = QProgressDialog ("Shutting down VMs <b>{0}</b>...".format(', '.join(names)), "", 0, 0)
progress.setModal(True)
progress.show()
wait_for = wait_time
while self.check_running() and wait_for > 0:
self.app.processEvents()
time.sleep (0.5)
wait_for -= 0.5
progress.hide()
def get_running_vms(self):
names = []
vms = []
for i in range(self.select_vms_widget.selected_list.count()):
item = self.select_vms_widget.selected_list.item(i)
if item.vm.is_running() and item.vm.qid != 0:
names.append(item.vm.name)
vms.append(item.vm)
return (names, vms)
@pyqtSlot(name='on_select_path_button_clicked') @pyqtSlot(name='on_select_path_button_clicked')
def select_path_button_clicked(self): def select_path_button_clicked(self):
@ -268,44 +211,43 @@ class BackupVMsWindow(Ui_Backup, QWizard):
def validateCurrentPage(self): def validateCurrentPage(self):
if self.currentPage() is self.select_vms_page: if self.currentPage() is self.select_vms_page:
if self.check_running():
QMessageBox.information(None,
self.tr("Wait!"),
self.tr("Some selected VMs are running. "
"Running VMs can not be backuped. "
"Please shut them down or remove them from the list."))
return False
self.selected_vms = [] self.selected_vms = []
for i in range(self.select_vms_widget.selected_list.count()): for i in range(self.select_vms_widget.selected_list.count()):
self.selected_vms.append(self.select_vms_widget.selected_list.item(i).vm) self.selected_vms.append(
self.select_vms_widget.selected_list.item(i).vm)
elif self.currentPage() is self.select_dir_page: elif self.currentPage() is self.select_dir_page:
backup_location = str(self.dir_line_edit.text()) backup_location = str(self.dir_line_edit.text())
if not backup_location: if not backup_location:
QMessageBox.information(None, self.tr("Wait!"), QMessageBox.information(
None, self.tr("Wait!"),
self.tr("Enter backup target location first.")) self.tr("Enter backup target location first."))
return False return False
if self.appvm_combobox.currentIndex() == 0 and \ if self.appvm_combobox.currentIndex() == 0 \
not os.path.isdir(backup_location): and not os.path.isdir(backup_location):
QMessageBox.information(None, self.tr("Wait!"), QMessageBox.information(
None, self.tr("Wait!"),
self.tr("Selected directory do not exists or " self.tr("Selected directory do not exists or "
"not a directory (%s).") % backup_location) "not a directory (%s).") % backup_location)
return False return False
if not len(self.passphrase_line_edit.text()): if not len(self.passphrase_line_edit.text()):
QMessageBox.information(None, self.tr("Wait!"), QMessageBox.information(
self.tr("Enter passphrase for backup encryption/verification first.")) None, self.tr("Wait!"),
self.tr("Enter passphrase for backup "
"encryption/verification first."))
return False return False
if self.passphrase_line_edit.text() != self.passphrase_line_edit_verify.text(): if self.passphrase_line_edit.text() !=\
QMessageBox.information(None, self.passphrase_line_edit_verify.text():
self.tr("Wait!"), QMessageBox.information(
None, self.tr("Wait!"),
self.tr("Enter the same passphrase in both fields.")) self.tr("Enter the same passphrase in both fields."))
return False return False
return True return True
def gather_output(self, s): # def gather_output(self, s):
self.func_output.append(s) # self.func_output.append(s)
def update_progress_bar(self, value): def update_progress_bar(self, value):
self.emit(SIGNAL("backup_progress(int)"), value) self.emit(SIGNAL("backup_progress(int)"), value)
@ -314,19 +256,23 @@ class BackupVMsWindow(Ui_Backup, QWizard):
msg = [] msg = []
try: try:
backup.backup_do(self.dir_line_edit.text(), # TODO: this does nothing, events are not handled
self.files_to_backup, events_dispatcher = events.EventsDispatcher(self.app)
self.passphrase_line_edit.text(), events_dispatcher.add_handler('backup-progress',
progress_callback=self.update_progress_bar, self.update_progress_bar)
encrypted=self.encryption_checkbox.isChecked(), try:
appvm=self.target_appvm) vm = self.qvm_collection.domains[
#simulate_long_lasting_proces(10, self.update_progress_bar) self.appvm_combobox.currentText()]
except backup.BackupCanceledError as ex: if not vm.is_running():
msg.append(str(ex)) vm.start()
self.canceled = True self.qvm_collection.qubesd_call('dom0',
if ex.tmpdir: 'admin.backup.Execute',
self.tmpdir_to_remove = ex.tmpdir 'qubes-manager-backup')
except Exception as ex: except exc.QubesException as err:
# TODO fixme
print('\nBackup error: {}'.format(err), file=sys.stderr)
return 1
except Exception as ex: # TODO: fixme
print("Exception:", ex) print("Exception:", ex)
msg.append(str(ex)) msg.append(str(ex))
@ -340,27 +286,13 @@ class BackupVMsWindow(Ui_Backup, QWizard):
old_sigchld_handler = signal.signal(signal.SIGCHLD, signal.SIG_DFL) old_sigchld_handler = signal.signal(signal.SIGCHLD, signal.SIG_DFL)
if self.currentPage() is self.confirm_page: if self.currentPage() is self.confirm_page:
self.target_appvm = None self.save_settings()
if self.appvm_combobox.currentIndex() != 0: #An existing appvm chosen backup_summary = self.qvm_collection.qubesd_call(
self.target_appvm = self.qvm_collection.get_vm_by_name( 'dom0', 'admin.backup.Info', 'qubes-manager-backup')
self.appvm_combobox.currentText())
del self.func_output[:]
try:
self.files_to_backup = backup.backup_prepare(
self.selected_vms,
print_callback = self.gather_output,
hide_vm_names=self.encryption_checkbox.isChecked())
except Exception as ex:
print("Exception:", ex)
QMessageBox.critical(None,
self.tr("Error while preparing backup."),
self.tr("ERROR: {0}").format(ex))
self.textEdit.setReadOnly(True) self.textEdit.setReadOnly(True)
self.textEdit.setFontFamily("Monospace") self.textEdit.setFontFamily("Monospace")
self.textEdit.setText("\n".join(self.func_output)) self.textEdit.setText(backup_summary.decode())
self.save_settings()
elif self.currentPage() is self.commit_page: elif self.currentPage() is self.commit_page:
self.button(self.FinishButton).setDisabled(True) self.button(self.FinishButton).setDisabled(True)
@ -370,11 +302,11 @@ class BackupVMsWindow(Ui_Backup, QWizard):
and str(self.dir_line_edit.text()) and str(self.dir_line_edit.text())
.count("media/") > 0) .count("media/") > 0)
self.thread_monitor = ThreadMonitor() self.thread_monitor = ThreadMonitor()
thread = threading.Thread (target= self.__do_backup__ , args=(self.thread_monitor,)) thread = threading.Thread(target=self.__do_backup__,
args=(self.thread_monitor,))
thread.daemon = True thread.daemon = True
thread.start() thread.start()
counter = 0
while not self.thread_monitor.is_finished(): while not self.thread_monitor.is_finished():
self.app.processEvents() self.app.processEvents()
time.sleep(0.1) time.sleep(0.1)
@ -383,14 +315,18 @@ class BackupVMsWindow(Ui_Backup, QWizard):
if self.canceled: if self.canceled:
self.progress_status.setText(self.tr("Backup aborted.")) self.progress_status.setText(self.tr("Backup aborted."))
if self.tmpdir_to_remove: if self.tmpdir_to_remove:
if QMessageBox.warning(None, self.tr("Backup aborted"), if QMessageBox.warning(
self.tr("Do you want to remove temporary files from " None, self.tr("Backup aborted"),
"%s?") % self.tmpdir_to_remove, self.tr(
QMessageBox.Yes, QMessageBox.No) == QMessageBox.Yes: "Do you want to remove temporary files "
"from %s?") % self.tmpdir_to_remove,
QMessageBox.Yes, QMessageBox.No) == \
QMessageBox.Yes:
shutil.rmtree(self.tmpdir_to_remove) shutil.rmtree(self.tmpdir_to_remove)
else: else:
self.progress_status.setText(self.tr("Backup error.")) self.progress_status.setText(self.tr("Backup error."))
QMessageBox.warning(self, self.tr("Backup error!"), QMessageBox.warning(
self, self.tr("Backup error!"),
self.tr("ERROR: {}").format( self.tr("ERROR: {}").format(
self.thread_monitor.error_msg)) self.thread_monitor.error_msg))
else: else:
@ -402,9 +338,9 @@ class BackupVMsWindow(Ui_Backup, QWizard):
orig_text + self.tr( orig_text + self.tr(
" Please unmount your backup volume and cancel " " Please unmount your backup volume and cancel "
"the file selection dialog.")) "the file selection dialog."))
if self.target_appvm: if self.target_appvm: # FIXME I'm not sure if this works
self.target_appvm.run("QUBESRPC %s dom0" % "qubes" self.target_appvm.run(
".SelectDirectory") "QUBESRPC %s dom0" % "qubes.SelectDirectory")
self.button(self.CancelButton).setEnabled(False) self.button(self.CancelButton).setEnabled(False)
self.button(self.FinishButton).setEnabled(True) self.button(self.FinishButton).setEnabled(True)
self.showFileDialog.setEnabled(False) self.showFileDialog.setEnabled(False)
@ -414,8 +350,9 @@ class BackupVMsWindow(Ui_Backup, QWizard):
# cancell clicked while the backup is in progress. # cancell clicked while the backup is in progress.
# calling kill on tar. # calling kill on tar.
if self.currentPage() is self.commit_page: if self.currentPage() is self.commit_page:
if backup.backup_cancel(): pass # TODO: this does nothing
self.button(self.CancelButton).setDisabled(True) # if backup.backup_cancel():
# self.button(self.CancelButton).setDisabled(True)
else: else:
self.done(0) self.done(0)
@ -425,7 +362,8 @@ class BackupVMsWindow(Ui_Backup, QWizard):
def has_selected_dir_and_pass(self): def has_selected_dir_and_pass(self):
if not len(self.passphrase_line_edit.text()): if not len(self.passphrase_line_edit.text()):
return False return False
if self.passphrase_line_edit.text() != self.passphrase_line_edit_verify.text(): if self.passphrase_line_edit.text() != \
self.passphrase_line_edit_verify.text():
return False return False
return len(self.dir_line_edit.text()) > 0 return len(self.dir_line_edit.text()) > 0
@ -437,49 +375,37 @@ class BackupVMsWindow(Ui_Backup, QWizard):
# Copyright (c) 2002-2007 Pascal Varet <p.varet@gmail.com> # Copyright (c) 2002-2007 Pascal Varet <p.varet@gmail.com>
def handle_exception(exc_type, exc_value, exc_traceback): def handle_exception(exc_type, exc_value, exc_traceback):
import sys
import os.path
import traceback
filename, line, dummy, dummy = traceback.extract_tb(exc_traceback).pop() filename, line, dummy, dummy = traceback.extract_tb(exc_traceback).pop()
filename = os.path.basename(filename) filename = os.path.basename(filename)
error = "%s: %s" % (exc_type.__name__, exc_value) error = "%s: %s" % (exc_type.__name__, exc_value)
QMessageBox.critical(None, "Houston, we have a problem...", QtGui.QMessageBox.critical(
None,
"Houston, we have a problem...",
"Whoops. A critical error has occured. This is most likely a bug " "Whoops. A critical error has occured. This is most likely a bug "
"in Qubes Restore VMs application.<br><br>" "in Qubes Global Settings application.<br><br><b><i>%s</i></b>" %
"<b><i>%s</i></b>" % error + error + "at <b>line %d</b> of file <b>%s</b>.<br/><br/>"
"at <b>line %d</b> of file <b>%s</b>.<br/><br/>"
% (line, filename)) % (line, filename))
def app_main(): def main():
global qubes_host qtapp = QtGui.QApplication(sys.argv)
qubes_host = QubesHost() qtapp.setOrganizationName("The Qubes Project")
qtapp.setOrganizationDomain("http://qubes-os.org")
global app qtapp.setApplicationName("Qubes Backup VMs")
app = QApplication(sys.argv)
app.setOrganizationName("The Qubes Project")
app.setOrganizationDomain("http://qubes-os.org")
app.setApplicationName("Qubes Backup VMs")
sys.excepthook = handle_exception sys.excepthook = handle_exception
qvm_collection = QubesVmCollection() app = Qubes()
qvm_collection.lock_db_for_reading()
qvm_collection.load()
qvm_collection.unlock_db()
global backup_window backup_window = BackupVMsWindow(qtapp, app)
backup_window = BackupVMsWindow()
backup_window.show() backup_window.show()
app.exec_() qtapp.exec_()
app.exit() qtapp.exit()
if __name__ == "__main__": if __name__ == "__main__":
app_main() main()

View File

@ -20,90 +20,97 @@
# #
# #
import re import re
import sys
import os
from PyQt4.QtCore import * from PyQt4.QtCore import *
from PyQt4.QtGui import * from PyQt4.QtGui import *
import subprocess import subprocess
import time from . import utils
import yaml
from .thread_monitor import *
path_re = re.compile(r"[a-zA-Z0-9/:.,_+=() -]*") path_re = re.compile(r"[a-zA-Z0-9/:.,_+=() -]*")
path_max_len = 512 path_max_len = 512
# TODO: replace it with a more dynamic approach: allowing user to turn on or off
# profile saving
backup_profile_path = '/etc/qubes/backup/qubes-manager-backup.conf'
def fill_appvms_list(dialog): def fill_appvms_list(dialog):
dialog.appvm_combobox.clear() dialog.appvm_combobox.clear()
dialog.appvm_combobox.addItem("dom0") dialog.appvm_combobox.addItem("dom0")
dialog.appvm_combobox.setCurrentIndex(0) # current selected is null "" dialog.appvm_combobox.setCurrentIndex(0) # current selected is null ""
for vm in dialog.qvm_collection.values(): for vm in dialog.qvm_collection.domains:
if vm.is_appvm() and vm.internal: if vm.klass == 'AppVM' and vm.features.get('internal', False):
continue continue
if vm.is_template() and vm.installed_by_rpm: if vm.klass == 'TemplateVM' and vm.installed_by_rpm:
continue continue
if vm.is_running() and vm.qid != 0: # TODO: is the is_running criterion really necessary? It's designed to
# avoid backuping a VM into itself or surprising the user with starting
# a VM when they didn't plan to.
# TODO: remove debug
debug = True
if (debug or vm.is_running()) and vm.qid != 0:
dialog.appvm_combobox.addItem(vm.name) dialog.appvm_combobox.addItem(vm.name)
def enable_dir_line_edit(dialog, boolean): def enable_dir_line_edit(dialog, boolean):
dialog.dir_line_edit.setEnabled(boolean) dialog.dir_line_edit.setEnabled(boolean)
dialog.select_path_button.setEnabled(boolean) dialog.select_path_button.setEnabled(boolean)
def get_path_for_vm(vm, service_name):
if not vm:
return None
proc = vm.run("QUBESRPC %s dom0" % service_name, passio_popen=True)
proc.stdin.close()
untrusted_path = proc.stdout.readline(path_max_len)
if len(untrusted_path) == 0:
return None
if path_re.match(untrusted_path):
assert '../' not in untrusted_path
assert '\0' not in untrusted_path
return untrusted_path.strip()
else:
return None
def select_path_button_clicked(dialog, select_file=False): def select_path_button_clicked(dialog, select_file=False):
backup_location = str(dialog.dir_line_edit.text()) backup_location = str(dialog.dir_line_edit.text())
file_dialog = QFileDialog() file_dialog = QFileDialog()
file_dialog.setReadOnly(True) file_dialog.setReadOnly(True)
if select_file:
file_dialog_function = file_dialog.getOpenFileName
else:
file_dialog_function = file_dialog.getExistingDirectory
new_appvm = None
new_path = None new_path = None
if dialog.appvm_combobox.currentIndex() != 0: #An existing appvm chosen # TODO: check if dom0 is available
new_appvm = str(dialog.appvm_combobox.currentText()) new_appvm = str(dialog.appvm_combobox.currentText())
vm = dialog.qvm_collection.get_vm_by_name(new_appvm) vm = dialog.qvm_collection.domains[new_appvm]
if vm: try:
new_path = get_path_for_vm(vm, "qubes.SelectFile" if select_file new_path = utils.get_path_from_vm(
vm,
"qubes.SelectFile" if select_file
else "qubes.SelectDirectory") else "qubes.SelectDirectory")
else: except subprocess.CalledProcessError as ex:
new_path = file_dialog_function(dialog, QMessageBox.warning(
dialog.tr("Select backup location."), None,
backup_location if backup_location else '/') dialog.tr("Nothing selected!"),
dialog.tr("No file or directory selected."))
if new_path != None: # TODO: check if this works for restore
if os.path.basename(new_path) == 'qubes.xml': if new_path:
backup_location = os.path.dirname(new_path) dialog.dir_line_edit.setText(new_path)
else:
backup_location = new_path
dialog.dir_line_edit.setText(backup_location)
if (new_path or new_appvm) and len(backup_location) > 0: if new_path and len(backup_location) > 0:
dialog.select_dir_page.emit(SIGNAL("completeChanged()")) dialog.select_dir_page.emit(SIGNAL("completeChanged()"))
def simulate_long_lasting_proces(period, progress_callback):
for i in range(period):
progress_callback((i*100)/period)
time.sleep(1)
progress_callback(100) def load_backup_profile():
return 0 with open(backup_profile_path) as profile_file:
profile_data = yaml.safe_load(profile_file)
return profile_data
def write_backup_profile(args):
'''Format limited backup profile (for GUI purposes and print it to
*output_stream* (a file or stdout)
:param output_stream: file-like object ro print the profile to
:param args: dictionary with arguments
:param passphrase: passphrase to use
'''
acceptable_fields = ['include', 'passphrase_text', 'compression',
'destination_vm', 'destination_path']
profile_data = {key: value for key, value in args.items()
if key in acceptable_fields}
# TODO add compression parameter to GUI issue#943
with open(backup_profile_path, 'w') as profile_file:
yaml.safe_dump(profile_data, profile_file)

View File

@ -26,23 +26,15 @@ import os
import shutil import shutil
from PyQt4.QtCore import * from PyQt4.QtCore import *
from PyQt4.QtGui import * from PyQt4.QtGui import *
from .thread_monitor import *
import time
import os.path
import traceback
from qubes.qubes import QubesVmCollection
from qubes.qubes import QubesException
from qubes.qubes import QubesDaemonPidfile
from qubes.qubes import QubesHost
from qubes.qubes import qubes_base_dir
import qubesmanager.resources_rc import qubesmanager.resources_rc
import signal import signal
from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent
import time
from operator import itemgetter
from .thread_monitor import *
from qubes import backup from qubes import backup
from qubes import qubesutils
from .ui_restoredlg import * from .ui_restoredlg import *
from .multiselectwidget import * from .multiselectwidget import *
@ -50,17 +42,20 @@ from .multiselectwidget import *
from .backup_utils import * from .backup_utils import *
from multiprocessing import Queue, Event from multiprocessing import Queue, Event
from multiprocessing.queues import Empty from multiprocessing.queues import Empty
from qubesadmin import Qubes, events, exc
from qubesadmin import utils as admin_utils
from qubesadmin.backup import restore
class RestoreVMsWindow(Ui_Restore, QWizard): class RestoreVMsWindow(Ui_Restore, QWizard):
__pyqtSignals__ = ("restore_progress(int)", "backup_progress(int)") __pyqtSignals__ = ("restore_progress(int)", "backup_progress(int)")
def __init__(self, app, qvm_collection, blk_manager, parent=None): def __init__(self, app, qvm_collection, parent=None):
super(RestoreVMsWindow, self).__init__(parent) super(RestoreVMsWindow, self).__init__(parent)
self.app = app self.app = app
self.qvm_collection = qvm_collection self.qvm_collection = qvm_collection
self.blk_manager = blk_manager
self.restore_options = None self.restore_options = None
self.vms_to_restore = None self.vms_to_restore = None
@ -72,19 +67,21 @@ class RestoreVMsWindow(Ui_Restore, QWizard):
self.excluded = {} self.excluded = {}
self.vm = self.qvm_collection[0]
assert self.vm != None
self.setupUi(self) self.setupUi(self)
self.select_vms_widget = MultiSelectWidget(self) self.select_vms_widget = MultiSelectWidget(self)
self.select_vms_layout.insertWidget(1, self.select_vms_widget) self.select_vms_layout.insertWidget(1, self.select_vms_widget)
self.connect(self, SIGNAL("currentIdChanged(int)"), self.current_page_changed) self.connect(self,
self.connect(self, SIGNAL("restore_progress(QString)"), self.commit_text_edit.append) SIGNAL("currentIdChanged(int)"), self.current_page_changed)
self.connect(self, SIGNAL("backup_progress(int)"), self.progress_bar.setValue) self.connect(self,
self.dir_line_edit.connect(self.dir_line_edit, SIGNAL("textChanged(QString)"), self.backup_location_changed) SIGNAL("restore_progress(QString)"),
self.commit_text_edit.append)
self.connect(self,
SIGNAL("backup_progress(int)"), self.progress_bar.setValue)
self.dir_line_edit.connect(self.dir_line_edit,
SIGNAL("textChanged(QString)"),
self.backup_location_changed)
self.connect(self.verify_only, SIGNAL("stateChanged(int)"), self.connect(self.verify_only, SIGNAL("stateChanged(int)"),
self.on_verify_only_toogled) self.on_verify_only_toogled)
@ -93,10 +90,13 @@ class RestoreVMsWindow(Ui_Restore, QWizard):
self.confirm_page.isComplete = self.all_vms_good self.confirm_page.isComplete = self.all_vms_good
# FIXME # FIXME
# this causes to run isComplete() twice, I don't know why # this causes to run isComplete() twice, I don't know why
self.select_vms_page.connect(self.select_vms_widget, SIGNAL("selected_changed()"), SIGNAL("completeChanged()")) self.select_vms_page.connect(
self.select_vms_widget,
SIGNAL("selected_changed()"),
SIGNAL("completeChanged()"))
fill_appvms_list(self) fill_appvms_list(self)
self.__init_restore_options__() # self.__init_restore_options__()
@pyqtSlot(name='on_select_path_button_clicked') @pyqtSlot(name='on_select_path_button_clicked')
def select_path_button_clicked(self): def select_path_button_clicked(self):
@ -125,42 +125,41 @@ class RestoreVMsWindow(Ui_Restore, QWizard):
self.select_vms_widget.selected_list.clear() self.select_vms_widget.selected_list.clear()
self.select_vms_widget.available_list.clear() self.select_vms_widget.available_list.clear()
self.target_appvm = None self.target_appvm = None # TODO: what is the purpose of this
if self.appvm_combobox.currentIndex() != 0: # An existing appvm chosen if self.appvm_combobox.currentIndex() != 0: # An existing appvm chosen
self.target_appvm = self.qvm_collection.get_vm_by_name( self.target_appvm = self.qvm_collection.domains[
str(self.appvm_combobox.currentText())) str(self.appvm_combobox.currentText())]
try: try:
self.vms_to_restore = backup.backup_restore_prepare( self.backup_restore = restore.BackupRestore(
self.qvm_collection,
self.dir_line_edit.text(), self.dir_line_edit.text(),
self.passphrase_line_edit.text(), self.target_appvm,
options=self.restore_options, self.passphrase_line_edit.text()
host_collection=self.qvm_collection, )
encrypted=self.encryption_checkbox.isChecked(),
appvm=self.target_appvm) # TODO: change text of ignore missing to ignore
# missing templates and netvms
if self.ignore_missing.isChecked():
self.backup_restore.options.use_default_template = True
self.backup_restore.options.use_default_netvm = True
if self.ignore_uname_mismatch.isChecked():
self.backup_restore.options.ignore_username_mismatch = True
if self.verify_only.isChecked():
self.backup_restore.options.verify_only = True
self.vms_to_restore = self.backup_restore.get_restore_info()
for vmname in self.vms_to_restore: for vmname in self.vms_to_restore:
if vmname.startswith('$'): if vmname.startswith('$'):
# Internal info # Internal info
continue continue
self.select_vms_widget.available_list.addItem(vmname) self.select_vms_widget.available_list.addItem(vmname)
except QubesException as ex: except exc.QubesException as ex:
QMessageBox.warning(None, self.tr("Restore error!"), str(ex)) QMessageBox.warning(None, self.tr("Restore error!"), str(ex))
def __init_restore_options__(self):
if not self.restore_options:
self.restore_options = {}
backup.backup_restore_set_defaults(self.restore_options)
if 'use-default-template' in self.restore_options and 'use-default-netvm' in self.restore_options:
val = self.restore_options['use-default-template'] and self.restore_options['use-default-netvm']
self.ignore_missing.setChecked(val)
else:
self.ignore_missing.setChecked(False)
if 'ignore-username-mismatch' in self.restore_options:
self.ignore_uname_mismatch.setChecked(self.restore_options['ignore-username-mismatch'])
def gather_output(self, s): def gather_output(self, s):
self.func_output.append(s) self.func_output.append(s)
@ -178,25 +177,20 @@ class RestoreVMsWindow(Ui_Restore, QWizard):
def __do_restore__(self, thread_monitor): def __do_restore__(self, thread_monitor):
err_msg = [] err_msg = []
self.qvm_collection.lock_db_for_writing()
try: try:
backup.backup_restore_do(self.vms_to_restore, self.backup_restore.progress_callback = self.update_progress_bar
self.qvm_collection, self.backup_restore.restore_do(self.vms_to_restore)
print_callback=self.restore_output,
error_callback=self.restore_error_output,
progress_callback=self.update_progress_bar)
except backup.BackupCanceledError as ex: except backup.BackupCanceledError as ex:
self.canceled = True self.canceled = True
self.tmpdir_to_remove = ex.tmpdir self.tmpdir_to_remove = ex.tmpdir
err_msg.append(str(ex)) err_msg.append(str(ex))
except Exception as ex: except Exception as ex:
print ("Exception:", ex)
err_msg.append(str(ex)) err_msg.append(str(ex))
err_msg.append( err_msg.append(
self.tr("Partially restored files left in " self.tr("Partially restored files left in /var/tmp/restore_*, "
"/var/tmp/restore_*, investigate them and/or clean them up")) "investigate them and/or clean them up"))
self.qvm_collection.unlock_db()
if self.canceled: if self.canceled:
self.emit(SIGNAL("restore_progress(QString)"), self.emit(SIGNAL("restore_progress(QString)"),
'<b><font color="red">{0}</font></b>' '<b><font color="red">{0}</font></b>'
@ -230,13 +224,15 @@ class RestoreVMsWindow(Ui_Restore, QWizard):
del self.vms_to_restore[str(vmname)] del self.vms_to_restore[str(vmname)]
del self.func_output[:] del self.func_output[:]
self.vms_to_restore = backup.restore_info_verify(self.vms_to_restore, # TODO: am I ignoring changes made by user?
self.qvm_collection) self.vms_to_restore = self.backup_restore.restore_info_verify(
backup.backup_restore_print_summary( self.backup_restore.get_restore_info())
self.vms_to_restore, print_callback = self.gather_output) self.func_output = self.backup_restore.get_restore_summary(
self.backup_restore.get_restore_info()
)
self.confirm_text_edit.setReadOnly(True) self.confirm_text_edit.setReadOnly(True)
self.confirm_text_edit.setFontFamily("Monospace") self.confirm_text_edit.setFontFamily("Monospace")
self.confirm_text_edit.setText("\n".join(self.func_output)) self.confirm_text_edit.setText(self.func_output)
self.confirm_page.emit(SIGNAL("completeChanged()")) self.confirm_page.emit(SIGNAL("completeChanged()"))
@ -275,13 +271,13 @@ class RestoreVMsWindow(Ui_Restore, QWizard):
self.tr("Backup error!"), self.tr("ERROR: {0}") self.tr("Backup error!"), self.tr("ERROR: {0}")
.format(self.thread_monitor.error_msg)) .format(self.thread_monitor.error_msg))
if self.showFileDialog.isChecked(): if self.showFileDialog.isChecked(): # TODO: this is not working
self.emit(SIGNAL("restore_progress(QString)"), self.emit(SIGNAL("restore_progress(QString)"),
'<b><font color="black">{0}</font></b>'.format( '<b><font color="black">{0}</font></b>'.format(
self.tr( self.tr(
"Please unmount your backup volume and cancel" "Please unmount your backup volume and cancel"
" the file selection dialog."))) " the file selection dialog.")))
if self.target_appvm: if self.target_appvm: # TODO does this work at all?
self.target_appvm.run("QUBESRPC %s dom0" % self.target_appvm.run("QUBESRPC %s dom0" %
"qubes.SelectDirectory") "qubes.SelectDirectory")
else: else:
@ -298,16 +294,16 @@ class RestoreVMsWindow(Ui_Restore, QWizard):
signal.signal(signal.SIGCHLD, old_sigchld_handler) signal.signal(signal.SIGCHLD, old_sigchld_handler)
def all_vms_good(self): def all_vms_good(self):
for vminfo in self.vms_to_restore.values(): for vm_info in self.vms_to_restore.values():
if not vminfo.has_key('vm'): if not vm_info.vm:
continue continue
if not vminfo['good-to-go']: if not vm_info.good_to_go:
return False return False
return True return True
def reject(self): def reject(self): # TODO: probably not working too
if self.currentPage() is self.commit_page: if self.currentPage() is self.commit_page:
if backup.backup_cancel(): if self.backup_restore.canceled:
self.emit(SIGNAL("restore_progress(QString)"), self.emit(SIGNAL("restore_progress(QString)"),
'<font color="red">{0}</font>' '<font color="red">{0}</font>'
.format(self.tr("Aborting the operation..."))) .format(self.tr("Aborting the operation...")))
@ -339,50 +335,37 @@ class RestoreVMsWindow(Ui_Restore, QWizard):
# Copyright (c) 2002-2007 Pascal Varet <p.varet@gmail.com> # Copyright (c) 2002-2007 Pascal Varet <p.varet@gmail.com>
def handle_exception(exc_type, exc_value, exc_traceback): def handle_exception(exc_type, exc_value, exc_traceback):
import sys
import os.path
import traceback
filename, line, dummy, dummy = traceback.extract_tb(exc_traceback).pop() filename, line, dummy, dummy = traceback.extract_tb(exc_traceback).pop()
filename = os.path.basename(filename) filename = os.path.basename(filename)
error = "%s: %s" % (exc_type.__name__, exc_value) error = "%s: %s" % (exc_type.__name__, exc_value)
QMessageBox.critical(None, "Houston, we have a problem...", QMessageBox.critical(None, "Houston, we have a problem...",
"Whoops. A critical error has occured. This is most likely a bug " "Whoops. A critical error has occured. "
"This is most likely a bug "
"in Qubes Restore VMs application.<br><br>" "in Qubes Restore VMs application.<br><br>"
"<b><i>%s</i></b>" % error + "<b><i>%s</i></b>" % error +
"at <b>line %d</b> of file <b>%s</b>.<br/><br/>" "at <b>line %d</b> of file <b>%s</b>.<br/><br/>"
% (line, filename)) % (line, filename))
def main(): def main():
global qubes_host qtapp = QApplication(sys.argv)
qubes_host = QubesHost() qtapp.setOrganizationName("The Qubes Project")
qtapp.setOrganizationDomain("http://qubes-os.org")
global app qtapp.setApplicationName("Qubes Restore VMs")
app = QApplication(sys.argv)
app.setOrganizationName("The Qubes Project")
app.setOrganizationDomain("http://qubes-os.org")
app.setApplicationName("Qubes Restore VMs")
sys.excepthook = handle_exception sys.excepthook = handle_exception
qvm_collection = QubesVmCollection() app = Qubes()
qvm_collection.lock_db_for_reading()
qvm_collection.load()
qvm_collection.unlock_db()
global restore_window restore_window = RestoreVMsWindow(qtapp, app)
restore_window = RestoreVMsWindow()
restore_window.show() restore_window.show()
app.exec_() qtapp.exec_()
app.exit() qtapp.exit()
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -60,6 +60,8 @@ rm -rf $RPM_BUILD_ROOT
/usr/bin/qubes-vm-settings /usr/bin/qubes-vm-settings
/usr/bin/qubes-vm-create /usr/bin/qubes-vm-create
/usr/bin/qubes-vm-boot-from-device /usr/bin/qubes-vm-boot-from-device
/usr/bin/qubes-backup
/usr/bin/qubes-backup-restore
/usr/libexec/qubes-manager/mount_for_backup.sh /usr/libexec/qubes-manager/mount_for_backup.sh
/usr/libexec/qubes-manager/qvm_about.sh /usr/libexec/qubes-manager/qvm_about.sh

View File

@ -21,6 +21,8 @@ if __name__ == '__main__':
'qubes-global-settings = qubesmanager.global_settings:main', 'qubes-global-settings = qubesmanager.global_settings:main',
'qubes-vm-settings = qubesmanager.settings:main', 'qubes-vm-settings = qubesmanager.settings:main',
'qubes-vm-create = qubesmanager.create_new_vm:main', 'qubes-vm-create = qubesmanager.create_new_vm:main',
'qubes-vm-boot-from-device = qubesmanager.bootfromdevice:main' 'qubes-vm-boot-from-device = qubesmanager.bootfromdevice:main',
'qubes-backup = qubesmanager.backup:main',
'qubes-backup-restore = qubesmanager.restore:main'
], ],
}) })

View File

@ -23,73 +23,8 @@
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="verticalLayout">
<item> <item>
<layout class="QGridLayout" name="gridLayout_2"> <layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="1">
<widget class="QPushButton" name="shutdown_running_vms_button">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Shutdown all running selected VMs</string>
</property>
<property name="icon">
<iconset resource="../resources.qrc">
<normaloff>:/shutdownvm.png</normaloff>:/shutdownvm.png</iconset>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="refresh_button">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Refresh running states.</string>
</property>
</widget>
</item>
<item row="1" column="0" rowspan="2">
<widget class="QLabel" name="running_vms_warning">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<weight>75</weight>
<italic>true</italic>
<bold>true</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">color:rgb(255, 0, 0)</string>
</property>
<property name="text">
<string>Some of the selected VMs are running (red). Running VMs cannot be backed up!</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0"> <item row="0" column="0">
<widget class="QLabel" name="label_4"> <widget class="QLabel" name="label_4">
<property name="font">
<font>
<pointsize>9</pointsize>
<weight>50</weight>
<italic>false</italic>
<bold>false</bold>
<underline>false</underline>
</font>
</property>
<property name="text"> <property name="text">
<string>Select VMs to backup:</string> <string>Select VMs to backup:</string>
</property> </property>
@ -145,6 +80,57 @@
</item> </item>
</layout> </layout>
</item> </item>
<item>
<widget class="QLabel" name="unrecognized_config_label">
<property name="palette">
<palette>
<active>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
</active>
<inactive>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
</inactive>
<disabled>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>139</red>
<green>142</green>
<blue>142</blue>
</color>
</brush>
</colorrole>
</disabled>
</palette>
</property>
<property name="font">
<font>
<weight>75</weight>
<italic>true</italic>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Warning: unrecognized data found in configuration files. </string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<widget class="QWizardPage" name="select_dir_page"> <widget class="QWizardPage" name="select_dir_page">
@ -281,8 +267,8 @@
<string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt; <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt; &lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; } p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;&quot;&gt; &lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Cantarell'; font-size:11pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> &lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Sans Serif'; font-size:9pt;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property> </property>
</widget> </widget>
</item> </item>