diff --git a/qubesmanager/backup.py b/qubesmanager/backup.py index 5150cc2..ef3623d 100644 --- a/qubesmanager/backup.py +++ b/qubesmanager/backup.py @@ -47,6 +47,7 @@ from string import replace from ui_backupdlg import * from multiselectwidget import * +from backup_utils import * class BackupVMsWindow(Ui_Backup, QWizard): @@ -63,6 +64,7 @@ class BackupVMsWindow(Ui_Backup, QWizard): self.qvm_collection = qvm_collection self.blk_manager = blk_manager + self.dev_mount_path = None self.backup_dir = None self.func_output = [] @@ -91,7 +93,7 @@ class BackupVMsWindow(Ui_Backup, QWizard): self.select_vms_page.connect(self.select_vms_widget, SIGNAL("selected_changed()"), SIGNAL("completeChanged()")) self.__fill_vms_list__() - self.__fill_devs_list__() + fill_devs_list(self) def __fill_vms_list__(self): for vm in self.qvm_collection.values(): @@ -110,112 +112,14 @@ class BackupVMsWindow(Ui_Backup, QWizard): self.to_backup.append(vm.name) self.select_vms_widget.available_list.addItem(vm.name) - def __fill_devs_list__(self): - self.dev_combobox.clear() - self.dev_combobox.addItem("None") - for a in self.blk_manager.attached_devs: - if self.blk_manager.attached_devs[a]['attached_to']['vm'] == self.vm.name : - att = a + " " + unicode(self.blk_manager.attached_devs[a]['size']) + " " + self.blk_manager.attached_devs[a]['desc'] - self.dev_combobox.addItem(att, QVariant(a)) - for a in self.blk_manager.free_devs: - att = a + " " + unicode(self.blk_manager.free_devs[a]['size']) + " " + self.blk_manager.free_devs[a]['desc'] - self.dev_combobox.addItem(att, QVariant(a)) - self.dev_combobox.setCurrentIndex(0) #current selected is null "" - self.prev_dev_idx = 0 - self.dir_line_edit.clear() - self.dir_line_edit.setEnabled(False) - self.select_path_button.setEnabled(False) - - def __check_if_mounted__(self, dev_path): - mounts_file = open("/proc/mounts") - for m in list(mounts_file): - if m.startswith(dev_path): - print m - return m.split(" ")[1] - return None - - def __mount_device__(self, dev_path): - try: - mount_dir_name = "backup" + replace(str(datetime.now()),' ', '-').split(".")[0] - pmount_cmd = ["pmount", dev_path, mount_dir_name] - res = subprocess.check_call(pmount_cmd) - print "pmount device res: ", res - except Exception as ex: - QMessageBox.warning (None, "Error mounting selected device!", "ERROR: {0}".format(ex)) - return None - if res == 0: - self.dev_mount_path = "/media/"+mount_dir_name - return self.dev_mount_path - - def __umount_device__(self, dev_mount_path): - try: - pumount_cmd = ["pumount", dev_mount_path] - res = subprocess.check_call(pumount_cmd) - print "pumount device res: ", res - except Exception as ex: - QMessageBox.warning (None, "Could not unmount backup device!", "ERROR: {0}".format(ex)) - - - - def __enable_dir_line_edit__(self, boolean): - self.dir_line_edit.setEnabled(boolean) - self.select_path_button.setEnabled(boolean) - - + def dev_combobox_activated(self, idx): - print self.dev_combobox.currentText() - if idx == self.prev_dev_idx: #nothing has changed - return - #there was a change - self.prev_dev_idx = idx - - self.dir_line_edit.setText("") - self.backup_dir = None - self.dev_mount_path = None - self.__enable_dir_line_edit__(False) - - if self.dev_combobox.currentText() != "None": #An existing device chosen - dev_name = str(self.dev_combobox.itemData(idx).toString()) - - if dev_name in self.blk_manager.free_devs: - if dev_name.startswith(self.vm.name): # originally attached to dom0 - dev_path = "/dev/"+dev_name.split(":")[1] - print "device from dom0 - no need to attach" - - else: # originally attached to another domain, eg. usbvm - print "device from " + dev_name.split(":")[0] - #attach it to dom0, then treat it as an attached device - self.blk_manager.attach_device(self.vm, dev_name) - - if dev_name in self.blk_manager.attached_devs: #is attached to dom0 - print "device attached as " + self.blk_manager.attached_devs[dev_name]['attached_to']['frontend'] - assert self.blk_manager.attached_devs[dev_name]['attached_to']['vm'] == self.vm.name - - dev_path = "/dev/" + self.blk_manager.attached_devs[dev_name]['attached_to']['frontend'] - - #check if device mounted - self.dev_mount_path = self.__check_if_mounted__(dev_path) - if self.dev_mount_path != None: - self.__enable_dir_line_edit__(True) - else: - self.dev_mount_path = self.__mount_device__(dev_path) - if self.dev_mount_path != None: - self.__enable_dir_line_edit__(True) - - self.select_dir_page.emit(SIGNAL("completeChanged()")) - + dev_combobox_activated(self, idx) @pyqtSlot(name='on_select_path_button_clicked') def select_path_button_clicked(self): - self.backup_dir = self.dir_line_edit.text() - file_dialog = QFileDialog() - file_dialog.setReadOnly(True) - new_path = file_dialog.getExistingDirectory(self, "Select backup directory.", self.dev_mount_path) - if new_path: - self.dir_line_edit.setText(new_path) - self.backup_dir = new_path - self.select_dir_page.emit(SIGNAL("completeChanged()")) + select_path_button_clicked(self) def validateCurrentPage(self): if self.currentPage() is self.select_vms_page: @@ -228,18 +132,15 @@ class BackupVMsWindow(Ui_Backup, QWizard): self.func_output.append(s) def update_progress_bar(self, value): - print "progress bar value: ", value self.emit(SIGNAL("backup_progress(int)"), value) def __do_backup__(self, thread_monitor): - print "doiing backup" msg = [] try: qubesutils.backup_do(str(self.backup_dir), self.files_to_backup, self.update_progress_bar) #simulate_long_lasting_proces(10, self.update_progress_bar) except Exception as ex: - print "got exception from backup" msg.append(str(ex)) if len(msg) > 0 : @@ -252,25 +153,17 @@ class BackupVMsWindow(Ui_Backup, QWizard): if self.currentPage() is self.confirm_page: del self.func_output[:] self.files_to_backup = qubesutils.backup_prepare(str(self.backup_dir), exclude_list = self.excluded, print_callback = self.gather_output) - for i in self.excluded: - print i + self.textEdit.setReadOnly(True) self.textEdit.setFontFamily("Monospace") self.textEdit.setText("\n".join(self.func_output)) - for i in self.func_output: - print i - - for s in self.files_to_backup: - print s elif self.currentPage() is self.commit_page: self.button(self.CancelButton).setDisabled(True) self.button(self.FinishButton).setDisabled(True) - print "butons disabled" self.thread_monitor = ThreadMonitor() thread = threading.Thread (target= self.__do_backup__ , args=(self.thread_monitor,)) thread.daemon = True - print "will start thread" thread.start() while not self.thread_monitor.is_finished(): @@ -280,25 +173,16 @@ class BackupVMsWindow(Ui_Backup, QWizard): if not self.thread_monitor.success: QMessageBox.warning (None, "Backup error!", "ERROR: {1}".format(self.vm.name, self.thread_monitor.error_msg)) - self.__umount_device__(self.dev_mount_path) + umount_device(self.dev_mount_path) self.button(self.FinishButton).setEnabled(True) - def has_selected_vms(self): - print "isComplete called" return self.select_vms_widget.selected_list.count() > 0 def has_selected_dir(self): return self.backup_dir != None -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 # Bases on the original code by: diff --git a/qubesmanager/backup_utils.py b/qubesmanager/backup_utils.py new file mode 100644 index 0000000..0978b03 --- /dev/null +++ b/qubesmanager/backup_utils.py @@ -0,0 +1,159 @@ +#!/usr/bin/python2.6 +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2012 Agnieszka Kostrzewa +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# + +import sys +import os +from PyQt4.QtCore import * +from PyQt4.QtGui import * + +from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent + +import subprocess +import time + +from thread_monitor import * + +from datetime import datetime +from string import replace + + +def check_if_mounted(dev_path): + mounts_file = open("/proc/mounts") + for m in list(mounts_file): + if m.startswith(dev_path): + return m.split(" ")[1] + return None + + +def mount_device(dev_path): + try: + mount_dir_name = "backup" + replace(str(datetime.now()),' ', '-').split(".")[0] + pmount_cmd = ["pmount", dev_path, mount_dir_name] + res = subprocess.check_call(pmount_cmd) + except Exception as ex: + QMessageBox.warning (None, "Error mounting selected device!", "ERROR: {0}".format(ex)) + return None + if res == 0: + dev_mount_path = "/media/"+mount_dir_name + return dev_mount_path + return None + +def umount_device(dev_mount_path): + try: + pumount_cmd = ["pumount", dev_mount_path] + res = subprocess.check_call(pumount_cmd) + if res == 0: + dev_mount_path = None + except Exception as ex: + QMessageBox.warning (None, "Could not unmount backup device!", "ERROR: {0}".format(ex)) + return dev_mount_path + + +def fill_devs_list(dialog): + dialog.dev_combobox.clear() + dialog.dev_combobox.addItem("None") + for a in dialog.blk_manager.attached_devs: + if dialog.blk_manager.attached_devs[a]['attached_to']['vm'] == dialog.vm.name : + att = a + " " + unicode(dialog.blk_manager.attached_devs[a]['size']) + " " + dialog.blk_manager.attached_devs[a]['desc'] + dialog.dev_combobox.addItem(att, QVariant(a)) + for a in dialog.blk_manager.free_devs: + att = a + " " + unicode(dialog.blk_manager.free_devs[a]['size']) + " " + dialog.blk_manager.free_devs[a]['desc'] + dialog.dev_combobox.addItem(att, QVariant(a)) + dialog.dev_combobox.setCurrentIndex(0) #current selected is null "" + dialog.prev_dev_idx = 0 + dialog.dir_line_edit.clear() + dialog.dir_line_edit.setEnabled(False) + dialog.select_path_button.setEnabled(False) + + +def enable_dir_line_edit(dialog, boolean): + dialog.dir_line_edit.setEnabled(boolean) + dialog.select_path_button.setEnabled(boolean) + + +def dev_combobox_activated(dialog, idx): + if idx == dialog.prev_dev_idx: #nothing has changed + return + #there was a change + + dialog.dir_line_edit.setText("") + dialog.backup_dir = None + + if dialog.dev_mount_path != None: + dialog.dev_mount_path = umount_device(dialog.dev_mount_path) + if dialog_dev_mount_path != None: + dialog.dev_combobox.setCurrentIndex(dialog.prev_dev_idx) + return + + enable_dir_line_edit(dialog, False) + + if dialog.dev_combobox.currentText() != "None": #An existing device chosen + dev_name = str(dialog.dev_combobox.itemData(idx).toString()) + + if dev_name in dialog.blk_manager.free_devs: + if dev_name.startswith(dialog.vm.name): # originally attached to dom0 + dev_path = "/dev/"+dev_name.split(":")[1] + + else: # originally attached to another domain, eg. usbvm + #attach it to dom0, then treat it as an attached device + dialog.blk_manager.attach_device(dialog.vm, dev_name) + + if dev_name in dialog.blk_manager.attached_devs: #is attached to dom0 + assert dialog.blk_manager.attached_devs[dev_name]['attached_to']['vm'] == dialog.vm.name + dev_path = "/dev/" + dialog.blk_manager.attached_devs[dev_name]['attached_to']['frontend'] + + #check if device mounted + dialog.dev_mount_path = check_if_mounted(dev_path) + if dialog.dev_mount_path != None: + enable_dir_line_edit(dialog, True) + else: + dialog.dev_mount_path = mount_device(dev_path) + if dialog.dev_mount_path != None: + enable_dir_line_edit(dialog, True) + else: + dialog.dev_combobox.setCurrentIndex(0) #if couldn't mount - set current device to "None" + + + dialog.prev_dev_idx = idx + dialog.select_dir_page.emit(SIGNAL("completeChanged()")) + + +def select_path_button_clicked(dialog): + dialog.backup_dir = dialog.dir_line_edit.text() + file_dialog = QFileDialog() + file_dialog.setReadOnly(True) + new_path = file_dialog.getExistingDirectory(dialog, "Select backup directory.", dialog.dev_mount_path) + if new_path: + dialog.dir_line_edit.setText(new_path) + dialog.backup_dir = new_path + 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 + diff --git a/qubesmanager/main.py b/qubesmanager/main.py index 9e666d1..5f578cc 100755 --- a/qubesmanager/main.py +++ b/qubesmanager/main.py @@ -1083,7 +1083,7 @@ class VmManagerWindow(Ui_VmManagerWindow, QMainWindow): @pyqtSlot(name='on_action_restore_triggered') def action_restore_triggered(self): - restore_window = RestoreVMsWindow() + restore_window = RestoreVMsWindow(app, self.qvm_collection, self.blk_manager) restore_window.exec_() @pyqtSlot(name='on_action_backup_triggered') diff --git a/qubesmanager/restore.py b/qubesmanager/restore.py index d243a25..a2d509e 100644 --- a/qubesmanager/restore.py +++ b/qubesmanager/restore.py @@ -30,54 +30,197 @@ 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 from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent import subprocess import time -import threading from operator import itemgetter +from thread_monitor import * + +from qubes import qubesutils from ui_restoredlg import * from multiselectwidget import * +from backup_utils import * + class RestoreVMsWindow(Ui_Restore, QWizard): - def __init__(self, parent=None): + __pyqtSignals__ = ("restore_progress(int)",) + + def __init__(self, app, qvm_collection, blk_manager, parent=None): super(RestoreVMsWindow, self).__init__(parent) + self.app = app + self.qvm_collection = qvm_collection + self.blk_manager = blk_manager + + self.dev_mount_path = None + self.backup_dir = None + self.restore_options = None + self.backup_vms_list = None + self.func_output = [] + + self.excluded = {} + + for vm in self.qvm_collection.values(): + if vm.qid == 0: + self.vm = vm + break; + + assert self.vm != None + self.setupUi(self) - self.selectVMsWidget = MultiSelectWidget(self) - self.selectVMsLayout.insertWidget(1, self.selectVMsWidget) + self.select_vms_widget = MultiSelectWidget(self) + self.select_vms_layout.insertWidget(1, self.select_vms_widget) - self.selectVMsWidget.available_list.addItem("netVM1") - self.selectVMsWidget.available_list.addItem("appVM1") - self.selectVMsWidget.available_list.addItem("appVM2") - self.selectVMsWidget.available_list.addItem("templateVM1") - self.connect(self, SIGNAL("currentIdChanged(int)"), self.current_page_changed) - - def reject(self): - self.done(0) + self.connect(self.dev_combobox, SIGNAL("activated(int)"), self.dev_combobox_activated) + self.connect(self, SIGNAL("restore_progress(QString)"), self.commit_text_edit.append) + + self.select_dir_page.isComplete = self.has_selected_dir + self.select_vms_page.isComplete = self.has_selected_vms + #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_devs_list(self) + self.__init_restore_options__() + + + def dev_combobox_activated(self, idx): + dev_combobox_activated(self, idx) + + + @pyqtSlot(name='on_select_path_button_clicked') + def select_path_button_clicked(self): + select_path_button_clicked(self) + + def on_ignore_missing_toggled(self, checked): + self.restore_options['use-default-template'] = checked + self.restore_options['use-default-netvm'] = checked + + def on_ignore_uname_mismatch_toggled(self, checked): + self.restore_options['ignore-username-mismatch'] = checked + + def on_skip_dom0_toggled(self, checked): + self.restore_options['dom0-home'] = checked + + + def __fill_vms_list__(self): + if self.backup_vms_list != None: + return + + self.select_vms_widget.selected_list.clear() + self.select_vms_widget.available_list.clear() + + self.vms_to_restore = qubesutils.backup_restore_prepare(str(self.backup_dir), self.restore_options, self.qvm_collection) + for vmname in self.vms_to_restore: + self.select_vms_widget.available_list.addItem(vmname) + + def __init_restore_options__(self): + if not self.restore_options: + self.restore_options = {} + qubesutils.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']) + + if 'dom0-home' in self.restore_options: + self.skip_dom0.setChecked(self.restore_options['dom0-home']) - def save_and_apply(self): - pass - def current_page_changed(self, id): - self.button(self.CancelButton).setDisabled(id==3) - @pyqtSlot(name='on_selectPathButton_clicked') - def selectPathButton_clicked(self): - self.path = self.pathLineEdit.text() - newPath = QFileDialog.getExistingDirectory(self, 'Select backup directory.') - if newPath: - self.pathLineEdit.setText(newPath) - self.path = newPath + def gather_output(self, s): + self.func_output.append(s) + + def restore_error_output(self, s): + self.emit(SIGNAL("restore_progress(QString)"), '{0}'.format(s)) + + + def restore_output(self, s): + self.emit(SIGNAL("restore_progress(QString)"),'{0}'.format(s)) + + + def __do_restore__(self, thread_monitor): + err_msg = [] + self.qvm_collection.lock_db_for_writing() + try: + qubesutils.backup_restore_do(str(self.backup_dir), self.vms_to_restore, self.qvm_collection, self.restore_output, self.restore_error_output) + except Exception as ex: + err_msg.append(str(ex)) + + self.qvm_collection.unlock_db() + if len(err_msg) > 0 : + thread_monitor.set_error_msg('\n'.join(err_msg)) + self.emit(SIGNAL("restore_progress(QString)"),'{0}'.format("Finished with errors!")) + else: + self.emit(SIGNAL("restore_progress(QString)"),'{0}'.format("Finished successfully!")) + + thread_monitor.set_finished() + + + def current_page_changed(self, id): + + if self.currentPage() is self.select_vms_page: + self.__fill_vms_list__() + + elif self.currentPage() is self.confirm_page: + for v in self.excluded: + self.vms_to_restore[v] = self.excluded[v] + self.excluded = {} + for i in range(self.select_vms_widget.available_list.count()): + vmname = self.select_vms_widget.available_list.item(i).text() + self.excluded[str(vmname)] = self.vms_to_restore[str(vmname)] + del self.vms_to_restore[str(vmname)] + + del self.func_output[:] + qubesutils.backup_restore_print_summary(self.vms_to_restore, print_callback = self.gather_output) + self.confirm_text_edit.setReadOnly(True) + self.confirm_text_edit.setFontFamily("Monospace") + self.confirm_text_edit.setText("\n".join(self.func_output)) + + + elif self.currentPage() is self.commit_page: + self.button(self.CancelButton).setDisabled(True) + self.button(self.FinishButton).setDisabled(True) + + self.thread_monitor = ThreadMonitor() + thread = threading.Thread (target= self.__do_restore__ , args=(self.thread_monitor,)) + thread.daemon = True + thread.start() + + while not self.thread_monitor.is_finished(): + self.app.processEvents() + time.sleep (0.1) + + #if not self.thread_monitor.success: + #QMessageBox.warning (None, "Backup error!", "ERROR: {1}".format(self.vm.name, self.thread_monitor.error_msg)) + + umount_device(self.dev_mount_path) + self.button(self.FinishButton).setEnabled(True) + + + + def has_selected_dir(self): + return self.backup_dir != None + + def has_selected_vms(self): + return self.select_vms_widget.selected_list.count() > 0 + + # Bases on the original code by: # Copyright (c) 2002-2007 Pascal Varet diff --git a/restoredlg.ui b/restoredlg.ui index 9c9d244..3c908e9 100644 --- a/restoredlg.ui +++ b/restoredlg.ui @@ -47,7 +47,7 @@ - + 0 @@ -84,10 +84,10 @@ - + - + ... @@ -96,6 +96,48 @@ + + + + + 50 + false + + + + Restore options + + + + + + Ignore missing templates or netvms, restore VMs anyway. + + + ignore missing + + + + + + + skip dom0 + + + + + + + Ignore dom0 username mismatch while restoring homedir. + + + ignore username mismatch + + + + + + @@ -114,72 +156,11 @@ - + VMs to restore - - - - - - - - 50 - false - - - - Restore options - - - - - - - 50 - false - - - - Do not restore VMs that have missing templates or netvms. - - - skip broken - - - - - - - Ignore missing templates or netvms, restore VMs anyway. - - - ignore missing - - - - - - - Do not restore VMs that are already present on the host. - - - skip conflicting - - - - - - - Ignore dom0 username mismatch while restoring homedir. - - - ignore username mismatch - - - - + @@ -203,7 +184,7 @@ - + <!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"> @@ -238,33 +219,7 @@ p, li { white-space: pre-wrap; } - - - - 9 - 50 - false - false - false - - - - Restore in progress... - - - - - - - 24 - - - Qt::AlignCenter - - - false - - +