From 7a4e4b35d5ee665d722d21f931481d3422452358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marta=20Marczykowska-G=C3=B3recka?= Date: Sun, 10 Dec 2017 21:14:14 +0100 Subject: [PATCH] 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 --- qubesmanager/backup.py | 444 +++++++++++++++-------------------- qubesmanager/backup_utils.py | 115 ++++----- qubesmanager/restore.py | 193 +++++++-------- rpm_spec/qmgr.spec | 2 + setup.py | 4 +- ui/backupdlg.ui | 120 +++++----- 6 files changed, 392 insertions(+), 486 deletions(-) diff --git a/qubesmanager/backup.py b/qubesmanager/backup.py index 6d808fe..b89b1cb 100644 --- a/qubesmanager/backup.py +++ b/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 +from qubesadmin import Qubes, events, exc +from qubesadmin import utils as admin_utils +from qubes.storage.file import get_disk_usage -import qubesmanager.resources_rc - -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 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) + try: + profile_data = load_backup_profile() + except Exception as ex: # TODO: fix just for file not found + return + if not profile_data: + return - 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 '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 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()) + 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) + ")") + self.size = vm.get_disk_utilization() + super(BackupVMsWindow.VmListItem, self).__init__( + vm.name + " (" + admin_utils.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) - - 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: " - "{0}?
" - "This will shutdown all the running applications " - "within them.").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 {0}...".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 -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 " - "in Qubes Restore VMs application.

" - "%s" % error + - "at line %d of file %s.

" - % ( line, filename )) + 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.

%s" % + error + "at line %d of file %s.

" + % (line, filename)) -def app_main(): +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 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() diff --git a/qubesmanager/backup_utils.py b/qubesmanager/backup_utils.py index 8a104e0..102b5be 100644 --- a/qubesmanager/backup_utils.py +++ b/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 '/') + # TODO: check if dom0 is available - 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) + 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.")) - if (new_path or new_appvm) and len(backup_location) > 0: + # 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) diff --git a/qubesmanager/restore.py b/qubesmanager/restore.py index c1609c5..f7f44b8 100644 --- a/qubesmanager/restore.py +++ b/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)"), '{0}' @@ -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)"), '{0}'.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)"), '{0}' .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 -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.

" "%s" % error + "at line %d of file %s.

" - % ( 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__": diff --git a/rpm_spec/qmgr.spec b/rpm_spec/qmgr.spec index 7c473fa..1a06cdd 100644 --- a/rpm_spec/qmgr.spec +++ b/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 diff --git a/setup.py b/setup.py index cba28dd..c89c898 100644 --- a/setup.py +++ b/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' ], }) diff --git a/ui/backupdlg.ui b/ui/backupdlg.ui index 31760d5..6b50497 100644 --- a/ui/backupdlg.ui +++ b/ui/backupdlg.ui @@ -23,73 +23,8 @@ - - - - - 0 - 0 - - - - Shutdown all running selected VMs - - - - :/shutdownvm.png:/shutdownvm.png - - - - - - - - 0 - 0 - - - - Refresh running states. - - - - - - - - 0 - 0 - - - - - 75 - true - true - - - - color:rgb(255, 0, 0) - - - Some of the selected VMs are running (red). Running VMs cannot be backed up! - - - true - - - - - - 9 - 50 - false - false - false - - Select VMs to backup: @@ -145,6 +80,57 @@ + + + + + + + + + 255 + 0 + 0 + + + + + + + + + 255 + 0 + 0 + + + + + + + + + 139 + 142 + 142 + + + + + + + + + 75 + true + true + + + + Warning: unrecognized data found in configuration files. + + + @@ -281,8 +267,8 @@ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> -<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html> +</style></head><body style=" font-family:'Cantarell'; font-size:11pt; font-weight:400; font-style:normal;"> +<p style="-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;"><br /></p></body></html>