Initial backup-and-restore GUI restoration

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

View File

@ -1,4 +1,4 @@
#!/usr/bin/python2
#!/usr/bin/python3
# pylint: skip-file
#
# 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: "
"<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 )
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 ))
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))
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()

View File

@ -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)

View File

@ -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__":

View File

@ -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

View File

@ -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'
],
})

View File

@ -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>