backup_utils.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  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. #
  7. # This program is free software; you can redistribute it and/or
  8. # modify it under the terms of the GNU General Public License
  9. # as published by the Free Software Foundation; either version 2
  10. # of the License, or (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with this program; if not, write to the Free Software
  19. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  20. #
  21. #
  22. import re
  23. import sys
  24. import os
  25. from PyQt4.QtCore import *
  26. from PyQt4.QtGui import *
  27. import subprocess
  28. import time
  29. from qubes.qubes import QubesException
  30. from thread_monitor import *
  31. from datetime import datetime
  32. from string import replace
  33. path_re = re.compile(r"[a-zA-Z0-9/:.,_+=() -]*")
  34. path_max_len = 512
  35. mount_for_backup_path = '/usr/libexec/qubes-manager/mount_for_backup.sh'
  36. def check_if_mounted(dev_path):
  37. mounts_file = open("/proc/mounts")
  38. for m in list(mounts_file):
  39. if m.startswith(dev_path):
  40. return m.split(" ")[1]
  41. return None
  42. def mount_device(dev_path):
  43. try:
  44. mount_dir_name = "backup" + replace(str(datetime.now()),' ', '-').split(".")[0]
  45. pmount_cmd = [mount_for_backup_path, dev_path, mount_dir_name]
  46. res = subprocess.check_call(pmount_cmd)
  47. except Exception as ex:
  48. QMessageBox.warning (None, "Error mounting selected device!", "<b>Could not mount {0}.</b><br><br>ERROR: {1}".format(dev_path, ex))
  49. return None
  50. if res == 0:
  51. dev_mount_path = "/media/"+mount_dir_name
  52. return dev_mount_path
  53. return None
  54. def umount_device(dev_mount_path):
  55. while True:
  56. try:
  57. pumount_cmd = ["sudo", "pumount", "--luks-force", dev_mount_path]
  58. res = subprocess.check_call(pumount_cmd)
  59. if res == 0:
  60. dev_mount_path = None
  61. return dev_mount_path
  62. except Exception as ex:
  63. title = "Error unmounting backup device!"
  64. text = "<b>Could not unmount {0}.</b><br>\
  65. <b>Please retry or unmount it manually using</b><br> pumount {0}.<br><br>\
  66. ERROR: {1}".format(dev_mount_path, ex)
  67. button = QMessageBox.warning (None, title, text, QMessageBox.Ok | QMessageBox.Retry, QMessageBox.Retry)
  68. if button == QMessageBox.Ok:
  69. return dev_mount_path
  70. def detach_device(dialog, dev_name):
  71. """ Detach device from dom0, if device was attached from some VM"""
  72. if not dev_name.startswith(dialog.vm.name+":"):
  73. with dialog.blk_manager.blk_lock:
  74. dialog.blk_manager.detach_device(dialog.vm, dev_name)
  75. dialog.blk_manager.update()
  76. else:
  77. # umount/LUKS remove do not trigger udev event on underlying device,
  78. # so trigger it manually - to publish back as available device
  79. subprocess.call(["sudo", "udevadm", "trigger", "--action=change",
  80. "--subsystem-match=block",
  81. "--sysname-match=%s" % dev_name.split(":")[1]])
  82. with dialog.blk_manager.blk_lock:
  83. dialog.blk_manager.update()
  84. def fill_appvms_list(dialog):
  85. dialog.appvm_combobox.clear()
  86. dialog.appvm_combobox.addItem("dom0")
  87. dialog.appvm_combobox.setCurrentIndex(0) #current selected is null ""
  88. for vm in dialog.qvm_collection.values():
  89. if vm.is_appvm() and vm.internal:
  90. continue
  91. if vm.is_template() and vm.installed_by_rpm:
  92. continue
  93. if vm.is_running() and vm.qid != 0:
  94. dialog.appvm_combobox.addItem(vm.name)
  95. def fill_devs_list(dialog):
  96. dialog.dev_combobox.clear()
  97. dialog.dev_combobox.addItem("None")
  98. dialog.blk_manager.blk_lock.acquire()
  99. for a in dialog.blk_manager.attached_devs:
  100. if dialog.blk_manager.attached_devs[a]['attached_to']['vm'] == dialog.vm.name :
  101. att = a + " " + unicode(dialog.blk_manager.attached_devs[a]['size']) + " " + dialog.blk_manager.attached_devs[a]['desc']
  102. dialog.dev_combobox.addItem(att, QVariant(a))
  103. for a in dialog.blk_manager.free_devs:
  104. att = a + " " + unicode(dialog.blk_manager.free_devs[a]['size']) + " " + dialog.blk_manager.free_devs[a]['desc']
  105. dialog.dev_combobox.addItem(att, QVariant(a))
  106. dialog.blk_manager.blk_lock.release()
  107. dialog.dev_combobox.setCurrentIndex(0) #current selected is null ""
  108. dialog.prev_dev_idx = 0
  109. dialog.dir_line_edit.clear()
  110. enable_dir_line_edit(dialog, True)
  111. def enable_dir_line_edit(dialog, boolean):
  112. dialog.dir_line_edit.setEnabled(boolean)
  113. dialog.select_path_button.setEnabled(boolean)
  114. def update_device_appvm_enabled(dialog, idx):
  115. # Only one of those can be used
  116. dialog.dev_combobox.setEnabled(dialog.appvm_combobox.currentIndex() == 0)
  117. dialog.appvm_combobox.setEnabled(dialog.dev_combobox.currentIndex() == 0)
  118. def dev_combobox_activated(dialog, idx):
  119. if idx == dialog.prev_dev_idx: #nothing has changed
  120. return
  121. #there was a change
  122. dialog.dir_line_edit.setText("")
  123. # umount old device if any
  124. if dialog.dev_mount_path != None:
  125. dialog.dev_mount_path = umount_device(dialog.dev_mount_path)
  126. if dialog.dev_mount_path != None:
  127. dialog.dev_combobox.setCurrentIndex(dialog.prev_dev_idx)
  128. return
  129. else:
  130. detach_device(dialog,
  131. str(dialog.dev_combobox.itemData(dialog.prev_dev_idx).toString()))
  132. # then attach new one
  133. if dialog.dev_combobox.currentIndex() != 0: #An existing device chosen
  134. dev_name = str(dialog.dev_combobox.itemData(idx).toString())
  135. if dev_name.startswith(dialog.vm.name+":"):
  136. # originally attached to dom0
  137. dev_path = "/dev/"+dev_name.split(":")[1]
  138. else:
  139. try:
  140. with dialog.blk_manager.blk_lock:
  141. if dev_name in dialog.blk_manager.free_devs:
  142. #attach it to dom0, then treat it as an attached device
  143. dialog.blk_manager.attach_device(dialog.vm, dev_name)
  144. dialog.blk_manager.update()
  145. if dev_name in dialog.blk_manager.attached_devs: #is attached to dom0
  146. assert dialog.blk_manager.attached_devs[dev_name]['attached_to']['vm'] == dialog.vm.name
  147. dev_path = "/dev/" + dialog.blk_manager.attached_devs[dev_name]['attached_to']['frontend']
  148. else:
  149. raise QubesException("device not attached?!")
  150. except QubesException as ex:
  151. QMessageBox.warning (None, "Error attaching selected device!",
  152. "<b>Could not attach {0}.</b><br><br>ERROR: {1}".format(dev_name, ex))
  153. dialog.dev_combobox.setCurrentIndex(0) #if couldn't mount - set current device to "None"
  154. dialog.prev_dev_idx = 0
  155. return
  156. #check if device mounted
  157. dialog.dev_mount_path = check_if_mounted(dev_path)
  158. if dialog.dev_mount_path == None:
  159. dialog.dev_mount_path = mount_device(dev_path)
  160. if dialog.dev_mount_path == None:
  161. dialog.dev_combobox.setCurrentIndex(0) #if couldn't mount - set current device to "None"
  162. dialog.prev_dev_idx = 0
  163. detach_device(dialog,
  164. str(dialog.dev_combobox.itemData(idx).toString()))
  165. return
  166. dialog.prev_dev_idx = idx
  167. if hasattr(dialog, 'selected_vms'):
  168. # backup window
  169. if dialog.dev_mount_path != None:
  170. # Initialize path with root of mounted device
  171. dialog.dir_line_edit.setText(dialog.dev_mount_path)
  172. dialog.select_dir_page.emit(SIGNAL("completeChanged()"))
  173. def get_path_for_vm(vm, service_name):
  174. if not vm:
  175. return None
  176. proc = vm.run("QUBESRPC %s dom0" % service_name, passio_popen=True)
  177. proc.stdin.close()
  178. untrusted_path = proc.stdout.readline(path_max_len)
  179. if len(untrusted_path) == 0:
  180. return None
  181. if path_re.match(untrusted_path):
  182. assert '../' not in untrusted_path
  183. assert '\0' not in untrusted_path
  184. return untrusted_path.strip()
  185. else:
  186. return None
  187. def select_path_button_clicked(dialog, select_file = False):
  188. backup_location = str(dialog.dir_line_edit.text())
  189. file_dialog = QFileDialog()
  190. file_dialog.setReadOnly(True)
  191. if select_file:
  192. file_dialog_function = file_dialog.getOpenFileName
  193. else:
  194. file_dialog_function = file_dialog.getExistingDirectory
  195. new_appvm = None
  196. new_path = None
  197. if dialog.appvm_combobox.currentIndex() != 0: #An existing appvm chosen
  198. new_appvm = str(dialog.appvm_combobox.currentText())
  199. vm = dialog.qvm_collection.get_vm_by_name(new_appvm)
  200. if vm:
  201. new_path = get_path_for_vm(vm, "qubes.SelectFile" if select_file
  202. else "qubes.SelectDirectory")
  203. elif dialog.dev_mount_path != None:
  204. new_path = file_dialog_function(dialog, "Select backup location.", dialog.dev_mount_path)
  205. else:
  206. new_path = file_dialog_function(dialog, "Select backup location.",
  207. backup_location if backup_location
  208. else '/')
  209. if new_path != None:
  210. new_path = unicode(new_path)
  211. if os.path.basename(new_path) == 'qubes.xml':
  212. backup_location = os.path.dirname(new_path)
  213. else:
  214. backup_location = new_path
  215. dialog.dir_line_edit.setText(backup_location)
  216. if (new_path or new_appvm) and len(backup_location) > 0:
  217. dialog.select_dir_page.emit(SIGNAL("completeChanged()"))
  218. def simulate_long_lasting_proces(period, progress_callback):
  219. for i in range(period):
  220. progress_callback((i*100)/period)
  221. time.sleep(1)
  222. progress_callback(100)
  223. return 0