Эх сурвалжийг харах

tools: add qvm-backup-restore

Frontend tool for backup restore code.

Fixes QubesOS/qubes-issues#1214
Marek Marczykowski-Górecki 6 жил өмнө
parent
commit
f0151d73b3

+ 0 - 4
doc/manpages/qvm-backup-restore.rst

@@ -53,10 +53,6 @@ Options
 
    Restore VMs that are already present on the host under different names
 
-.. option:: --force-root
-
-    Force to run, even with root privileges
-
 .. option:: --replace-template=REPLACE_TEMPLATE
 
     Restore VMs using another template, syntax:

+ 259 - 0
qubesadmin/tools/qvm_backup_restore.py

@@ -0,0 +1,259 @@
+#
+# 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
+
+import qubesadmin.backup
+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("-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, qubesadmin.backup.BackupRestore.VMToRestore)
+        if qubesadmin.backup.BackupRestore.VMToRestore.EXCLUDED in \
+                vm_info.problems:
+            continue
+        if qubesadmin.backup.BackupRestore.VMToRestore.MISSING_TEMPLATE in \
+                vm_info.problems:
+            there_are_missing_templates = True
+        if qubesadmin.backup.BackupRestore.VMToRestore.MISSING_NETVM in \
+                vm_info.problems:
+            there_are_missing_netvms = True
+        if qubesadmin.backup.BackupRestore.VMToRestore.ALREADY_EXISTS in \
+                vm_info.problems:
+            there_are_conflicting_vms = True
+        if qubesadmin.backup.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 NOT be restored.")
+        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 NOT be restored.")
+        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):
+    '''Main function of qvm-backup-restore'''
+    # pylint: disable=too-many-return-statements
+    args = parser.parse_args(args)
+
+    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 = qubesadmin.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.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))
+
+    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()