backup.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  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 General Public License
  19. # along with this program; if not, write to the Free Software
  20. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  21. #
  22. #
  23. import sys
  24. import os
  25. import signal
  26. import shutil
  27. from PyQt4.QtCore import *
  28. from PyQt4.QtGui import *
  29. from qubes.qubes import QubesVmCollection
  30. from qubes.qubes import QubesException
  31. from qubes.qubes import QubesDaemonPidfile
  32. from qubes.qubes import QubesHost
  33. from qubes import backup
  34. from qubes import qubesutils
  35. import qubesmanager.resources_rc
  36. from pyinotify import WatchManager, Notifier, ThreadedNotifier, EventsCodes, ProcessEvent
  37. import time
  38. from .thread_monitor import *
  39. from operator import itemgetter
  40. from datetime import datetime
  41. from string import replace
  42. from .ui_backupdlg import *
  43. from .multiselectwidget import *
  44. from .backup_utils import *
  45. import main
  46. import grp,pwd
  47. class BackupVMsWindow(Ui_Backup, QWizard):
  48. __pyqtSignals__ = ("backup_progress(int)",)
  49. def __init__(self, app, qvm_collection, blk_manager, shutdown_vm_func, parent=None):
  50. super(BackupVMsWindow, self).__init__(parent)
  51. self.app = app
  52. self.qvm_collection = qvm_collection
  53. self.blk_manager = blk_manager
  54. self.shutdown_vm_func = shutdown_vm_func
  55. self.func_output = []
  56. self.selected_vms = []
  57. self.tmpdir_to_remove = None
  58. self.canceled = False
  59. self.vm = self.qvm_collection[0]
  60. self.files_to_backup = None
  61. assert self.vm != None
  62. self.setupUi(self)
  63. self.progress_status.text = self.tr("Backup in progress...")
  64. self.show_running_vms_warning(False)
  65. self.dir_line_edit.setReadOnly(False)
  66. self.select_vms_widget = MultiSelectWidget(self)
  67. self.verticalLayout.insertWidget(1, self.select_vms_widget)
  68. self.connect(self, SIGNAL("currentIdChanged(int)"), self.current_page_changed)
  69. self.connect(self.select_vms_widget, SIGNAL("selected_changed()"), self.check_running)
  70. self.connect(self.select_vms_widget, SIGNAL("items_removed(PyQt_PyObject)"), self.vms_removed)
  71. self.connect(self.select_vms_widget, SIGNAL("items_added(PyQt_PyObject)"), self.vms_added)
  72. self.refresh_button.clicked.connect(self.check_running)
  73. self.shutdown_running_vms_button.clicked.connect(self.shutdown_all_running_selected)
  74. self.connect(self, SIGNAL("backup_progress(int)"), self.progress_bar.setValue)
  75. self.dir_line_edit.connect(self.dir_line_edit, SIGNAL("textChanged(QString)"), self.backup_location_changed)
  76. self.select_vms_page.isComplete = self.has_selected_vms
  77. self.select_dir_page.isComplete = self.has_selected_dir_and_pass
  78. #FIXME
  79. #this causes to run isComplete() twice, I don't know why
  80. self.select_vms_page.connect(
  81. self.select_vms_widget,
  82. SIGNAL("selected_changed()"),
  83. SIGNAL("completeChanged()"))
  84. self.passphrase_line_edit.connect(
  85. self.passphrase_line_edit,
  86. SIGNAL("textChanged(QString)"),
  87. self.backup_location_changed)
  88. self.passphrase_line_edit_verify.connect(
  89. self.passphrase_line_edit_verify,
  90. SIGNAL("textChanged(QString)"),
  91. self.backup_location_changed)
  92. self.total_size = 0
  93. self.__fill_vms_list__()
  94. fill_appvms_list(self)
  95. self.load_settings()
  96. def load_settings(self):
  97. dest_vm_name = main.manager_window.manager_settings.value(
  98. 'backup/vmname', defaultValue="")
  99. dest_vm_idx = self.appvm_combobox.findText(dest_vm_name.toString())
  100. if dest_vm_idx > -1:
  101. self.appvm_combobox.setCurrentIndex(dest_vm_idx)
  102. if main.manager_window.manager_settings.contains('backup/path'):
  103. dest_path = main.manager_window.manager_settings.value(
  104. 'backup/path', defaultValue=None)
  105. self.dir_line_edit.setText(dest_path.toString())
  106. if main.manager_window.manager_settings.contains('backup/encrypt'):
  107. encrypt = main.manager_window.manager_settings.value(
  108. 'backup/encrypt', defaultValue=None)
  109. self.encryption_checkbox.setChecked(encrypt.toBool())
  110. def save_settings(self):
  111. main.manager_window.manager_settings.setValue(
  112. 'backup/vmname', self.appvm_combobox.currentText())
  113. main.manager_window.manager_settings.setValue(
  114. 'backup/path', self.dir_line_edit.text())
  115. main.manager_window.manager_settings.setValue(
  116. 'backup/encrypt', self.encryption_checkbox.isChecked())
  117. def show_running_vms_warning(self, show):
  118. self.running_vms_warning.setVisible(show)
  119. self.shutdown_running_vms_button.setVisible(show)
  120. self.refresh_button.setVisible(show)
  121. class VmListItem(QListWidgetItem):
  122. def __init__(self, vm):
  123. self.vm = vm
  124. if vm.qid == 0:
  125. local_user = grp.getgrnam('qubes').gr_mem[0]
  126. home_dir = pwd.getpwnam(local_user).pw_dir
  127. self.size = qubesutils.get_disk_usage(home_dir)
  128. else:
  129. self.size = self.get_vm_size(vm)
  130. super(BackupVMsWindow.VmListItem, self).__init__(vm.name+ " (" + qubesutils.size_to_human(self.size) + ")")
  131. def get_vm_size(self, vm):
  132. size = 0
  133. if vm.private_img is not None:
  134. size += qubesutils.get_disk_usage (vm.private_img)
  135. if vm.updateable:
  136. size += qubesutils.get_disk_usage(vm.root_img)
  137. return size
  138. def __fill_vms_list__(self):
  139. for vm in self.qvm_collection.values():
  140. if vm.internal:
  141. continue
  142. item = BackupVMsWindow.VmListItem(vm)
  143. if vm.include_in_backups == True:
  144. self.select_vms_widget.selected_list.addItem(item)
  145. self.total_size += item.size
  146. else:
  147. self.select_vms_widget.available_list.addItem(item)
  148. self.select_vms_widget.available_list.sortItems()
  149. self.select_vms_widget.selected_list.sortItems()
  150. self.check_running()
  151. self.total_size_label.setText(qubesutils.size_to_human(self.total_size))
  152. def vms_added(self, items):
  153. for i in items:
  154. self.total_size += i.size
  155. self.total_size_label.setText(qubesutils.size_to_human(self.total_size))
  156. def vms_removed(self, items):
  157. for i in items:
  158. self.total_size -= i.size
  159. self.total_size_label.setText(qubesutils.size_to_human(self.total_size))
  160. def check_running(self):
  161. some_selected_vms_running = False
  162. for i in range(self.select_vms_widget.selected_list.count()):
  163. item = self.select_vms_widget.selected_list.item(i)
  164. if item.vm.is_running() and item.vm.qid != 0:
  165. item.setForeground(QBrush(QColor(255, 0, 0)))
  166. some_selected_vms_running = True
  167. else:
  168. item.setForeground(QBrush(QColor(0, 0, 0)))
  169. self.show_running_vms_warning(some_selected_vms_running)
  170. for i in range(self.select_vms_widget.available_list.count()):
  171. item = self.select_vms_widget.available_list.item(i)
  172. if item.vm.is_running() and item.vm.qid != 0:
  173. item.setForeground(QBrush(QColor(255, 0, 0)))
  174. else:
  175. item.setForeground(QBrush(QColor(0, 0, 0)))
  176. return some_selected_vms_running
  177. def shutdown_all_running_selected(self):
  178. (names, vms) = self.get_running_vms()
  179. if len(vms) == 0:
  180. return
  181. for vm in vms:
  182. self.blk_manager.check_if_serves_as_backend(vm)
  183. reply = QMessageBox.question(None, self.tr("VM Shutdown Confirmation"),
  184. self.tr(
  185. "Are you sure you want to power down the following VMs: "
  186. "<b>{0}</b>?<br/>"
  187. "<small>This will shutdown all the running applications "
  188. "within them.</small>").format(', '.join(names)),
  189. QMessageBox.Yes | QMessageBox.Cancel)
  190. self.app.processEvents()
  191. if reply == QMessageBox.Yes:
  192. wait_time = 60.0
  193. for vm in vms:
  194. self.shutdown_vm_func(vm, wait_time*1000)
  195. progress = QProgressDialog ("Shutting down VMs <b>{0}</b>...".format(', '.join(names)), "", 0, 0)
  196. progress.setModal(True)
  197. progress.show()
  198. wait_for = wait_time
  199. while self.check_running() and wait_for > 0:
  200. self.app.processEvents()
  201. time.sleep (0.5)
  202. wait_for -= 0.5
  203. progress.hide()
  204. def get_running_vms(self):
  205. names = []
  206. vms = []
  207. for i in range(self.select_vms_widget.selected_list.count()):
  208. item = self.select_vms_widget.selected_list.item(i)
  209. if item.vm.is_running() and item.vm.qid != 0:
  210. names.append(item.vm.name)
  211. vms.append(item.vm)
  212. return (names, vms)
  213. @pyqtSlot(name='on_select_path_button_clicked')
  214. def select_path_button_clicked(self):
  215. select_path_button_clicked(self)
  216. def validateCurrentPage(self):
  217. if self.currentPage() is self.select_vms_page:
  218. if self.check_running():
  219. QMessageBox.information(None,
  220. self.tr("Wait!"),
  221. self.tr("Some selected VMs are running. "
  222. "Running VMs can not be backuped. "
  223. "Please shut them down or remove them from the list."))
  224. return False
  225. self.selected_vms = []
  226. for i in range(self.select_vms_widget.selected_list.count()):
  227. self.selected_vms.append(self.select_vms_widget.selected_list.item(i).vm)
  228. elif self.currentPage() is self.select_dir_page:
  229. backup_location = str(self.dir_line_edit.text())
  230. if not backup_location:
  231. QMessageBox.information(None, self.tr("Wait!"),
  232. self.tr("Enter backup target location first."))
  233. return False
  234. if self.appvm_combobox.currentIndex() == 0 and \
  235. not os.path.isdir(backup_location):
  236. QMessageBox.information(None, self.tr("Wait!"),
  237. self.tr("Selected directory do not exists or "
  238. "not a directory (%s).") % backup_location)
  239. return False
  240. if not len(self.passphrase_line_edit.text()):
  241. QMessageBox.information(None, self.tr("Wait!"),
  242. self.tr("Enter passphrase for backup encryption/verification first."))
  243. return False
  244. if self.passphrase_line_edit.text() != self.passphrase_line_edit_verify.text():
  245. QMessageBox.information(None,
  246. self.tr("Wait!"),
  247. self.tr("Enter the same passphrase in both fields."))
  248. return False
  249. return True
  250. def gather_output(self, s):
  251. self.func_output.append(s)
  252. def update_progress_bar(self, value):
  253. self.emit(SIGNAL("backup_progress(int)"), value)
  254. def __do_backup__(self, thread_monitor):
  255. msg = []
  256. try:
  257. backup.backup_do(self.dir_line_edit.text(),
  258. self.files_to_backup,
  259. self.passphrase_line_edit.text(),
  260. progress_callback=self.update_progress_bar,
  261. encrypted=self.encryption_checkbox.isChecked(),
  262. appvm=self.target_appvm)
  263. #simulate_long_lasting_proces(10, self.update_progress_bar)
  264. except backup.BackupCanceledError as ex:
  265. msg.append(str(ex))
  266. self.canceled = True
  267. if ex.tmpdir:
  268. self.tmpdir_to_remove = ex.tmpdir
  269. except Exception as ex:
  270. print("Exception:", ex)
  271. msg.append(str(ex))
  272. if len(msg) > 0 :
  273. thread_monitor.set_error_msg('\n'.join(msg))
  274. thread_monitor.set_finished()
  275. def current_page_changed(self, id):
  276. old_sigchld_handler = signal.signal(signal.SIGCHLD, signal.SIG_DFL)
  277. if self.currentPage() is self.confirm_page:
  278. self.target_appvm = None
  279. if self.appvm_combobox.currentIndex() != 0: #An existing appvm chosen
  280. self.target_appvm = self.qvm_collection.get_vm_by_name(
  281. self.appvm_combobox.currentText())
  282. del self.func_output[:]
  283. try:
  284. self.files_to_backup = backup.backup_prepare(
  285. self.selected_vms,
  286. print_callback = self.gather_output,
  287. hide_vm_names=self.encryption_checkbox.isChecked())
  288. except Exception as ex:
  289. print("Exception:", ex)
  290. QMessageBox.critical(None,
  291. self.tr("Error while preparing backup."),
  292. self.tr("ERROR: {0}").format(ex))
  293. self.textEdit.setReadOnly(True)
  294. self.textEdit.setFontFamily("Monospace")
  295. self.textEdit.setText("\n".join(self.func_output))
  296. self.save_settings()
  297. elif self.currentPage() is self.commit_page:
  298. self.button(self.FinishButton).setDisabled(True)
  299. self.showFileDialog.setEnabled(
  300. self.appvm_combobox.currentIndex() != 0)
  301. self.showFileDialog.setChecked(self.showFileDialog.isEnabled()
  302. and str(self.dir_line_edit.text())
  303. .count("media/") > 0)
  304. self.thread_monitor = ThreadMonitor()
  305. thread = threading.Thread (target= self.__do_backup__ , args=(self.thread_monitor,))
  306. thread.daemon = True
  307. thread.start()
  308. counter = 0
  309. while not self.thread_monitor.is_finished():
  310. self.app.processEvents()
  311. time.sleep (0.1)
  312. if not self.thread_monitor.success:
  313. if self.canceled:
  314. self.progress_status.setText(self.tr("Backup aborted."))
  315. if self.tmpdir_to_remove:
  316. if QMessageBox.warning(None, self.tr("Backup aborted"),
  317. self.tr("Do you want to remove temporary files from "
  318. "%s?") % self.tmpdir_to_remove,
  319. QMessageBox.Yes, QMessageBox.No) == QMessageBox.Yes:
  320. shutil.rmtree(self.tmpdir_to_remove)
  321. else:
  322. self.progress_status.setText(self.tr("Backup error."))
  323. QMessageBox.warning(self, self.tr("Backup error!"),
  324. self.tr("ERROR: {}").format(
  325. self.thread_monitor.error_msg))
  326. else:
  327. self.progress_bar.setValue(100)
  328. self.progress_status.setText(self.tr("Backup finished."))
  329. if self.showFileDialog.isChecked():
  330. orig_text = self.progress_status.text
  331. self.progress_status.setText(
  332. orig_text + self.tr(
  333. " Please unmount your backup volume and cancel "
  334. "the file selection dialog."))
  335. if self.target_appvm:
  336. self.target_appvm.run("QUBESRPC %s dom0" % "qubes"
  337. ".SelectDirectory")
  338. self.button(self.CancelButton).setEnabled(False)
  339. self.button(self.FinishButton).setEnabled(True)
  340. self.showFileDialog.setEnabled(False)
  341. signal.signal(signal.SIGCHLD, old_sigchld_handler)
  342. def reject(self):
  343. #cancell clicked while the backup is in progress.
  344. #calling kill on tar.
  345. if self.currentPage() is self.commit_page:
  346. if backup.backup_cancel():
  347. self.button(self.CancelButton).setDisabled(True)
  348. else:
  349. self.done(0)
  350. def has_selected_vms(self):
  351. return self.select_vms_widget.selected_list.count() > 0
  352. def has_selected_dir_and_pass(self):
  353. if not len(self.passphrase_line_edit.text()):
  354. return False
  355. if self.passphrase_line_edit.text() != self.passphrase_line_edit_verify.text():
  356. return False
  357. return len(self.dir_line_edit.text()) > 0
  358. def backup_location_changed(self, new_dir = None):
  359. self.select_dir_page.emit(SIGNAL("completeChanged()"))
  360. # Bases on the original code by:
  361. # Copyright (c) 2002-2007 Pascal Varet <p.varet@gmail.com>
  362. def handle_exception(exc_type, exc_value, exc_traceback ):
  363. import sys
  364. import os.path
  365. import traceback
  366. filename, line, dummy, dummy = traceback.extract_tb( exc_traceback ).pop()
  367. filename = os.path.basename( filename )
  368. error = "%s: %s" % ( exc_type.__name__, exc_value )
  369. QMessageBox.critical(None, "Houston, we have a problem...",
  370. "Whoops. A critical error has occured. This is most likely a bug "
  371. "in Qubes Restore VMs application.<br><br>"
  372. "<b><i>%s</i></b>" % error +
  373. "at <b>line %d</b> of file <b>%s</b>.<br/><br/>"
  374. % ( line, filename ))
  375. def app_main():
  376. global qubes_host
  377. qubes_host = QubesHost()
  378. global app
  379. app = QApplication(sys.argv)
  380. app.setOrganizationName("The Qubes Project")
  381. app.setOrganizationDomain("http://qubes-os.org")
  382. app.setApplicationName("Qubes Backup VMs")
  383. sys.excepthook = handle_exception
  384. qvm_collection = QubesVmCollection()
  385. qvm_collection.lock_db_for_reading()
  386. qvm_collection.load()
  387. qvm_collection.unlock_db()
  388. global backup_window
  389. backup_window = BackupVMsWindow()
  390. backup_window.show()
  391. app.exec_()
  392. app.exit()
  393. if __name__ == "__main__":
  394. app_main()