123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273 |
- #
- # The Qubes OS Project, http://www.qubes-os.org
- #
- # Copyright (C) 2016 Marek Marczykowski-Górecki
- # <marmarek@invisiblethingslab.com>
- #
- # This program is free software; you can redistribute it and/or modify
- # it under the terms of the GNU Lesser General Public License as published by
- # the Free Software Foundation; either version 2.1 of the License, or
- # (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU Lesser General Public License for more details.
- #
- # You should have received a copy of the GNU Lesser General Public License along
- # with this program; if not, write to the Free Software Foundation, Inc.,
- # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- '''Console frontend for backup restore code'''
- import getpass
- import sys
- from qubesadmin.backup.restore import BackupRestore
- import qubesadmin.exc
- import qubesadmin.tools
- import qubesadmin.utils
- parser = qubesadmin.tools.QubesArgumentParser()
- parser.add_argument("--verify-only", action="store_true",
- dest="verify_only", default=False,
- help="Verify backup integrity without restoring any "
- "data")
- parser.add_argument("--skip-broken", action="store_true", dest="skip_broken",
- default=False,
- help="Do not restore VMs that have missing TemplateVMs "
- "or NetVMs")
- parser.add_argument("--ignore-missing", action="store_true",
- dest="ignore_missing", default=False,
- help="Restore VMs even if their associated TemplateVMs "
- "and NetVMs are missing")
- parser.add_argument("--skip-conflicting", action="store_true",
- dest="skip_conflicting", default=False,
- help="Do not restore VMs that are already present on "
- "the host")
- parser.add_argument("--rename-conflicting", action="store_true",
- dest="rename_conflicting", default=False,
- help="Restore VMs that are already present on the host "
- "under different names")
- parser.add_argument("--replace-template", action="append",
- dest="replace_template", default=[],
- help="Restore VMs using another TemplateVM; syntax: "
- "old-template-name:new-template-name (may be "
- "repeated)")
- parser.add_argument("-x", "--exclude", action="append", dest="exclude",
- default=[],
- help="Skip restore of specified VM (may be repeated)")
- parser.add_argument("--skip-dom0-home", action="store_false", dest="dom0_home",
- default=True,
- help="Do not restore dom0 user home directory")
- parser.add_argument("--ignore-username-mismatch", action="store_true",
- dest="ignore_username_mismatch", default=False,
- help="Ignore dom0 username mismatch when restoring home "
- "directory")
- parser.add_argument("--ignore-size-limit", action="store_true",
- dest="ignore_size_limit", default=False,
- help="Ignore size limit calculated from backup metadata")
- parser.add_argument("-d", "--dest-vm", action="store", dest="appvm",
- help="Specify VM containing the backup to be restored")
- parser.add_argument("-p", "--passphrase-file", action="store",
- dest="pass_file", default=None,
- help="Read passphrase from file, or use '-' to read from stdin")
- parser.add_argument('backup_location', action='store',
- help="Backup directory name, or command to pipe from")
- parser.add_argument('vms', nargs='*', action='store', default=[],
- help='Restore only those VMs')
- def handle_broken(app, args, restore_info):
- '''Display information about problems with VMs selected for resetore'''
- there_are_conflicting_vms = False
- there_are_missing_templates = False
- there_are_missing_netvms = False
- dom0_username_mismatch = False
- for vm_info in restore_info.values():
- assert isinstance(vm_info, BackupRestore.VMToRestore)
- if BackupRestore.VMToRestore.EXCLUDED in \
- vm_info.problems:
- continue
- if BackupRestore.VMToRestore.MISSING_TEMPLATE in \
- vm_info.problems:
- there_are_missing_templates = True
- if BackupRestore.VMToRestore.MISSING_NETVM in \
- vm_info.problems:
- there_are_missing_netvms = True
- if BackupRestore.VMToRestore.ALREADY_EXISTS in \
- vm_info.problems:
- there_are_conflicting_vms = True
- if BackupRestore.Dom0ToRestore.USERNAME_MISMATCH in \
- vm_info.problems:
- dom0_username_mismatch = True
- if there_are_conflicting_vms:
- app.log.error(
- "*** There are VMs with conflicting names on the host! ***")
- if args.skip_conflicting:
- app.log.error(
- "Those VMs will not be restored. "
- "The host VMs will NOT be overwritten.")
- else:
- raise qubesadmin.exc.QubesException(
- "Remove VMs with conflicting names from the host "
- "before proceeding.\n"
- "Or use --skip-conflicting to restore only those VMs that "
- "do not exist on the host.\n"
- "Or use --rename-conflicting to restore those VMs under "
- "modified names (with numbers at the end).")
- app.log.info("The above VMs will be copied and added to your system.")
- app.log.info("Exisiting VMs will NOT be removed.")
- if there_are_missing_templates:
- app.log.warning("*** One or more TemplateVMs are missing on the "
- "host! ***")
- if not (args.skip_broken or args.ignore_missing):
- raise qubesadmin.exc.QubesException(
- "Install them before proceeding with the restore."
- "Or pass: --skip-broken or --ignore-missing.")
- elif args.skip_broken:
- app.log.warning("Skipping broken entries: VMs that depend on "
- "missing TemplateVMs will NOT be restored.")
- elif args.ignore_missing:
- app.log.warning("Ignoring missing entries: VMs that depend "
- "on missing TemplateVMs will have default value "
- "assigned.")
- else:
- raise qubesadmin.exc.QubesException(
- "INTERNAL ERROR! Please report this to the Qubes OS team!")
- if there_are_missing_netvms:
- app.log.warning("*** One or more NetVMs are missing on the "
- "host! ***")
- if not (args.skip_broken or args.ignore_missing):
- raise qubesadmin.exc.QubesException(
- "Install them before proceeding with the restore."
- "Or pass: --skip-broken or --ignore-missing.")
- elif args.skip_broken:
- app.log.warning("Skipping broken entries: VMs that depend on "
- "missing NetVMs will NOT be restored.")
- elif args.ignore_missing:
- app.log.warning("Ignoring missing entries: VMs that depend "
- "on missing NetVMs will have default value assigned.")
- else:
- raise qubesadmin.exc.QubesException(
- "INTERNAL ERROR! Please report this to the Qubes OS team!")
- if 'dom0' in restore_info.keys() and args.dom0_home:
- if dom0_username_mismatch:
- app.log.warning("*** Dom0 username mismatch! This can break "
- "some settings! ***")
- if not args.ignore_username_mismatch:
- raise qubesadmin.exc.QubesException(
- "Skip restoring the dom0 home directory "
- "(--skip-dom0-home), or pass "
- "--ignore-username-mismatch to continue anyway.")
- else:
- app.log.warning("Continuing as directed.")
- app.log.warning("NOTE: Before restoring the dom0 home directory, "
- "a new directory named "
- "'home-pre-restore-<current-time>' will be "
- "created inside the dom0 home directory. If any "
- "restored files conflict with existing files, "
- "the existing files will be moved to this new "
- "directory.")
- def main(args=None, app=None):
- '''Main function of qvm-backup-restore'''
- # pylint: disable=too-many-return-statements
- args = parser.parse_args(args, app=app)
- appvm = None
- if args.appvm:
- try:
- appvm = args.app.domains[args.appvm]
- except KeyError:
- parser.error('no such domain: {!r}'.format(args.appvm))
- if args.pass_file is not None:
- pass_f = open(args.pass_file) if args.pass_file != "-" else sys.stdin
- passphrase = pass_f.readline().rstrip()
- if pass_f is not sys.stdin:
- pass_f.close()
- else:
- passphrase = getpass.getpass("Please enter the passphrase to verify "
- "and (if encrypted) decrypt the backup: ")
- args.app.log.info("Checking backup content...")
- try:
- backup = BackupRestore(args.app, args.backup_location,
- appvm, passphrase)
- except qubesadmin.exc.QubesException as e:
- parser.error_runtime(str(e))
- # unreachable - error_runtime will raise SystemExit
- return 1
- if args.ignore_missing:
- backup.options.use_default_template = True
- backup.options.use_default_netvm = True
- if args.replace_template:
- backup.options.replace_template = args.replace_template
- if args.rename_conflicting:
- backup.options.rename_conflicting = True
- if not args.dom0_home:
- backup.options.dom0_home = False
- if args.ignore_username_mismatch:
- backup.options.ignore_username_mismatch = True
- if args.ignore_size_limit:
- backup.options.ignore_size_limit = True
- if args.exclude:
- backup.options.exclude = args.exclude
- if args.verify_only:
- backup.options.verify_only = True
- restore_info = None
- try:
- restore_info = backup.get_restore_info()
- except qubesadmin.exc.QubesException as e:
- parser.error_runtime(str(e))
- if args.vms:
- # use original name here, not renamed
- backup.options.exclude += [vm_info.vm.name
- for vm_info in restore_info.values()
- if vm_info.vm.name not in args.vms]
- restore_info = backup.restore_info_verify(restore_info)
- print(backup.get_restore_summary(restore_info))
- try:
- handle_broken(args.app, args, restore_info)
- except qubesadmin.exc.QubesException as e:
- parser.error_runtime(str(e))
- if args.pass_file is None:
- if input("Do you want to proceed? [y/N] ").upper() != "Y":
- exit(0)
- try:
- backup.restore_do(restore_info)
- except qubesadmin.exc.QubesException as e:
- parser.error_runtime(str(e))
- if __name__ == '__main__':
- main()
|