qvm_backup_restore.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. #
  2. # The Qubes OS Project, http://www.qubes-os.org
  3. #
  4. # Copyright (C) 2016 Marek Marczykowski-Górecki
  5. # <marmarek@invisiblethingslab.com>
  6. #
  7. # This program is free software; you can redistribute it and/or modify
  8. # it under the terms of the GNU Lesser General Public License as published by
  9. # the Free Software Foundation; either version 2.1 of the License, or
  10. # (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 Lesser General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU Lesser General Public License along
  18. # with this program; if not, write to the Free Software Foundation, Inc.,
  19. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  20. '''Console frontend for backup restore code'''
  21. import getpass
  22. import os
  23. import sys
  24. from qubesadmin.backup.restore import BackupRestore
  25. from qubesadmin.backup.dispvm import RestoreInDisposableVM
  26. import qubesadmin.exc
  27. import qubesadmin.tools
  28. import qubesadmin.utils
  29. parser = qubesadmin.tools.QubesArgumentParser()
  30. # WARNING:
  31. # When adding options, update/verify also
  32. # qubeadmin.restore.dispvm.RestoreInDisposableVM.arguments
  33. #
  34. parser.add_argument("--verify-only", action="store_true",
  35. dest="verify_only", default=False,
  36. help="Verify backup integrity without restoring any "
  37. "data")
  38. parser.add_argument("--skip-broken", action="store_true", dest="skip_broken",
  39. default=False,
  40. help="Do not restore VMs that have missing TemplateVMs "
  41. "or NetVMs")
  42. parser.add_argument("--ignore-missing", action="store_true",
  43. dest="ignore_missing", default=False,
  44. help="Restore VMs even if their associated TemplateVMs "
  45. "and NetVMs are missing")
  46. parser.add_argument("--skip-conflicting", action="store_true",
  47. dest="skip_conflicting", default=False,
  48. help="Do not restore VMs that are already present on "
  49. "the host")
  50. parser.add_argument("--rename-conflicting", action="store_true",
  51. dest="rename_conflicting", default=False,
  52. help="Restore VMs that are already present on the host "
  53. "under different names")
  54. parser.add_argument("-x", "--exclude", action="append", dest="exclude",
  55. default=[],
  56. help="Skip restore of specified VM (may be repeated)")
  57. parser.add_argument("--skip-dom0-home", action="store_false", dest="dom0_home",
  58. default=True,
  59. help="Do not restore dom0 user home directory")
  60. parser.add_argument("--ignore-username-mismatch", action="store_true",
  61. dest="ignore_username_mismatch", default=False,
  62. help="Ignore dom0 username mismatch when restoring home "
  63. "directory")
  64. parser.add_argument("--ignore-size-limit", action="store_true",
  65. dest="ignore_size_limit", default=False,
  66. help="Ignore size limit calculated from backup metadata")
  67. parser.add_argument("--compression-filter", "-Z", action="store",
  68. dest="compression",
  69. help="Force specific compression filter program, "
  70. "instead of the one from the backup header")
  71. parser.add_argument("-d", "--dest-vm", action="store", dest="appvm",
  72. help="Specify VM containing the backup to be restored")
  73. parser.add_argument("-p", "--passphrase-file", action="store",
  74. dest="pass_file", default=None,
  75. help="Read passphrase from file, or use '-' to read from stdin")
  76. parser.add_argument('--auto-close', action="store_true",
  77. help="Auto-close restore window and display log on the stdout "
  78. "(applies to --paranoid-mode)")
  79. parser.add_argument("--location-is-service", action="store_true",
  80. help="Interpret backup location as a qrexec service name,"
  81. "possibly with an argument separated by +.Requires -d option.")
  82. parser.add_argument('--paranoid-mode', '--plan-b', action="store_true",
  83. help="Isolate restore process in a DispVM, defend against untrusted backup;"
  84. "implies --skip-dom0-home")
  85. parser.add_argument('backup_location', action='store',
  86. help="Backup directory name, or command to pipe from")
  87. parser.add_argument('vms', nargs='*', action='store', default=[],
  88. help='Restore only those VMs')
  89. def handle_broken(app, args, restore_info):
  90. '''Display information about problems with VMs selected for resetore'''
  91. there_are_conflicting_vms = False
  92. there_are_missing_templates = False
  93. there_are_missing_netvms = False
  94. dom0_username_mismatch = False
  95. for vm_info in restore_info.values():
  96. assert isinstance(vm_info, BackupRestore.VMToRestore)
  97. if BackupRestore.VMToRestore.EXCLUDED in \
  98. vm_info.problems:
  99. continue
  100. if BackupRestore.VMToRestore.MISSING_TEMPLATE in \
  101. vm_info.problems:
  102. there_are_missing_templates = True
  103. if BackupRestore.VMToRestore.MISSING_NETVM in \
  104. vm_info.problems:
  105. there_are_missing_netvms = True
  106. if BackupRestore.VMToRestore.ALREADY_EXISTS in \
  107. vm_info.problems:
  108. there_are_conflicting_vms = True
  109. if BackupRestore.Dom0ToRestore.USERNAME_MISMATCH in \
  110. vm_info.problems:
  111. dom0_username_mismatch = True
  112. if there_are_conflicting_vms:
  113. app.log.error(
  114. "*** There are VMs with conflicting names on the host! ***")
  115. if args.skip_conflicting:
  116. app.log.error(
  117. "Those VMs will not be restored. "
  118. "The host VMs will NOT be overwritten.")
  119. else:
  120. raise qubesadmin.exc.QubesException(
  121. "Remove VMs with conflicting names from the host "
  122. "before proceeding.\n"
  123. "Or use --skip-conflicting to restore only those VMs that "
  124. "do not exist on the host.\n"
  125. "Or use --rename-conflicting to restore those VMs under "
  126. "modified names (with numbers at the end).")
  127. if args.verify_only:
  128. app.log.info("The above VM archive(s) will be verified.")
  129. app.log.info("Existing VMs will NOT be removed or altered.")
  130. else:
  131. app.log.info("The above VMs will be copied and added to your system.")
  132. app.log.info("Exisiting VMs will NOT be removed.")
  133. if there_are_missing_templates:
  134. app.log.warning("*** One or more TemplateVMs are missing on the "
  135. "host! ***")
  136. if not (args.skip_broken or args.ignore_missing):
  137. raise qubesadmin.exc.QubesException(
  138. "Install them before proceeding with the restore."
  139. "Or pass: --skip-broken or --ignore-missing.")
  140. if args.skip_broken:
  141. app.log.warning("Skipping broken entries: VMs that depend on "
  142. "missing TemplateVMs will NOT be restored.")
  143. elif args.ignore_missing:
  144. app.log.warning("Ignoring missing entries: VMs that depend "
  145. "on missing TemplateVMs will have default value "
  146. "assigned.")
  147. else:
  148. raise qubesadmin.exc.QubesException(
  149. "INTERNAL ERROR! Please report this to the Qubes OS team!")
  150. if there_are_missing_netvms:
  151. app.log.warning("*** One or more NetVMs are missing on the "
  152. "host! ***")
  153. if not (args.skip_broken or args.ignore_missing):
  154. raise qubesadmin.exc.QubesException(
  155. "Install them before proceeding with the restore."
  156. "Or pass: --skip-broken or --ignore-missing.")
  157. if args.skip_broken:
  158. app.log.warning("Skipping broken entries: VMs that depend on "
  159. "missing NetVMs will NOT be restored.")
  160. elif args.ignore_missing:
  161. app.log.warning("Ignoring missing entries: VMs that depend "
  162. "on missing NetVMs will have default value assigned.")
  163. else:
  164. raise qubesadmin.exc.QubesException(
  165. "INTERNAL ERROR! Please report this to the Qubes OS team!")
  166. if 'dom0' in restore_info.keys() and args.dom0_home \
  167. and not args.verify_only:
  168. if dom0_username_mismatch:
  169. app.log.warning("*** Dom0 username mismatch! This can break "
  170. "some settings! ***")
  171. if not args.ignore_username_mismatch:
  172. raise qubesadmin.exc.QubesException(
  173. "Skip restoring the dom0 home directory "
  174. "(--skip-dom0-home), or pass "
  175. "--ignore-username-mismatch to continue anyway.")
  176. app.log.warning("Continuing as directed.")
  177. app.log.warning("NOTE: The archived dom0 home directory "
  178. "will be restored to a new directory "
  179. "'home-restore-<current-time>' "
  180. "created inside the dom0 home directory. Restored "
  181. "files should be copied or moved out of the new "
  182. "directory before using them.")
  183. def print_backup_log(backup_log):
  184. """Print a log on stdout, coloring it red if it's a terminal"""
  185. if os.isatty(sys.stdout.fileno()):
  186. sys.stdout.write('\033[0;31m')
  187. sys.stdout.flush()
  188. sys.stdout.buffer.write(backup_log)
  189. if os.isatty(sys.stdout.fileno()):
  190. sys.stdout.write('\033[0m')
  191. sys.stdout.flush()
  192. def main(args=None, app=None):
  193. '''Main function of qvm-backup-restore'''
  194. # pylint: disable=too-many-return-statements
  195. args = parser.parse_args(args, app=app)
  196. appvm = None
  197. if args.appvm:
  198. try:
  199. appvm = args.app.domains[args.appvm]
  200. except KeyError:
  201. parser.error('no such domain: {!r}'.format(args.appvm))
  202. if args.location_is_service and not args.appvm:
  203. parser.error('--location-is-service option requires -d')
  204. if args.paranoid_mode:
  205. args.dom0_home = False
  206. args.app.log.info("Starting restore process in a DisposableVM...")
  207. args.app.log.info("When operation completes, close its window "
  208. "manually.")
  209. restore_in_dispvm = RestoreInDisposableVM(args.app, args)
  210. try:
  211. backup_log = restore_in_dispvm.run()
  212. if args.auto_close:
  213. print_backup_log(backup_log)
  214. except qubesadmin.exc.BackupRestoreError as e:
  215. if e.backup_log is not None:
  216. print_backup_log(e.backup_log)
  217. parser.error_runtime(str(e))
  218. return 1
  219. except qubesadmin.exc.QubesException as e:
  220. parser.error_runtime(str(e))
  221. return 1
  222. return
  223. if args.pass_file is not None:
  224. pass_f = open(args.pass_file) if args.pass_file != "-" else sys.stdin
  225. passphrase = pass_f.readline().rstrip()
  226. if pass_f is not sys.stdin:
  227. pass_f.close()
  228. else:
  229. passphrase = getpass.getpass("Please enter the passphrase to verify "
  230. "and (if encrypted) decrypt the backup: ")
  231. args.app.log.info("Checking backup content...")
  232. try:
  233. backup = BackupRestore(args.app, args.backup_location,
  234. appvm, passphrase, location_is_service=args.location_is_service,
  235. force_compression_filter=args.compression)
  236. except qubesadmin.exc.QubesException as e:
  237. parser.error_runtime(str(e))
  238. # unreachable - error_runtime will raise SystemExit
  239. return 1
  240. backup.options.use_default_template = args.ignore_missing
  241. backup.options.use_default_netvm = args.ignore_missing
  242. backup.options.rename_conflicting = args.rename_conflicting
  243. backup.options.dom0_home = args.dom0_home
  244. backup.options.ignore_username_mismatch = args.ignore_username_mismatch
  245. backup.options.ignore_size_limit = args.ignore_size_limit
  246. backup.options.exclude = args.exclude
  247. backup.options.verify_only = args.verify_only
  248. restore_info = None
  249. try:
  250. restore_info = backup.get_restore_info()
  251. except qubesadmin.exc.QubesException as e:
  252. parser.error_runtime(str(e))
  253. if args.vms:
  254. # use original name here, not renamed
  255. backup.options.exclude += [vm_info.vm.name
  256. for vm_info in restore_info.values()
  257. if vm_info.vm.name not in args.vms]
  258. restore_info = backup.restore_info_verify(restore_info)
  259. print(backup.get_restore_summary(restore_info))
  260. try:
  261. handle_broken(args.app, args, restore_info)
  262. except qubesadmin.exc.QubesException as e:
  263. parser.error_runtime(str(e))
  264. if args.pass_file is None:
  265. if input("Do you want to proceed? [y/N] ").upper() != "Y":
  266. sys.exit(0)
  267. try:
  268. backup.restore_do(restore_info)
  269. except qubesadmin.exc.QubesException as e:
  270. parser.error_runtime(str(e))
  271. if __name__ == '__main__':
  272. main()