backup.py 19 KB

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