qvm-backup-restore 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. #!/usr/bin/python2.6
  2. #
  3. # The Qubes OS Project, http://www.qubes-os.org
  4. #
  5. # Copyright (C) 2010 Joanna Rutkowska <joanna@invisiblethingslab.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. from qubes.qubes import QubesVmCollection
  23. from qubes.qubes import QubesException
  24. from qubes.qubes import qubes_store_filename
  25. from qubes.qubes import qubes_base_dir
  26. from qubes.qubes import qubes_templates_dir
  27. from optparse import OptionParser
  28. import os
  29. import time
  30. import subprocess
  31. import sys
  32. def size_to_human (size):
  33. if size < 1024:
  34. return str (size);
  35. elif size < 1024*1024:
  36. return str(round(size/1024.0,1)) + ' KiB'
  37. elif size < 1024*1024*1024:
  38. return str(round(size/(1024.0*1024),1)) + ' MiB'
  39. else:
  40. return str(round(size/(1024.0*1024*1024),1)) + ' GiB'
  41. fields = {
  42. "qid": {"func": "vm.qid"},
  43. "name": {"func": "('=>' if backup_collection.get_default_template_vm() is not None\
  44. and vm.qid == backup_collection.get_default_template_vm().qid else '')\
  45. + ('[' if vm.is_templete() else '')\
  46. + ('{' if vm.is_netvm() else '')\
  47. + vm.name \
  48. + (']' if vm.is_templete() else '')\
  49. + ('}' if vm.is_netvm() else '')"},
  50. "type": {"func": "'Tpl' if vm.is_templete() else \
  51. (' Net' if vm.is_netvm() else 'App')"},
  52. "updbl" : {"func": "'Yes' if vm.is_updateable() else ''"},
  53. "template": {"func": "'n/a' if vm.is_templete() or vm.is_netvm() else\
  54. backup_collection[vm.template_vm.qid].name"},
  55. "netvm": {"func": "'n/a' if vm.is_netvm() else\
  56. ('*' if vm.uses_default_netvm else '') +\
  57. backup_collection[vm.netvm_vm.qid].name\
  58. if vm.netvm_vm is not None else '-'"},
  59. "label" : {"func" : "vm.label.name"},
  60. }
  61. def is_vm_included_in_backup (backup_dir, vm):
  62. if vm.qid == 0:
  63. # Dom0 is not included, obviously
  64. return False
  65. backup_vm_dir_path = vm.dir_path.replace (qubes_base_dir, backup_dir)
  66. if os.path.exists (backup_vm_dir_path):
  67. return True
  68. else:
  69. return False
  70. def restore_vm_file (backup_dir, file_path):
  71. backup_file_path = file_path.replace (qubes_base_dir, backup_dir)
  72. #print "cp -rp {0} {1}".format (backup_file_path, file_path)
  73. # We prefer to use Linux's cp, because it nicely handles sparse files
  74. retcode = subprocess.call (["cp", "-p", backup_file_path, file_path])
  75. if retcode != 0:
  76. print "*** Error while copying file {0} to {1}".format(backup_file_path, file_path)
  77. exit (1)
  78. def restore_vm_dir (backup_dir, src_dir, dst_dir):
  79. backup_src_dir = src_dir.replace (qubes_base_dir, backup_dir)
  80. # We prefer to use Linux's cp, because it nicely handles sparse files
  81. retcode = subprocess.call (["cp", "-rp", backup_src_dir, dst_dir])
  82. if retcode != 0:
  83. print "*** Error while copying file {0} to {1}".format(backup_src_dir, dest_dir)
  84. exit (1)
  85. def main():
  86. usage = "usage: %prog [options] <backup-dir>"
  87. parser = OptionParser (usage)
  88. parser.add_option ("--skip-broken", action="store_true", dest="skip_broken", default=False,
  89. help="Do not restore VMs that have missing templates or netvms")
  90. parser.add_option ("--ignore-missing", action="store_true", dest="ignore_missing", default=False,
  91. help="Ignore missing templates or netvms, restore VMs anyway")
  92. (options, args) = parser.parse_args ()
  93. if (len (args) != 1):
  94. print "You must specify the backup directory (e.g. /mnt/backup/qubes-2010-12-01-235959)"
  95. exit (0)
  96. backup_dir = args[0]
  97. if not os.path.exists (backup_dir):
  98. print "The backup directory doesn't exist!"
  99. exit(1)
  100. backup_collection = QubesVmCollection(store_filename = backup_dir + "/qubes.xml")
  101. backup_collection.lock_db_for_reading()
  102. backup_collection.load()
  103. host_collection = QubesVmCollection()
  104. host_collection.lock_db_for_writing()
  105. host_collection.load()
  106. backup_vms_list = [vm for vm in backup_collection.values()]
  107. host_vms_list = [vm for vm in host_collection.values()]
  108. vms_to_restore = []
  109. fields_to_display = ["name", "type", "template", "updbl", "netvm", "label" ]
  110. # First calculate the maximum width of each field we want to display
  111. total_width = 0;
  112. for f in fields_to_display:
  113. fields[f]["max_width"] = len(f)
  114. for vm in backup_vms_list:
  115. l = len(str(eval(fields[f]["func"])))
  116. if l > fields[f]["max_width"]:
  117. fields[f]["max_width"] = l
  118. total_width += fields[f]["max_width"]
  119. print
  120. print "The following VMs are included in the backup:"
  121. print
  122. # Display the header
  123. s = ""
  124. for f in fields_to_display:
  125. fmt="{{0:-^{0}}}-+".format(fields[f]["max_width"] + 1)
  126. s += fmt.format('-')
  127. print s
  128. s = ""
  129. for f in fields_to_display:
  130. fmt="{{0:>{0}}} |".format(fields[f]["max_width"] + 1)
  131. s += fmt.format(f)
  132. print s
  133. s = ""
  134. for f in fields_to_display:
  135. fmt="{{0:-^{0}}}-+".format(fields[f]["max_width"] + 1)
  136. s += fmt.format('-')
  137. print s
  138. there_are_conflicting_vms = False
  139. there_are_missing_templates = False
  140. there_are_missing_netvms = False
  141. # ... and the actual data
  142. for vm in backup_vms_list:
  143. if is_vm_included_in_backup (backup_dir, vm):
  144. s = ""
  145. good_to_go = True
  146. for f in fields_to_display:
  147. fmt="{{0:>{0}}} |".format(fields[f]["max_width"] + 1)
  148. s += fmt.format(eval(fields[f]["func"]))
  149. if host_collection.get_vm_by_name (vm.name) is not None:
  150. s += " <-- A VM with the same name already exists on the host!"
  151. there_are_conflicting_vms = True
  152. good_to_go = False
  153. if vm.is_appvm():
  154. templatevm_name = vm.template_vm.name
  155. template_vm_on_host = host_collection.get_vm_by_name (templatevm_name)
  156. # No template on the host?
  157. if not ((template_vm_on_host is not None) and template_vm_on_host.is_templete):
  158. # Maybe the (custom) template is in the backup?
  159. template_vm_on_backup = backup_collection.get_vm_by_name (templatevm_name)
  160. if not ((template_vm_on_backup is not None) and template_vm_on_backup.is_templete):
  161. s += " <-- No matching template on the host or in the backup found!"
  162. there_are_missing_templates = True
  163. good_to_go = False if not (options.ignore_missing) else True
  164. if not vm.is_netvm() and vm.netvm_vm is not None:
  165. netvm_name = vm.netvm_vm.name
  166. netvm_vm_on_host = host_collection.get_vm_by_name (netvm_name)
  167. # No netv, on the host?
  168. if not ((netvm_vm_on_host is not None) and netvm_vm_on_host.is_netvm):
  169. # Maybe the (custom) netvm is in the backup?
  170. netvm_vm_on_backup = backup_collection.get_vm_by_name (netvm_name)
  171. if not ((netvm_vm_on_backup is not None) and netvm_vm_on_backup.is_netvm):
  172. s += " <-- No matching netvm on the host found!"
  173. there_are_missing_netvms = True
  174. good_to_go = False if not (options.ignore_missing) else True
  175. print s
  176. if good_to_go:
  177. vms_to_restore.append (vm)
  178. print
  179. if there_are_conflicting_vms:
  180. print "*** There VMs with conflicting names on the host! ***"
  181. print "Remove VMs with conflicting names from the host before proceeding."
  182. print "You can use 'qvm-remove <vmname>' command for this."
  183. print "Be careful! Think twice before typing qvm-remove!!!"
  184. exit (1)
  185. print "The above VMs will be copied and added to your system."
  186. print "Exisiting VMs will not be removed."
  187. if there_are_missing_templates:
  188. print "*** One or more template VM is missing on the host! ***"
  189. if not (options.skip_broken or options.ignore_missing):
  190. print "Install it first, before proceeding with backup restore."
  191. print "Or pass: --skip-broken or --ignore-missing switch."
  192. exit (1)
  193. elif options.skip_broken:
  194. print "... VMs that depend on it will not be restored (--skip-broken used)"
  195. elif options.ignore_missing:
  196. print "... VMs that depend on it be restored anyway (--ignore-missing used)"
  197. else:
  198. print "INTERNAL ERROR?!"
  199. exit (1)
  200. if there_are_missing_netvms:
  201. print "*** One or more network VM is missing on the host! ***"
  202. if not (options.skip_broken or options.ignore_missing):
  203. print "Install it first, before proceeding with backup restore."
  204. print "Or pass: --skip_broken or --ignore_missing switch."
  205. exit (1)
  206. elif options.skip_broken:
  207. print "... VMs that depend on it will not be restored (--skip-broken used)"
  208. elif options.ignore_missing:
  209. print "... VMs that depend on it be restored anyway (--ignore-missing used)"
  210. else:
  211. print "INTERNAL ERROR?!"
  212. exit (1)
  213. prompt = raw_input ("Do you want to proceed? [y/N] ")
  214. if not (prompt == "y" or prompt == "Y"):
  215. exit (0)
  216. for vm in vms_to_restore:
  217. print "-> Restoring: {0} ...".format(vm.name)
  218. retcode = subprocess.call (["mkdir", "-p", vm.dir_path])
  219. if retcode != 0:
  220. print ("*** Cannot create directory: {0}?!".format(dest_dir))
  221. print ("Skiping...")
  222. continue
  223. if vm.is_appvm():
  224. restore_vm_file (backup_dir, vm.private_img)
  225. restore_vm_file (backup_dir, vm.icon_path)
  226. restore_vm_file (backup_dir, vm.conf_file)
  227. if vm.is_updateable():
  228. restore_vm_file (backup_dir, vm.rootcow_img)
  229. elif vm.is_templete():
  230. restore_vm_dir (backup_dir, vm.dir_path, qubes_templates_dir);
  231. else:
  232. print "ERROR: VM '{0}', type='{1}': unsupported VM type!".format(vm.name, vm.type)
  233. # Add templates...
  234. for vm in [ vm for vm in vms_to_restore if vm.is_templete()]:
  235. print "-> Adding Template VM {0}...".format(vm.name)
  236. updateable = vm.updateable
  237. vm = host_collection.add_new_templatevm(vm.name,
  238. conf_file=vm.conf_file,
  239. dir_path=vm.dir_path,
  240. installed_by_rpm=False)
  241. vm.updateable = updateable
  242. try:
  243. vm.verify_files()
  244. except QubesException as err:
  245. print "ERROR: {0}".format(err)
  246. print "*** Skiping VM: {0}".vm.name
  247. host_collection.pop(vm.qid)
  248. continue
  249. try:
  250. vm.add_to_xen_storage()
  251. except (IOError, OSError) as err:
  252. print "ERROR: {0}".format(err)
  253. print "*** Skiping VM: {0}".vm.name
  254. host_collection.pop(vm.qid)
  255. continue
  256. # ... then appvms...
  257. for vm in [ vm for vm in vms_to_restore if vm.is_appvm()]:
  258. print "-> Adding AppVM {0}...".format(vm.name)
  259. template_vm = host_collection.get_vm_by_name(vm.template_vm.name)
  260. if not vm.uses_default_netvm:
  261. uses_default_netvm = False
  262. netvm_vm = host_collection.get_vm_by_name (vm.netvm_vm.name) if vm.netvm_vm is not None else None
  263. else:
  264. uses_default_netvm = True
  265. updateable = vm.updateable
  266. vm = host_collection.add_new_appvm(vm.name, template_vm,
  267. conf_file=vm.conf_file,
  268. dir_path=vm.dir_path,
  269. label=vm.label)
  270. vm.updateable = updateable
  271. if not uses_default_netvm:
  272. vm.uses_default_netvm = False
  273. vm.netvm_vm = netvm_vm
  274. vm.create_appmenus(verbose=True)
  275. try:
  276. vm.verify_files()
  277. except QubesException as err:
  278. print "ERROR: {0}".format(err)
  279. print "*** Skiping VM: {0}".vm.name
  280. host_collection.pop(vm.qid)
  281. try:
  282. vm.add_to_xen_storage()
  283. except (IOError, OSError) as err:
  284. print "ERROR: {0}".format(err)
  285. print "*** Skiping VM: {0}".vm.name
  286. host_collection.pop(vm.qid)
  287. backup_collection.unlock_db()
  288. host_collection.save()
  289. host_collection.unlock_db()
  290. print "-> Done."
  291. main()