Browse Source

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
Marta Marczykowska-Górecka 6 years ago
parent
commit
7a4e4b35d5
6 changed files with 397 additions and 491 deletions
  1. 187 261
      qubesmanager/backup.py
  2. 64 57
      qubesmanager/backup_utils.py
  3. 88 105
      qubesmanager/restore.py
  4. 2 0
      rpm_spec/qmgr.spec
  5. 3 1
      setup.py
  6. 53 67
      ui/backupdlg.ui

+ 187 - 261
qubesmanager/backup.py

@@ -1,4 +1,4 @@
-#!/usr/bin/python2
+#!/usr/bin/python3
 # pylint: skip-file
 #
 # The Qubes OS Project, http://www.qubes-os.org
@@ -21,83 +21,73 @@
 #
 #
 
-import sys
-import os
+import traceback
+
 import signal
 import shutil
-from PyQt4.QtCore import *
-from PyQt4.QtGui import *
-
-from qubes.qubes import QubesVmCollection
-from qubes.qubes import QubesException
-from qubes.qubes import QubesDaemonPidfile
-from qubes.qubes import QubesHost
-from qubes import backup
-from qubes import qubesutils
 
-import qubesmanager.resources_rc
-
-from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent
-
-import time
-from .thread_monitor import *
-from operator import itemgetter
+from qubesadmin import Qubes, events, exc
+from qubesadmin import utils as admin_utils
+from qubes.storage.file import get_disk_usage
 
-from datetime import datetime
-from string import replace
+from PyQt4 import QtCore, QtGui  # pylint: disable=import-error
 
 from .ui_backupdlg import *
 from .multiselectwidget import *
 
 from .backup_utils import *
-import main
-import grp,pwd
-
+from . import utils
+import grp
+import pwd
+import sys
+import os
+from .thread_monitor import *
+import time
 
 class BackupVMsWindow(Ui_Backup, QWizard):
 
     __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)
 
         self.app = app
         self.qvm_collection = qvm_collection
-        self.blk_manager = blk_manager
-        self.shutdown_vm_func = shutdown_vm_func
+        self.backup_settings = QtCore.QSettings()
 
         self.func_output = []
         self.selected_vms = []
         self.tmpdir_to_remove = None
         self.canceled = False
 
-        self.vm = self.qvm_collection[0]
         self.files_to_backup = None
 
-        assert self.vm != None
-
         self.setupUi(self)
 
         self.progress_status.text = self.tr("Backup in progress...")
-        self.show_running_vms_warning(False)
         self.dir_line_edit.setReadOnly(False)
 
         self.select_vms_widget = MultiSelectWidget(self)
         self.verticalLayout.insertWidget(1, self.select_vms_widget)
 
-        self.connect(self, SIGNAL("currentIdChanged(int)"), self.current_page_changed)
-        self.connect(self.select_vms_widget, SIGNAL("selected_changed()"), self.check_running)
-        self.connect(self.select_vms_widget, SIGNAL("items_removed(PyQt_PyObject)"), self.vms_removed)
-        self.connect(self.select_vms_widget, SIGNAL("items_added(PyQt_PyObject)"), self.vms_added)
-        self.refresh_button.clicked.connect(self.check_running)
-        self.shutdown_running_vms_button.clicked.connect(self.shutdown_all_running_selected)
-        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, SIGNAL("currentIdChanged(int)"),
+                     self.current_page_changed)
+        self.connect(self.select_vms_widget,
+                     SIGNAL("items_removed(PyQt_PyObject)"),
+                     self.vms_removed)
+        self.connect(self.select_vms_widget,
+                     SIGNAL("items_added(PyQt_PyObject)"),
+                     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_dir_page.isComplete = self.has_selected_dir_and_pass
-        #FIXME
-        #this causes to run isComplete() twice, I don't know why
+        # FIXME
+        # this causes to run isComplete() twice, I don't know why
         self.select_vms_page.connect(
                 self.select_vms_widget,
                 SIGNAL("selected_changed()"),
@@ -112,40 +102,62 @@ class BackupVMsWindow(Ui_Backup, QWizard):
                 self.backup_location_changed)
 
         self.total_size = 0
-        self.__fill_vms_list__()
 
-        fill_appvms_list(self)
-        self.load_settings()
+        # 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: 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):
-        dest_vm_name = main.manager_window.manager_settings.value(
-            'backup/vmname', defaultValue="")
-        dest_vm_idx = self.appvm_combobox.findText(dest_vm_name.toString())
-        if dest_vm_idx > -1:
-            self.appvm_combobox.setCurrentIndex(dest_vm_idx)
-
-        if main.manager_window.manager_settings.contains('backup/path'):
-            dest_path = main.manager_window.manager_settings.value(
-                'backup/path', defaultValue=None)
-            self.dir_line_edit.setText(dest_path.toString())
-
-        if main.manager_window.manager_settings.contains('backup/encrypt'):
-            encrypt = main.manager_window.manager_settings.value(
-                'backup/encrypt', defaultValue=None)
-            self.encryption_checkbox.setChecked(encrypt.toBool())
+        try:
+            profile_data = load_backup_profile()
+        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:
+                self.appvm_combobox.setCurrentIndex(dest_vm_idx)
+
+        if 'destination_path' in profile_data:
+            dest_path = profile_data['destination_path']
+            self.dir_line_edit.setText(dest_path)
+
+        if 'passphrase_text' in profile_data:
+            self.passphrase_line_edit.setText(profile_data['passphrase_text'])
+            self.passphrase_line_edit_verify.setText(
+                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):
-        main.manager_window.manager_settings.setValue(
-            'backup/vmname', self.appvm_combobox.currentText())
-        main.manager_window.manager_settings.setValue(
-            'backup/path', self.dir_line_edit.text())
-        main.manager_window.manager_settings.setValue(
-            'backup/encrypt', self.encryption_checkbox.isChecked())
-
-    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)
+        settings = {'destination_vm': self.appvm_combobox.currentText(),
+                    'destination_path': self.dir_line_edit.text(),
+                    'include': [vm.name for vm in self.selected_vms],
+                    'passphrase_text': self.passphrase_line_edit.text()}
+        # TODO: add compression when it is added
+        write_backup_profile(settings)
 
     class VmListItem(QListWidgetItem):
         def __init__(self, vm):
@@ -153,114 +165,45 @@ class BackupVMsWindow(Ui_Backup, QWizard):
             if vm.qid == 0:
                 local_user = grp.getgrnam('qubes').gr_mem[0]
                 home_dir = pwd.getpwnam(local_user).pw_dir
-                self.size = qubesutils.get_disk_usage(home_dir)
+                self.size = get_disk_usage(home_dir)
             else:
-                self.size = self.get_vm_size(vm)
-            super(BackupVMsWindow.VmListItem, self).__init__(vm.name+ " (" + qubesutils.size_to_human(self.size) + ")")
-
-        def get_vm_size(self, vm):
-            size = 0
-            if vm.private_img is not None:
-                size += qubesutils.get_disk_usage (vm.private_img)
-
-            if vm.updateable:
-                size += qubesutils.get_disk_usage(vm.root_img)
+                self.size = vm.get_disk_utilization()
+            super(BackupVMsWindow.VmListItem, self).__init__(
+                vm.name + " (" + admin_utils.size_to_human(self.size) + ")")
 
-            return size
-
-
-    def __fill_vms_list__(self):
-        for vm in self.qvm_collection.values():
-            if vm.internal:
+    def __fill_vms_list__(self, selected=None):
+        for vm in self.qvm_collection.domains:
+            if vm.features.get('internal', False):
                 continue
 
             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.total_size += item.size
             else:
                 self.select_vms_widget.available_list.addItem(item)
         self.select_vms_widget.available_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):
         for i in items:
             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):
         for i in items:
             self.total_size -= i.size
-        self.total_size_label.setText(qubesutils.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)
+        self.total_size_label.setText(
+            admin_utils.size_to_human(self.total_size))
 
     @pyqtSlot(name='on_select_path_button_clicked')
     def select_path_button_clicked(self):
@@ -268,44 +211,43 @@ class BackupVMsWindow(Ui_Backup, QWizard):
 
     def validateCurrentPage(self):
         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 = []
             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:
             backup_location = str(self.dir_line_edit.text())
             if not backup_location:
-                QMessageBox.information(None, self.tr("Wait!"),
+                QMessageBox.information(
+                    None, self.tr("Wait!"),
                     self.tr("Enter backup target location first."))
                 return False
-            if self.appvm_combobox.currentIndex() == 0 and \
-                   not os.path.isdir(backup_location):
-                QMessageBox.information(None, self.tr("Wait!"),
+            if self.appvm_combobox.currentIndex() == 0 \
+                    and not os.path.isdir(backup_location):
+                QMessageBox.information(
+                    None, self.tr("Wait!"),
                     self.tr("Selected directory do not exists or "
                             "not a directory (%s).") % backup_location)
                 return False
             if not len(self.passphrase_line_edit.text()):
-                QMessageBox.information(None, self.tr("Wait!"),
-                    self.tr("Enter passphrase for backup encryption/verification first."))
+                QMessageBox.information(
+                    None, self.tr("Wait!"),
+                    self.tr("Enter passphrase for backup "
+                            "encryption/verification first."))
                 return False
-            if self.passphrase_line_edit.text() != self.passphrase_line_edit_verify.text():
-                QMessageBox.information(None,
-                    self.tr("Wait!"),
+            if self.passphrase_line_edit.text() !=\
+                    self.passphrase_line_edit_verify.text():
+                QMessageBox.information(
+                    None, self.tr("Wait!"),
                     self.tr("Enter the same passphrase in both fields."))
                 return False
 
         return True
 
-    def gather_output(self, s):
-        self.func_output.append(s)
+#    def gather_output(self, s):
+#        self.func_output.append(s)
 
     def update_progress_bar(self, value):
         self.emit(SIGNAL("backup_progress(int)"), value)
@@ -314,23 +256,27 @@ class BackupVMsWindow(Ui_Backup, QWizard):
         msg = []
 
         try:
-            backup.backup_do(self.dir_line_edit.text(),
-                    self.files_to_backup,
-                    self.passphrase_line_edit.text(),
-                    progress_callback=self.update_progress_bar,
-                    encrypted=self.encryption_checkbox.isChecked(),
-                    appvm=self.target_appvm)
-            #simulate_long_lasting_proces(10, self.update_progress_bar)
-        except backup.BackupCanceledError as ex:
-            msg.append(str(ex))
-            self.canceled = True
-            if ex.tmpdir:
-                self.tmpdir_to_remove = ex.tmpdir
-        except Exception as ex:
+            # TODO: this does nothing, events are not handled
+            events_dispatcher = events.EventsDispatcher(self.app)
+            events_dispatcher.add_handler('backup-progress',
+                                          self.update_progress_bar)
+            try:
+                vm = self.qvm_collection.domains[
+                    self.appvm_combobox.currentText()]
+                if not vm.is_running():
+                    vm.start()
+                self.qvm_collection.qubesd_call('dom0',
+                                                'admin.backup.Execute',
+                                                'qubes-manager-backup')
+            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)
             msg.append(str(ex))
 
-        if len(msg) > 0 :
+        if len(msg) > 0:
             thread_monitor.set_error_msg('\n'.join(msg))
 
         thread_monitor.set_finished()
@@ -340,27 +286,13 @@ class BackupVMsWindow(Ui_Backup, QWizard):
         old_sigchld_handler = signal.signal(signal.SIGCHLD, signal.SIG_DFL)
         if self.currentPage() is self.confirm_page:
 
-            self.target_appvm = None
-            if self.appvm_combobox.currentIndex() != 0:   #An existing appvm chosen
-                self.target_appvm = self.qvm_collection.get_vm_by_name(
-                        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.save_settings()
+            backup_summary = self.qvm_collection.qubesd_call(
+                'dom0', 'admin.backup.Info', 'qubes-manager-backup')
 
             self.textEdit.setReadOnly(True)
             self.textEdit.setFontFamily("Monospace")
-            self.textEdit.setText("\n".join(self.func_output))
-            self.save_settings()
+            self.textEdit.setText(backup_summary.decode())
 
         elif self.currentPage() is self.commit_page:
             self.button(self.FinishButton).setDisabled(True)
@@ -370,29 +302,33 @@ class BackupVMsWindow(Ui_Backup, QWizard):
                                            and str(self.dir_line_edit.text())
                                            .count("media/") > 0)
             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.start()
 
-            counter = 0
             while not self.thread_monitor.is_finished():
                 self.app.processEvents()
-                time.sleep (0.1)
+                time.sleep(0.1)
 
             if not self.thread_monitor.success:
                 if self.canceled:
                     self.progress_status.setText(self.tr("Backup aborted."))
                     if self.tmpdir_to_remove:
-                        if QMessageBox.warning(None, self.tr("Backup aborted"),
-                                self.tr("Do you want to remove temporary files from "
-                                        "%s?") % self.tmpdir_to_remove,
-                                QMessageBox.Yes, QMessageBox.No) == QMessageBox.Yes:
+                        if QMessageBox.warning(
+                                None, self.tr("Backup aborted"),
+                                self.tr(
+                                    "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)
                 else:
                     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.thread_monitor.error_msg))
+                            self.thread_monitor.error_msg))
             else:
                 self.progress_bar.setValue(100)
                 self.progress_status.setText(self.tr("Backup finished."))
@@ -402,20 +338,21 @@ class BackupVMsWindow(Ui_Backup, QWizard):
                     orig_text + self.tr(
                         " Please unmount your backup volume and cancel "
                         "the file selection dialog."))
-                if self.target_appvm:
-                    self.target_appvm.run("QUBESRPC %s dom0" % "qubes"
-                                                               ".SelectDirectory")
+                if self.target_appvm:  # FIXME I'm not sure if this works
+                    self.target_appvm.run(
+                        "QUBESRPC %s dom0" % "qubes.SelectDirectory")
             self.button(self.CancelButton).setEnabled(False)
             self.button(self.FinishButton).setEnabled(True)
             self.showFileDialog.setEnabled(False)
         signal.signal(signal.SIGCHLD, old_sigchld_handler)
 
     def reject(self):
-        #cancell clicked while the backup is in progress.
-        #calling kill on tar.
+        # cancell clicked while the backup is in progress.
+        # calling kill on tar.
         if self.currentPage() is self.commit_page:
-            if backup.backup_cancel():
-                self.button(self.CancelButton).setDisabled(True)
+            pass  # TODO: this does nothing
+            # if backup.backup_cancel():
+            #     self.button(self.CancelButton).setDisabled(True)
         else:
             self.done(0)
 
@@ -425,61 +362,50 @@ class BackupVMsWindow(Ui_Backup, QWizard):
     def has_selected_dir_and_pass(self):
         if not len(self.passphrase_line_edit.text()):
             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 len(self.dir_line_edit.text()) > 0
 
-    def backup_location_changed(self, new_dir = None):
+    def backup_location_changed(self, new_dir=None):
         self.select_dir_page.emit(SIGNAL("completeChanged()"))
 
 
 # Bases on the original code by:
 # Copyright (c) 2002-2007 Pascal Varet <p.varet@gmail.com>
 
-def handle_exception(exc_type, exc_value, exc_traceback ):
-    import sys
-    import os.path
-    import traceback
+def handle_exception(exc_type, exc_value, exc_traceback):
+    filename, line, dummy, dummy = traceback.extract_tb(exc_traceback).pop()
+    filename = os.path.basename(filename)
+    error = "%s: %s" % (exc_type.__name__, exc_value)
 
-    filename, line, dummy, dummy = traceback.extract_tb( exc_traceback ).pop()
-    filename = os.path.basename( filename )
-    error    = "%s: %s" % ( exc_type.__name__, exc_value )
+    QtGui.QMessageBox.critical(
+        None,
+        "Houston, we have a problem...",
+        "Whoops. A critical error has occured. This is most likely a bug "
+        "in Qubes Global Settings application.<br><br><b><i>%s</i></b>" %
+        error + "at <b>line %d</b> of file <b>%s</b>.<br/><br/>"
+        % (line, filename))
 
-    QMessageBox.critical(None, "Houston, we have a problem...",
-                         "Whoops. A critical error has occured. This is most likely a bug "
-                         "in Qubes Restore VMs application.<br><br>"
-                         "<b><i>%s</i></b>" % error +
-                         "at <b>line %d</b> of file <b>%s</b>.<br/><br/>"
-                         % ( line, filename ))
 
+def main():
 
-def app_main():
-
-    global qubes_host
-    qubes_host = QubesHost()
-
-    global app
-    app = QApplication(sys.argv)
-    app.setOrganizationName("The Qubes Project")
-    app.setOrganizationDomain("http://qubes-os.org")
-    app.setApplicationName("Qubes Backup VMs")
+    qtapp = QtGui.QApplication(sys.argv)
+    qtapp.setOrganizationName("The Qubes Project")
+    qtapp.setOrganizationDomain("http://qubes-os.org")
+    qtapp.setApplicationName("Qubes Backup VMs")
 
     sys.excepthook = handle_exception
 
-    qvm_collection = QubesVmCollection()
-    qvm_collection.lock_db_for_reading()
-    qvm_collection.load()
-    qvm_collection.unlock_db()
+    app = Qubes()
 
-    global backup_window
-    backup_window = BackupVMsWindow()
+    backup_window = BackupVMsWindow(qtapp, app)
 
     backup_window.show()
 
-    app.exec_()
-    app.exit()
-
+    qtapp.exec_()
+    qtapp.exit()
 
 
 if __name__ == "__main__":
-    app_main()
+    main()

+ 64 - 57
qubesmanager/backup_utils.py

@@ -20,90 +20,97 @@
 #
 #
 import re
-import sys
-import os
 from PyQt4.QtCore import *
 from PyQt4.QtGui import *
 
 import subprocess
-import time
-
-from .thread_monitor import *
+from . import utils
+import yaml
 
 path_re = re.compile(r"[a-zA-Z0-9/:.,_+=() -]*")
 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):
     dialog.appvm_combobox.clear()
     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():
-        if vm.is_appvm() and vm.internal:
+    for vm in dialog.qvm_collection.domains:
+        if vm.klass == 'AppVM' and vm.features.get('internal', False):
             continue
-        if vm.is_template() and vm.installed_by_rpm:
+        if vm.klass == 'TemplateVM' and vm.installed_by_rpm:
             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)
 
+
 def enable_dir_line_edit(dialog, boolean):
     dialog.dir_line_edit.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())
     file_dialog = QFileDialog()
     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
-    if dialog.appvm_combobox.currentIndex() != 0:   #An existing appvm chosen
-        new_appvm = str(dialog.appvm_combobox.currentText())
-        vm = dialog.qvm_collection.get_vm_by_name(new_appvm)
-        if vm:
-            new_path = get_path_for_vm(vm, "qubes.SelectFile" if select_file
-                    else "qubes.SelectDirectory")
-    else:
-        new_path = file_dialog_function(dialog,
-            dialog.tr("Select backup location."),
-            backup_location if backup_location else '/')
-
-    if new_path != None:
-        if os.path.basename(new_path) == 'qubes.xml':
-            backup_location = os.path.dirname(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:
+    # TODO: check if dom0 is available
+
+    new_appvm = str(dialog.appvm_combobox.currentText())
+    vm = dialog.qvm_collection.domains[new_appvm]
+    try:
+        new_path = utils.get_path_from_vm(
+            vm,
+            "qubes.SelectFile" if select_file
+            else "qubes.SelectDirectory")
+    except subprocess.CalledProcessError as ex:
+        QMessageBox.warning(
+            None,
+            dialog.tr("Nothing selected!"),
+            dialog.tr("No file or directory selected."))
+
+    # TODO: check if this works for restore
+    if new_path:
+        dialog.dir_line_edit.setText(new_path)
+
+    if new_path and len(backup_location) > 0:
         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)
-    return 0
+def load_backup_profile():
+    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)

+ 88 - 105
qubesmanager/restore.py

@@ -26,23 +26,15 @@ import os
 import shutil
 from PyQt4.QtCore 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 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 qubesutils
 
 from .ui_restoredlg import *
 from .multiselectwidget import *
@@ -50,17 +42,20 @@ from .multiselectwidget import *
 from .backup_utils import *
 from multiprocessing import Queue, Event
 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):
 
-    __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)
 
         self.app = app
         self.qvm_collection = qvm_collection
-        self.blk_manager = blk_manager
 
         self.restore_options = None
         self.vms_to_restore = None
@@ -72,31 +67,36 @@ class RestoreVMsWindow(Ui_Restore, QWizard):
 
         self.excluded = {}
 
-        self.vm = self.qvm_collection[0]
-
-        assert self.vm != None
-
         self.setupUi(self)
 
         self.select_vms_widget = MultiSelectWidget(self)
         self.select_vms_layout.insertWidget(1, self.select_vms_widget)
 
-        self.connect(self, SIGNAL("currentIdChanged(int)"), self.current_page_changed)
-        self.connect(self, 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,
+                     SIGNAL("currentIdChanged(int)"), self.current_page_changed)
+        self.connect(self,
+                     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.on_verify_only_toogled)
 
         self.select_dir_page.isComplete = self.has_selected_dir
         self.select_vms_page.isComplete = self.has_selected_vms
         self.confirm_page.isComplete = self.all_vms_good
-        #FIXME
-        #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()"))
+        # FIXME
+        # 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()"))
 
         fill_appvms_list(self)
-        self.__init_restore_options__()
+#        self.__init_restore_options__()
 
     @pyqtSlot(name='on_select_path_button_clicked')
     def select_path_button_clicked(self):
@@ -125,41 +125,40 @@ class RestoreVMsWindow(Ui_Restore, QWizard):
         self.select_vms_widget.selected_list.clear()
         self.select_vms_widget.available_list.clear()
 
-        self.target_appvm = None
-        if self.appvm_combobox.currentIndex() != 0:   #An existing appvm chosen
-            self.target_appvm = self.qvm_collection.get_vm_by_name(
-                    str(self.appvm_combobox.currentText()))
+        self.target_appvm = None  # TODO: what is the purpose of this
+        if self.appvm_combobox.currentIndex() != 0:   # An existing appvm chosen
+            self.target_appvm = self.qvm_collection.domains[
+                str(self.appvm_combobox.currentText())]
 
         try:
-            self.vms_to_restore = backup.backup_restore_prepare(
-                    self.dir_line_edit.text(),
-                    self.passphrase_line_edit.text(),
-                    options=self.restore_options,
-                    host_collection=self.qvm_collection,
-                    encrypted=self.encryption_checkbox.isChecked(),
-                    appvm=self.target_appvm)
+            self.backup_restore = restore.BackupRestore(
+                self.qvm_collection,
+                self.dir_line_edit.text(),
+                self.target_appvm,
+                self.passphrase_line_edit.text()
+            )
+
+            # 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:
                 if vmname.startswith('$'):
                     # Internal info
                     continue
                 self.select_vms_widget.available_list.addItem(vmname)
-        except QubesException as 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'])
+        except exc.QubesException as ex:
+            QMessageBox.warning(None, self.tr("Restore error!"), str(ex))
 
     def gather_output(self, s):
         self.func_output.append(s)
@@ -178,25 +177,20 @@ class RestoreVMsWindow(Ui_Restore, QWizard):
 
     def __do_restore__(self, thread_monitor):
         err_msg = []
-        self.qvm_collection.lock_db_for_writing()
         try:
-            backup.backup_restore_do(self.vms_to_restore,
-                                     self.qvm_collection,
-                                     print_callback=self.restore_output,
-                                     error_callback=self.restore_error_output,
-                                     progress_callback=self.update_progress_bar)
+            self.backup_restore.progress_callback = self.update_progress_bar
+            self.backup_restore.restore_do(self.vms_to_restore)
+
         except backup.BackupCanceledError as ex:
             self.canceled = True
             self.tmpdir_to_remove = ex.tmpdir
             err_msg.append(str(ex))
         except Exception as ex:
-            print ("Exception:", ex)
             err_msg.append(str(ex))
             err_msg.append(
-                self.tr("Partially restored files left in "
-                   "/var/tmp/restore_*, investigate them and/or clean them up"))
+                self.tr("Partially restored files left in /var/tmp/restore_*, "
+                        "investigate them and/or clean them up"))
 
-        self.qvm_collection.unlock_db()
         if self.canceled:
             self.emit(SIGNAL("restore_progress(QString)"),
                       '<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.func_output[:]
-            self.vms_to_restore = backup.restore_info_verify(self.vms_to_restore,
-                                                             self.qvm_collection)
-            backup.backup_restore_print_summary(
-                    self.vms_to_restore, print_callback = self.gather_output)
+            # TODO: am I ignoring changes made by user?
+            self.vms_to_restore = self.backup_restore.restore_info_verify(
+                self.backup_restore.get_restore_info())
+            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.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()"))
 
@@ -275,13 +271,13 @@ class RestoreVMsWindow(Ui_Restore, QWizard):
                         self.tr("Backup error!"), self.tr("ERROR: {0}")
                                       .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)"),
                           '<b><font color="black">{0}</font></b>'.format(
                               self.tr(
                                   "Please unmount your backup volume and cancel"
                                   " 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" %
                                           "qubes.SelectDirectory")
                 else:
@@ -298,16 +294,16 @@ class RestoreVMsWindow(Ui_Restore, QWizard):
         signal.signal(signal.SIGCHLD, old_sigchld_handler)
 
     def all_vms_good(self):
-        for vminfo in self.vms_to_restore.values():
-            if not vminfo.has_key('vm'):
+        for vm_info in self.vms_to_restore.values():
+            if not vm_info.vm:
                 continue
-            if not vminfo['good-to-go']:
+            if not vm_info.good_to_go:
                 return False
         return True
 
-    def reject(self):
+    def reject(self):  # TODO: probably not working too
         if self.currentPage() is self.commit_page:
-            if backup.backup_cancel():
+            if self.backup_restore.canceled:
                 self.emit(SIGNAL("restore_progress(QString)"),
                           '<font color="red">{0}</font>'
                           .format(self.tr("Aborting the operation...")))
@@ -331,58 +327,45 @@ class RestoreVMsWindow(Ui_Restore, QWizard):
     def has_selected_vms(self):
         return self.select_vms_widget.selected_list.count() > 0
 
-    def backup_location_changed(self, new_dir = None):
+    def backup_location_changed(self, new_dir=None):
         self.select_dir_page.emit(SIGNAL("completeChanged()"))
 
 
 # Bases on the original code by:
 # Copyright (c) 2002-2007 Pascal Varet <p.varet@gmail.com>
 
-def handle_exception( exc_type, exc_value, exc_traceback ):
-    import sys
-    import os.path
-    import traceback
+def handle_exception(exc_type, exc_value, exc_traceback):
 
-    filename, line, dummy, dummy = traceback.extract_tb( exc_traceback ).pop()
-    filename = os.path.basename( filename )
-    error    = "%s: %s" % ( exc_type.__name__, exc_value )
+    filename, line, dummy, dummy = traceback.extract_tb(exc_traceback).pop()
+    filename = os.path.basename(filename)
+    error = "%s: %s" % (exc_type.__name__, exc_value)
 
     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>"
                          "<b><i>%s</i></b>" % error +
                          "at <b>line %d</b> of file <b>%s</b>.<br/><br/>"
-                         % ( line, filename ))
-
-
+                         % (line, filename))
 
 
 def main():
 
-    global qubes_host
-    qubes_host = QubesHost()
-
-    global app
-    app = QApplication(sys.argv)
-    app.setOrganizationName("The Qubes Project")
-    app.setOrganizationDomain("http://qubes-os.org")
-    app.setApplicationName("Qubes Restore VMs")
+    qtapp = QApplication(sys.argv)
+    qtapp.setOrganizationName("The Qubes Project")
+    qtapp.setOrganizationDomain("http://qubes-os.org")
+    qtapp.setApplicationName("Qubes Restore VMs")
 
     sys.excepthook = handle_exception
 
-    qvm_collection = QubesVmCollection()
-    qvm_collection.lock_db_for_reading()
-    qvm_collection.load()
-    qvm_collection.unlock_db()
+    app = Qubes()
 
-    global restore_window
-    restore_window = RestoreVMsWindow()
+    restore_window = RestoreVMsWindow(qtapp, app)
 
     restore_window.show()
 
-    app.exec_()
-    app.exit()
-
+    qtapp.exec_()
+    qtapp.exit()
 
 
 if __name__ == "__main__":

+ 2 - 0
rpm_spec/qmgr.spec

@@ -60,6 +60,8 @@ rm -rf $RPM_BUILD_ROOT
 /usr/bin/qubes-vm-settings
 /usr/bin/qubes-vm-create
 /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/qvm_about.sh
 

+ 3 - 1
setup.py

@@ -21,6 +21,8 @@ if __name__ == '__main__':
                 'qubes-global-settings = qubesmanager.global_settings:main',
                 'qubes-vm-settings = qubesmanager.settings: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'
             ],
         })

+ 53 - 67
ui/backupdlg.ui

@@ -23,73 +23,8 @@
    <layout class="QVBoxLayout" name="verticalLayout">
     <item>
      <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">
        <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">
          <string>Select VMs to backup:</string>
         </property>
@@ -145,6 +80,57 @@
       </item>
      </layout>
     </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>
   </widget>
   <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;
 &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; }
-&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;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;/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; font-family:'Sans Serif'; font-size:9pt;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
       </property>
      </widget>
     </item>