2017-07-17 15:53:14 +02:00
|
|
|
#
|
|
|
|
# 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
|
2019-10-18 06:02:04 +02:00
|
|
|
import os
|
2017-07-17 15:53:14 +02:00
|
|
|
import sys
|
|
|
|
|
2017-07-20 20:51:30 +02:00
|
|
|
from qubesadmin.backup.restore import BackupRestore
|
2019-09-10 14:41:28 +02:00
|
|
|
from qubesadmin.backup.dispvm import RestoreInDisposableVM
|
2017-07-17 15:53:14 +02:00
|
|
|
import qubesadmin.exc
|
|
|
|
import qubesadmin.tools
|
|
|
|
import qubesadmin.utils
|
|
|
|
|
|
|
|
parser = qubesadmin.tools.QubesArgumentParser()
|
|
|
|
|
2019-09-10 14:41:28 +02:00
|
|
|
# WARNING:
|
|
|
|
# When adding options, update/verify also
|
|
|
|
# qubeadmin.restore.dispvm.RestoreInDisposableVM.arguments
|
|
|
|
#
|
2017-07-17 15:53:14 +02:00
|
|
|
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("-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")
|
|
|
|
|
2017-07-29 04:49:14 +02:00
|
|
|
parser.add_argument("--ignore-size-limit", action="store_true",
|
|
|
|
dest="ignore_size_limit", default=False,
|
|
|
|
help="Ignore size limit calculated from backup metadata")
|
|
|
|
|
2019-09-08 02:39:01 +02:00
|
|
|
parser.add_argument("--compression-filter", "-Z", action="store",
|
|
|
|
dest="compression",
|
|
|
|
help="Force specific compression filter program, "
|
|
|
|
"instead of the one from the backup header")
|
|
|
|
|
2017-07-17 15:53:14 +02:00
|
|
|
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")
|
|
|
|
|
2019-10-18 06:02:04 +02:00
|
|
|
parser.add_argument('--auto-close', action="store_true",
|
|
|
|
help="Auto-close restore window and display log on the stdout "
|
|
|
|
"(applies to --paranoid-mode)")
|
|
|
|
|
2019-09-08 05:08:24 +02:00
|
|
|
parser.add_argument("--location-is-service", action="store_true",
|
|
|
|
help="Interpret backup location as a qrexec service name,"
|
|
|
|
"possibly with an argument separated by +.Requires -d option.")
|
|
|
|
|
2019-09-10 14:41:28 +02:00
|
|
|
parser.add_argument('--paranoid-mode', '--plan-b', action="store_true",
|
|
|
|
help="Isolate restore process in a DispVM, defend against untrusted backup;"
|
|
|
|
"implies --skip-dom0-home")
|
|
|
|
|
2017-07-17 15:53:14 +02:00
|
|
|
parser.add_argument('backup_location', action='store',
|
|
|
|
help="Backup directory name, or command to pipe from")
|
|
|
|
|
2017-07-19 23:52:11 +02:00
|
|
|
parser.add_argument('vms', nargs='*', action='store', default=[],
|
2017-07-17 15:53:14 +02:00
|
|
|
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():
|
2017-07-20 20:51:30 +02:00
|
|
|
assert isinstance(vm_info, BackupRestore.VMToRestore)
|
|
|
|
if BackupRestore.VMToRestore.EXCLUDED in \
|
2017-07-17 15:53:14 +02:00
|
|
|
vm_info.problems:
|
|
|
|
continue
|
2017-07-20 20:51:30 +02:00
|
|
|
if BackupRestore.VMToRestore.MISSING_TEMPLATE in \
|
2017-07-17 15:53:14 +02:00
|
|
|
vm_info.problems:
|
|
|
|
there_are_missing_templates = True
|
2017-07-20 20:51:30 +02:00
|
|
|
if BackupRestore.VMToRestore.MISSING_NETVM in \
|
2017-07-17 15:53:14 +02:00
|
|
|
vm_info.problems:
|
|
|
|
there_are_missing_netvms = True
|
2017-07-20 20:51:30 +02:00
|
|
|
if BackupRestore.VMToRestore.ALREADY_EXISTS in \
|
2017-07-17 15:53:14 +02:00
|
|
|
vm_info.problems:
|
|
|
|
there_are_conflicting_vms = True
|
2017-07-20 20:51:30 +02:00
|
|
|
if BackupRestore.Dom0ToRestore.USERNAME_MISMATCH in \
|
2017-07-17 15:53:14 +02:00
|
|
|
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).")
|
|
|
|
|
2017-12-07 03:31:30 +01:00
|
|
|
if args.verify_only:
|
|
|
|
app.log.info("The above VM archive(s) will be verified.")
|
|
|
|
app.log.info("Existing VMs will NOT be removed or altered.")
|
|
|
|
else:
|
|
|
|
app.log.info("The above VMs will be copied and added to your system.")
|
|
|
|
app.log.info("Exisiting VMs will NOT be removed.")
|
2017-07-17 15:53:14 +02:00
|
|
|
|
|
|
|
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.")
|
2019-03-07 03:07:42 +01:00
|
|
|
if args.skip_broken:
|
2017-07-17 15:53:14 +02:00
|
|
|
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 "
|
2017-07-20 20:51:30 +02:00
|
|
|
"on missing TemplateVMs will have default value "
|
2017-07-20 14:11:55 +02:00
|
|
|
"assigned.")
|
2017-07-17 15:53:14 +02:00
|
|
|
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.")
|
2019-03-07 03:07:42 +01:00
|
|
|
if args.skip_broken:
|
2017-07-17 15:53:14 +02:00
|
|
|
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 "
|
2017-07-20 14:11:55 +02:00
|
|
|
"on missing NetVMs will have default value assigned.")
|
2017-07-17 15:53:14 +02:00
|
|
|
else:
|
|
|
|
raise qubesadmin.exc.QubesException(
|
|
|
|
"INTERNAL ERROR! Please report this to the Qubes OS team!")
|
|
|
|
|
2017-12-11 16:16:53 +01:00
|
|
|
if 'dom0' in restore_info.keys() and args.dom0_home \
|
|
|
|
and not args.verify_only:
|
2017-07-17 15:53:14 +02:00
|
|
|
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.")
|
2019-03-07 03:07:42 +01:00
|
|
|
app.log.warning("Continuing as directed.")
|
2018-02-02 04:21:24 +01:00
|
|
|
app.log.warning("NOTE: The archived dom0 home directory "
|
|
|
|
"will be restored to a new directory "
|
|
|
|
"'home-restore-<current-time>' "
|
|
|
|
"created inside the dom0 home directory. Restored "
|
|
|
|
"files should be copied or moved out of the new "
|
|
|
|
"directory before using them.")
|
2017-07-17 15:53:14 +02:00
|
|
|
|
2019-10-18 06:02:04 +02:00
|
|
|
|
|
|
|
def print_backup_log(backup_log):
|
|
|
|
"""Print a log on stdout, coloring it red if it's a terminal"""
|
|
|
|
if os.isatty(sys.stdout.fileno()):
|
|
|
|
sys.stdout.write('\033[0;31m')
|
|
|
|
sys.stdout.flush()
|
|
|
|
sys.stdout.buffer.write(backup_log)
|
|
|
|
if os.isatty(sys.stdout.fileno()):
|
|
|
|
sys.stdout.write('\033[0m')
|
|
|
|
sys.stdout.flush()
|
|
|
|
|
|
|
|
|
2017-07-20 14:11:55 +02:00
|
|
|
def main(args=None, app=None):
|
2017-07-17 15:53:14 +02:00
|
|
|
'''Main function of qvm-backup-restore'''
|
|
|
|
# pylint: disable=too-many-return-statements
|
2017-07-20 14:11:55 +02:00
|
|
|
args = parser.parse_args(args, app=app)
|
2017-07-17 15:53:14 +02:00
|
|
|
|
|
|
|
appvm = None
|
|
|
|
if args.appvm:
|
|
|
|
try:
|
|
|
|
appvm = args.app.domains[args.appvm]
|
|
|
|
except KeyError:
|
|
|
|
parser.error('no such domain: {!r}'.format(args.appvm))
|
|
|
|
|
2019-09-08 05:08:24 +02:00
|
|
|
if args.location_is_service and not args.appvm:
|
|
|
|
parser.error('--location-is-service option requires -d')
|
|
|
|
|
2019-09-10 14:41:28 +02:00
|
|
|
if args.paranoid_mode:
|
|
|
|
args.dom0_home = False
|
|
|
|
args.app.log.info("Starting restore process in a DisposableVM...")
|
|
|
|
args.app.log.info("When operation completes, close its window "
|
|
|
|
"manually.")
|
|
|
|
restore_in_dispvm = RestoreInDisposableVM(args.app, args)
|
|
|
|
try:
|
2019-10-18 06:02:04 +02:00
|
|
|
backup_log = restore_in_dispvm.run()
|
|
|
|
if args.auto_close:
|
|
|
|
print_backup_log(backup_log)
|
|
|
|
except qubesadmin.exc.BackupRestoreError as e:
|
|
|
|
if e.backup_log is not None:
|
|
|
|
print_backup_log(e.backup_log)
|
|
|
|
parser.error_runtime(str(e))
|
|
|
|
return 1
|
2019-09-10 14:41:28 +02:00
|
|
|
except qubesadmin.exc.QubesException as e:
|
|
|
|
parser.error_runtime(str(e))
|
|
|
|
return 1
|
|
|
|
return
|
|
|
|
|
2017-07-17 15:53:14 +02:00
|
|
|
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:
|
2017-07-20 20:51:30 +02:00
|
|
|
backup = BackupRestore(args.app, args.backup_location,
|
2019-09-08 05:08:24 +02:00
|
|
|
appvm, passphrase, location_is_service=args.location_is_service,
|
2019-09-08 02:39:01 +02:00
|
|
|
force_compression_filter=args.compression)
|
2017-07-17 15:53:14 +02:00
|
|
|
except qubesadmin.exc.QubesException as e:
|
|
|
|
parser.error_runtime(str(e))
|
|
|
|
# unreachable - error_runtime will raise SystemExit
|
|
|
|
return 1
|
|
|
|
|
2018-02-26 05:10:05 +01:00
|
|
|
backup.options.use_default_template = args.ignore_missing
|
|
|
|
backup.options.use_default_netvm = args.ignore_missing
|
2018-02-23 03:09:04 +01:00
|
|
|
backup.options.rename_conflicting = args.rename_conflicting
|
|
|
|
backup.options.dom0_home = args.dom0_home
|
|
|
|
backup.options.ignore_username_mismatch = args.ignore_username_mismatch
|
|
|
|
backup.options.ignore_size_limit = args.ignore_size_limit
|
|
|
|
backup.options.exclude = args.exclude
|
|
|
|
backup.options.verify_only = args.verify_only
|
2017-07-17 15:53:14 +02:00
|
|
|
|
|
|
|
restore_info = None
|
|
|
|
try:
|
|
|
|
restore_info = backup.get_restore_info()
|
|
|
|
except qubesadmin.exc.QubesException as e:
|
|
|
|
parser.error_runtime(str(e))
|
|
|
|
|
2017-07-17 23:34:03 +02:00
|
|
|
if args.vms:
|
2017-09-04 03:07:03 +02:00
|
|
|
# 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]
|
2017-07-17 23:34:03 +02:00
|
|
|
restore_info = backup.restore_info_verify(restore_info)
|
|
|
|
|
2017-07-17 15:53:14 +02:00
|
|
|
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":
|
2019-10-05 21:57:48 +02:00
|
|
|
sys.exit(0)
|
2017-07-17 15:53:14 +02:00
|
|
|
|
|
|
|
try:
|
|
|
|
backup.restore_do(restore_info)
|
|
|
|
except qubesadmin.exc.QubesException as e:
|
|
|
|
parser.error_runtime(str(e))
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|