backup.py 19 KB

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