restore.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. #!/usr/bin/python2
  2. #
  3. # The Qubes OS Project, http://www.qubes-os.org
  4. #
  5. # Copyright (C) 2012 Agnieszka Kostrzewa <agnieszka.kostrzewa@gmail.com>
  6. # Copyright (C) 2012 Marek Marczykowski <marmarek@mimuw.edu.pl>
  7. #
  8. # This program is free software; you can redistribute it and/or
  9. # modify it under the terms of the GNU General Public License
  10. # as published by the Free Software Foundation; either version 2
  11. # of the License, or (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU Lesser General Public License along
  19. # with this program; if not, see <http://www.gnu.org/licenses/>.
  20. #
  21. #
  22. import sys
  23. from PyQt4 import QtCore # pylint: disable=import-error
  24. from PyQt4 import QtGui # pylint: disable=import-error
  25. import threading
  26. import time
  27. import os
  28. import os.path
  29. import traceback
  30. import logging
  31. import logging.handlers
  32. import signal
  33. from qubes import backup
  34. from . import ui_restoredlg # pylint: disable=no-name-in-module
  35. from . import multiselectwidget
  36. from . import backup_utils
  37. from . import thread_monitor
  38. from multiprocessing import Queue, Event
  39. from multiprocessing.queues import Empty
  40. from qubesadmin import Qubes, exc
  41. from qubesadmin.backup import restore
  42. class RestoreVMsWindow(ui_restoredlg.Ui_Restore, QtGui.QWizard):
  43. def __init__(self, qt_app, qubes_app, parent=None):
  44. super(RestoreVMsWindow, self).__init__(parent)
  45. self.qt_app = qt_app
  46. self.qubes_app = qubes_app
  47. self.vms_to_restore = None
  48. self.func_output = []
  49. # Set up logging
  50. self.feedback_queue = Queue()
  51. handler = logging.handlers.QueueHandler(self.feedback_queue)
  52. logger = logging.getLogger('qubesadmin.backup')
  53. logger.addHandler(handler)
  54. logger.setLevel(logging.INFO)
  55. self.canceled = False
  56. self.error_detected = Event()
  57. self.thread_monitor = None
  58. self.backup_restore = None
  59. self.target_appvm = None
  60. self.setupUi(self)
  61. self.select_vms_widget = multiselectwidget.MultiSelectWidget(self)
  62. self.select_vms_layout.insertWidget(1, self.select_vms_widget)
  63. self.connect(self,
  64. QtCore.SIGNAL("currentIdChanged(int)"),
  65. self.current_page_changed)
  66. self.dir_line_edit.connect(self.dir_line_edit,
  67. QtCore.SIGNAL("textChanged(QString)"),
  68. self.backup_location_changed)
  69. self.select_dir_page.isComplete = self.has_selected_dir
  70. self.select_vms_page.isComplete = self.has_selected_vms
  71. self.confirm_page.isComplete = self.all_vms_good
  72. # FIXME
  73. # this causes to run isComplete() twice, I don't know why
  74. self.select_vms_page.connect(
  75. self.select_vms_widget,
  76. QtCore.SIGNAL("selected_changed()"),
  77. QtCore.SIGNAL("completeChanged()"))
  78. backup_utils.fill_appvms_list(self)
  79. @QtCore.pyqtSlot(name='on_select_path_button_clicked')
  80. def select_path_button_clicked(self):
  81. backup_utils.select_path_button_clicked(self, True)
  82. def cleanupPage(self, p_int): # pylint: disable=invalid-name
  83. if self.page(p_int) is self.select_vms_page:
  84. self.vms_to_restore = None
  85. else:
  86. super(RestoreVMsWindow, self).cleanupPage(p_int)
  87. def __fill_vms_list__(self):
  88. if self.vms_to_restore is not None:
  89. return
  90. self.select_vms_widget.selected_list.clear()
  91. self.select_vms_widget.available_list.clear()
  92. self.target_appvm = None
  93. if self.appvm_combobox.currentIndex() != 0: # An existing appvm chosen
  94. self.target_appvm = self.qubes_app.domains[
  95. str(self.appvm_combobox.currentText())]
  96. try:
  97. self.backup_restore = restore.BackupRestore(
  98. self.qubes_app,
  99. self.dir_line_edit.text(),
  100. self.target_appvm,
  101. self.passphrase_line_edit.text()
  102. )
  103. if self.ignore_missing.isChecked():
  104. self.backup_restore.options.use_default_template = True
  105. self.backup_restore.options.use_default_netvm = True
  106. if self.ignore_uname_mismatch.isChecked():
  107. self.backup_restore.options.ignore_username_mismatch = True
  108. if self.verify_only.isChecked():
  109. self.backup_restore.options.verify_only = True
  110. # pylint: disable=assignment-from-no-return
  111. self.vms_to_restore = self.backup_restore.get_restore_info()
  112. for vmname in self.vms_to_restore:
  113. if vmname.startswith('$'):
  114. # Internal info
  115. continue
  116. self.select_vms_widget.available_list.addItem(vmname)
  117. except exc.QubesException as ex:
  118. QtGui.QMessageBox.warning(None, self.tr("Restore error!"), str(ex))
  119. def append_output(self, text):
  120. self.commit_text_edit.append(text)
  121. def __do_restore__(self, t_monitor):
  122. err_msg = []
  123. try:
  124. self.backup_restore.restore_do(self.vms_to_restore)
  125. except backup.BackupCanceledError as ex:
  126. self.canceled = True
  127. err_msg.append(str(ex))
  128. except Exception as ex: # pylint: disable=broad-except
  129. err_msg.append(str(ex))
  130. err_msg.append(
  131. self.tr("Partially restored files left in /var/tmp/restore_*, "
  132. "investigate them and/or clean them up"))
  133. if self.canceled:
  134. self.append_output('<b><font color="red">{0}</font></b>'.format(
  135. self.tr("Restore aborted!")))
  136. elif err_msg or self.error_detected.is_set():
  137. if err_msg:
  138. t_monitor.set_error_msg('\n'.join(err_msg))
  139. self.append_output('<b><font color="red">{0}</font></b>'.format(
  140. self.tr("Finished with errors!")))
  141. else:
  142. self.append_output('<font color="green">{0}</font>'.format(
  143. self.tr("Finished successfully!")))
  144. t_monitor.set_finished()
  145. def current_page_changed(self, page_id): # pylint: disable=unused-argument
  146. old_sigchld_handler = signal.signal(signal.SIGCHLD, signal.SIG_DFL)
  147. if self.currentPage() is self.select_vms_page:
  148. self.__fill_vms_list__()
  149. elif self.currentPage() is self.confirm_page:
  150. # pylint: disable=assignment-from-no-return
  151. self.vms_to_restore = self.backup_restore.get_restore_info()
  152. for i in range(self.select_vms_widget.available_list.count()):
  153. vmname = self.select_vms_widget.available_list.item(i).text()
  154. del self.vms_to_restore[str(vmname)]
  155. self.vms_to_restore = self.backup_restore.restore_info_verify(
  156. self.vms_to_restore)
  157. self.func_output = self.backup_restore.get_restore_summary(
  158. self.vms_to_restore
  159. )
  160. self.confirm_text_edit.setReadOnly(True)
  161. self.confirm_text_edit.setFontFamily("Monospace")
  162. self.confirm_text_edit.setText(self.func_output)
  163. self.confirm_page.emit(QtCore.SIGNAL("completeChanged()"))
  164. elif self.currentPage() is self.commit_page:
  165. self.button(self.FinishButton).setDisabled(True)
  166. self.showFileDialog.setEnabled(True)
  167. self.showFileDialog.setChecked(self.showFileDialog.isEnabled()
  168. and str(self.dir_line_edit.text())
  169. .count("media/") > 0)
  170. self.thread_monitor = thread_monitor.ThreadMonitor()
  171. thread = threading.Thread(target=self.__do_restore__,
  172. args=(self.thread_monitor,))
  173. thread.daemon = True
  174. thread.start()
  175. while not self.thread_monitor.is_finished():
  176. self.qt_app.processEvents()
  177. time.sleep(0.1)
  178. try:
  179. log_record = self.feedback_queue.get_nowait()
  180. while log_record:
  181. if log_record.levelno == logging.ERROR or\
  182. log_record.levelno == logging.CRITICAL:
  183. output = '<font color="red">{0}</font>'.format(
  184. log_record.getMessage())
  185. else:
  186. output = log_record.getMessage()
  187. self.append_output(output)
  188. log_record = self.feedback_queue.get_nowait()
  189. except Empty:
  190. pass
  191. if not self.thread_monitor.success:
  192. if not self.canceled:
  193. QtGui.QMessageBox.warning(
  194. None,
  195. self.tr("Backup error!"),
  196. self.tr("ERROR: {0}").format(
  197. self.thread_monitor.error_msg))
  198. self.progress_bar.setMaximum(100)
  199. self.progress_bar.setValue(100)
  200. if self.showFileDialog.isChecked():
  201. self.append_output(
  202. '<b><font color="black">{0}</font></b>'.format(
  203. self.tr("Please unmount your backup volume and cancel "
  204. "the file selection dialog.")))
  205. self.qt_app.processEvents()
  206. backup_utils.select_path_button_clicked(self, False, True)
  207. self.button(self.FinishButton).setEnabled(True)
  208. self.button(self.CancelButton).setEnabled(False)
  209. self.showFileDialog.setEnabled(False)
  210. signal.signal(signal.SIGCHLD, old_sigchld_handler)
  211. def all_vms_good(self):
  212. for vm_info in self.vms_to_restore.values():
  213. if not vm_info.vm:
  214. continue
  215. if not vm_info.good_to_go:
  216. return False
  217. return True
  218. def reject(self):
  219. if self.currentPage() is self.commit_page:
  220. self.backup_restore.canceled = True
  221. self.append_output('<font color="red">{0}</font>'.format(
  222. self.tr("Aborting the operation...")))
  223. self.button(self.CancelButton).setDisabled(True)
  224. else:
  225. self.done(0)
  226. def has_selected_dir(self):
  227. backup_location = self.dir_line_edit.text()
  228. if not backup_location:
  229. return False
  230. if self.appvm_combobox.currentIndex() == 0:
  231. if os.path.isfile(backup_location) or \
  232. os.path.isfile(os.path.join(backup_location, 'qubes.xml')):
  233. return True
  234. else:
  235. return True
  236. return False
  237. def has_selected_vms(self):
  238. return self.select_vms_widget.selected_list.count() > 0
  239. def backup_location_changed(self, new_dir=None):
  240. # pylint: disable=unused-argument
  241. self.select_dir_page.emit(QtCore.SIGNAL("completeChanged()"))
  242. # Bases on the original code by:
  243. # Copyright (c) 2002-2007 Pascal Varet <p.varet@gmail.com>
  244. def handle_exception(exc_type, exc_value, exc_traceback):
  245. filename, line, dummy, dummy = traceback.extract_tb(exc_traceback).pop()
  246. filename = os.path.basename(filename)
  247. error = "%s: %s" % (exc_type.__name__, exc_value)
  248. QtGui.QMessageBox.critical(None, "Houston, we have a problem...",
  249. "Whoops. A critical error has occured. "
  250. "This is most likely a bug "
  251. "in Qubes Restore VMs application.<br><br>"
  252. "<b><i>%s</i></b>" % error +
  253. "at <b>line %d</b> of file <b>%s</b>.<br/><br/>"
  254. % (line, filename))
  255. def main():
  256. qt_app = QtGui.QApplication(sys.argv)
  257. qt_app.setOrganizationName("The Qubes Project")
  258. qt_app.setOrganizationDomain("http://qubes-os.org")
  259. qt_app.setApplicationName("Qubes Restore VMs")
  260. sys.excepthook = handle_exception
  261. qubes_app = Qubes()
  262. restore_window = RestoreVMsWindow(qt_app, qubes_app)
  263. restore_window.show()
  264. qt_app.exec_()
  265. qt_app.exit()
  266. if __name__ == "__main__":
  267. main()