qvm_backup_restore.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  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 sys
  23. import qubesadmin.backup
  24. import qubesadmin.exc
  25. import qubesadmin.tools
  26. import qubesadmin.utils
  27. parser = qubesadmin.tools.QubesArgumentParser()
  28. parser.add_argument("--verify-only", action="store_true",
  29. dest="verify_only", default=False,
  30. help="Verify backup integrity without restoring any "
  31. "data")
  32. parser.add_argument("--skip-broken", action="store_true", dest="skip_broken",
  33. default=False,
  34. help="Do not restore VMs that have missing TemplateVMs "
  35. "or NetVMs")
  36. parser.add_argument("--ignore-missing", action="store_true",
  37. dest="ignore_missing", default=False,
  38. help="Restore VMs even if their associated TemplateVMs "
  39. "and NetVMs are missing")
  40. parser.add_argument("--skip-conflicting", action="store_true",
  41. dest="skip_conflicting", default=False,
  42. help="Do not restore VMs that are already present on "
  43. "the host")
  44. parser.add_argument("--rename-conflicting", action="store_true",
  45. dest="rename_conflicting", default=False,
  46. help="Restore VMs that are already present on the host "
  47. "under different names")
  48. parser.add_argument("--replace-template", action="append",
  49. dest="replace_template", default=[],
  50. help="Restore VMs using another TemplateVM; syntax: "
  51. "old-template-name:new-template-name (may be "
  52. "repeated)")
  53. parser.add_argument("-x", "--exclude", action="append", dest="exclude",
  54. default=[],
  55. help="Skip restore of specified VM (may be repeated)")
  56. parser.add_argument("--skip-dom0-home", action="store_false", dest="dom0_home",
  57. default=True,
  58. help="Do not restore dom0 user home directory")
  59. parser.add_argument("--ignore-username-mismatch", action="store_true",
  60. dest="ignore_username_mismatch", default=False,
  61. help="Ignore dom0 username mismatch when restoring home "
  62. "directory")
  63. parser.add_argument("-d", "--dest-vm", action="store", dest="appvm",
  64. help="Specify VM containing the backup to be restored")
  65. parser.add_argument("-p", "--passphrase-file", action="store",
  66. dest="pass_file", default=None,
  67. help="Read passphrase from file, or use '-' to read from stdin")
  68. parser.add_argument('backup_location', action='store',
  69. help="Backup directory name, or command to pipe from")
  70. parser.add_argument('vms', nargs='*', action='store', default=[],
  71. help='Restore only those VMs')
  72. def handle_broken(app, args, restore_info):
  73. '''Display information about problems with VMs selected for resetore'''
  74. there_are_conflicting_vms = False
  75. there_are_missing_templates = False
  76. there_are_missing_netvms = False
  77. dom0_username_mismatch = False
  78. for vm_info in restore_info.values():
  79. assert isinstance(vm_info, qubesadmin.backup.BackupRestore.VMToRestore)
  80. if qubesadmin.backup.BackupRestore.VMToRestore.EXCLUDED in \
  81. vm_info.problems:
  82. continue
  83. if qubesadmin.backup.BackupRestore.VMToRestore.MISSING_TEMPLATE in \
  84. vm_info.problems:
  85. there_are_missing_templates = True
  86. if qubesadmin.backup.BackupRestore.VMToRestore.MISSING_NETVM in \
  87. vm_info.problems:
  88. there_are_missing_netvms = True
  89. if qubesadmin.backup.BackupRestore.VMToRestore.ALREADY_EXISTS in \
  90. vm_info.problems:
  91. there_are_conflicting_vms = True
  92. if qubesadmin.backup.BackupRestore.Dom0ToRestore.USERNAME_MISMATCH in \
  93. vm_info.problems:
  94. dom0_username_mismatch = True
  95. if there_are_conflicting_vms:
  96. app.log.error(
  97. "*** There are VMs with conflicting names on the host! ***")
  98. if args.skip_conflicting:
  99. app.log.error(
  100. "Those VMs will not be restored. "
  101. "The host VMs will NOT be overwritten.")
  102. else:
  103. raise qubesadmin.exc.QubesException(
  104. "Remove VMs with conflicting names from the host "
  105. "before proceeding.\n"
  106. "Or use --skip-conflicting to restore only those VMs that "
  107. "do not exist on the host.\n"
  108. "Or use --rename-conflicting to restore those VMs under "
  109. "modified names (with numbers at the end).")
  110. app.log.info("The above VMs will be copied and added to your system.")
  111. app.log.info("Exisiting VMs will NOT be removed.")
  112. if there_are_missing_templates:
  113. app.log.warning("*** One or more TemplateVMs are missing on the "
  114. "host! ***")
  115. if not (args.skip_broken or args.ignore_missing):
  116. raise qubesadmin.exc.QubesException(
  117. "Install them before proceeding with the restore."
  118. "Or pass: --skip-broken or --ignore-missing.")
  119. elif args.skip_broken:
  120. app.log.warning("Skipping broken entries: VMs that depend on "
  121. "missing TemplateVMs will NOT be restored.")
  122. elif args.ignore_missing:
  123. app.log.warning("Ignoring missing entries: VMs that depend "
  124. "on missing TemplateVMs will have default value "\
  125. "assigned.")
  126. else:
  127. raise qubesadmin.exc.QubesException(
  128. "INTERNAL ERROR! Please report this to the Qubes OS team!")
  129. if there_are_missing_netvms:
  130. app.log.warning("*** One or more NetVMs are missing on the "
  131. "host! ***")
  132. if not (args.skip_broken or args.ignore_missing):
  133. raise qubesadmin.exc.QubesException(
  134. "Install them before proceeding with the restore."
  135. "Or pass: --skip-broken or --ignore-missing.")
  136. elif args.skip_broken:
  137. app.log.warning("Skipping broken entries: VMs that depend on "
  138. "missing NetVMs will NOT be restored.")
  139. elif args.ignore_missing:
  140. app.log.warning("Ignoring missing entries: VMs that depend "
  141. "on missing NetVMs will have default value assigned.")
  142. else:
  143. raise qubesadmin.exc.QubesException(
  144. "INTERNAL ERROR! Please report this to the Qubes OS team!")
  145. if 'dom0' in restore_info.keys() and args.dom0_home:
  146. if dom0_username_mismatch:
  147. app.log.warning("*** Dom0 username mismatch! This can break "
  148. "some settings! ***")
  149. if not args.ignore_username_mismatch:
  150. raise qubesadmin.exc.QubesException(
  151. "Skip restoring the dom0 home directory "
  152. "(--skip-dom0-home), or pass "
  153. "--ignore-username-mismatch to continue anyway.")
  154. else:
  155. app.log.warning("Continuing as directed.")
  156. app.log.warning("NOTE: Before restoring the dom0 home directory, "
  157. "a new directory named "
  158. "'home-pre-restore-<current-time>' will be "
  159. "created inside the dom0 home directory. If any "
  160. "restored files conflict with existing files, "
  161. "the existing files will be moved to this new "
  162. "directory.")
  163. def main(args=None, app=None):
  164. '''Main function of qvm-backup-restore'''
  165. # pylint: disable=too-many-return-statements
  166. args = parser.parse_args(args, app=app)
  167. appvm = None
  168. if args.appvm:
  169. try:
  170. appvm = args.app.domains[args.appvm]
  171. except KeyError:
  172. parser.error('no such domain: {!r}'.format(args.appvm))
  173. if args.pass_file is not None:
  174. pass_f = open(args.pass_file) if args.pass_file != "-" else sys.stdin
  175. passphrase = pass_f.readline().rstrip()
  176. if pass_f is not sys.stdin:
  177. pass_f.close()
  178. else:
  179. passphrase = getpass.getpass("Please enter the passphrase to verify "
  180. "and (if encrypted) decrypt the backup: ")
  181. args.app.log.info("Checking backup content...")
  182. try:
  183. backup = qubesadmin.backup.BackupRestore(args.app, args.backup_location,
  184. appvm, passphrase)
  185. except qubesadmin.exc.QubesException as e:
  186. parser.error_runtime(str(e))
  187. # unreachable - error_runtime will raise SystemExit
  188. return 1
  189. if args.ignore_missing:
  190. backup.options.use_default_template = True
  191. backup.options.use_default_netvm = True
  192. if args.replace_template:
  193. backup.options.replace_template = args.replace_template
  194. if args.rename_conflicting:
  195. backup.options.rename_conflicting = True
  196. if not args.dom0_home:
  197. backup.options.dom0_home = False
  198. if args.ignore_username_mismatch:
  199. backup.options.ignore_username_mismatch = True
  200. if args.exclude:
  201. backup.options.exclude = args.exclude
  202. if args.verify_only:
  203. backup.options.verify_only = True
  204. restore_info = None
  205. try:
  206. restore_info = backup.get_restore_info()
  207. except qubesadmin.exc.QubesException as e:
  208. parser.error_runtime(str(e))
  209. if args.vms:
  210. backup.options.exclude += [vm.name for vm in restore_info.values()
  211. if vm.name not in args.vms]
  212. restore_info = backup.restore_info_verify(restore_info)
  213. print(backup.get_restore_summary(restore_info))
  214. try:
  215. handle_broken(args.app, args, restore_info)
  216. except qubesadmin.exc.QubesException as e:
  217. parser.error_runtime(str(e))
  218. if args.pass_file is None:
  219. if input("Do you want to proceed? [y/N] ").upper() != "Y":
  220. exit(0)
  221. try:
  222. backup.restore_do(restore_info)
  223. except qubesadmin.exc.QubesException as e:
  224. parser.error_runtime(str(e))
  225. if __name__ == '__main__':
  226. main()