From 302533ff41d5b474188744b3f477e22fedd68ce9 Mon Sep 17 00:00:00 2001 From: Ali Mirjamali Date: Thu, 15 May 2025 12:46:09 +0330 Subject: [PATCH] Improve `qvm-backup-restore --paranoid-mode` Before openning a Shell window in the DispVM, check for availability of `core-admin-client` in the DisposableVM and show error message if not available resolves: https://github.com/QubesOS/qubes-issues/issues/9962 --- doc/manpages/qvm-backup-restore.rst | 2 ++ qubesadmin/backup/dispvm.py | 17 +++++++++++ qubesadmin/tests/backup/dispvm.py | 39 ++++++++++++++++++++++++++ qubesadmin/tools/qvm_backup_restore.py | 3 -- 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/doc/manpages/qvm-backup-restore.rst b/doc/manpages/qvm-backup-restore.rst index 3ecb891e..9fb23c2d 100644 --- a/doc/manpages/qvm-backup-restore.rst +++ b/doc/manpages/qvm-backup-restore.rst @@ -101,6 +101,8 @@ Options - dom0 home directory (desktop environment settings) - PCI devices assignments + This operation requires `qubes-core-admin-client` package in the DisposableVM + .. option:: --auto-close When running with --paranoid-mode (see above), automatically close restore diff --git a/qubesadmin/backup/dispvm.py b/qubesadmin/backup/dispvm.py index 15a45678..c26c0596 100644 --- a/qubesadmin/backup/dispvm.py +++ b/qubesadmin/backup/dispvm.py @@ -276,10 +276,27 @@ def run(self): lock = qubesadmin.utils.LockFile(LOCKFILE, True) lock.acquire() try: + self.app.log.info("Starting restore process in a DisposableVM...") self.create_dispvm() self.clear_old_tags() self.register_backup_source() self.dispvm.start() + try: + self.app.log.debug( + "Checking for existence of qubes-core-admin-client" + ) + self.dispvm.run("command -v qvm-backup-restore") + except subprocess.CalledProcessError: + raise qubesadmin.exc.QubesException( + 'qvm-backup-restore tool ' + 'missing in {} template, install qubes-core-admin-client ' + 'package there'.format( + getattr(self.dispvm.template, + 'template', + self.dispvm.template).name) + ) + self.app.log.info("When operation completes, close its window " + "manually.") self.dispvm.run_service_for_stdio('qubes.WaitForSession') if self.args.pass_file: self.args.pass_file = self.transfer_pass_file( diff --git a/qubesadmin/tests/backup/dispvm.py b/qubesadmin/tests/backup/dispvm.py index bf8cc608..9fd70c69 100644 --- a/qubesadmin/tests/backup/dispvm.py +++ b/qubesadmin/tests/backup/dispvm.py @@ -330,6 +330,7 @@ def test_100_run(self): self.assertEqual(len(getattr(obj, m).mock_calls), 1) self.assertEqual(obj.dispvm.mock_calls, [ call.start(), + call.run('command -v qvm-backup-restore'), call.run_service_for_stdio('qubes.WaitForSession'), call.tags.add('backup-restore-mgmt'), call.run_with_args('terminal', 'qvm-backup-restore', 'args', @@ -368,6 +369,7 @@ def test_101_run_pass_file(self): self.assertEqual(len(getattr(obj, m).mock_calls), 1) self.assertEqual(obj.dispvm.mock_calls, [ call.start(), + call.run('command -v qvm-backup-restore'), call.run_service_for_stdio('qubes.WaitForSession'), call.tags.add('backup-restore-mgmt'), call.run_with_args('terminal', 'qvm-backup-restore', 'args', @@ -405,6 +407,7 @@ def test_102_run_error(self): self.assertEqual(len(getattr(obj, m).mock_calls), 1) self.assertEqual(obj.dispvm.mock_calls, [ call.start(), + call.run('command -v qvm-backup-restore'), call.run_service_for_stdio('qubes.WaitForSession'), call.tags.add('backup-restore-mgmt'), call.run_with_args('terminal', 'qvm-backup-restore', 'args', @@ -414,3 +417,39 @@ def test_102_run_error(self): call.kill() ]) obj.transfer_pass_file.assert_not_called() + + def test_103_missing_package(self): + self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = ( + b'0\0dom0 class=AdminVM state=Running\n' + b'fedora-25 class=TemplateVM state=Halted\n' + ) + args = unittest.mock.Mock(backup_location='/backup/path', + pass_file=None, + appvm=None) + obj = RestoreInDisposableVM(self.app, args) + methods = ['create_dispvm', 'clear_old_tags', 'register_backup_source', + 'finalize_tags'] + for m in methods: + setattr(obj, m, unittest.mock.Mock()) + obj.transfer_pass_file = unittest.mock.Mock() + obj.dispvm = unittest.mock.Mock() + obj.dispvm.run = unittest.mock.Mock() + obj.dispvm.run.side_effect = subprocess.CalledProcessError( + '1', + 'command -v qvm-backup-restore', + ) + with tempfile.NamedTemporaryFile() as tmp: + with unittest.mock.patch('qubesadmin.backup.dispvm.LOCKFILE', + tmp.name): + with self.assertRaises(qubesadmin.exc.QubesException): + obj.run() + # pylint: disable=no-member + for m in methods: + self.assertEqual(len(getattr(obj, m).mock_calls), 1) + self.assertEqual(obj.dispvm.mock_calls, [ + call.start(), + call.run('command -v qvm-backup-restore'), + call.tags.discard('backup-restore-mgmt'), + call.kill() + ]) + obj.transfer_pass_file.assert_not_called() diff --git a/qubesadmin/tools/qvm_backup_restore.py b/qubesadmin/tools/qvm_backup_restore.py index ceb0e24e..7070eb02 100644 --- a/qubesadmin/tools/qvm_backup_restore.py +++ b/qubesadmin/tools/qvm_backup_restore.py @@ -240,9 +240,6 @@ def main(args=None, app=None): 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: backup_log = restore_in_dispvm.run()