From 6f87f310dbd4b355f30fb4ff09b3aecf74759d02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Sun, 15 Sep 2019 03:38:37 +0200 Subject: [PATCH 1/4] Add support for 'tag-created-vm-with' feature When a VM with 'tag-created-vm-with' feature set creates a VM (using Admin API), that VM will get all the tags listed in the feature. Multiple tags can be separated with spaces. This will be useful to tag VMs created during paranoid mode backup restore. QubesOS/qubes-issues#5310 --- qubes/ext/admin.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/qubes/ext/admin.py b/qubes/ext/admin.py index ae052eea..c58c7bca 100644 --- a/qubes/ext/admin.py +++ b/qubes/ext/admin.py @@ -142,3 +142,12 @@ class AdminExtension(qubes.ext.Extension): if hasattr(self, 'policy_cache'): self.policy_cache.cleanup() del self.policy_cache + + @qubes.ext.handler('domain-tag-add:created-by-*') + def on_tag_add(self, vm, event, tag, **kwargs): + '''Add extra tags based on creators 'tag-created-vm-with' feature''' + # pylint: disable=unused-argument,no-self-use + created_by = vm.app.domains[tag.partition('created-by-')[2]] + tag_with = created_by.features.get('tag-created-vm-with', '') + for tag_with_single in tag_with.split(): + vm.tags.add(tag_with_single) From 2cdba05c99688229eaeced52503d43db955f9006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Sun, 15 Sep 2019 03:41:33 +0200 Subject: [PATCH 2/4] Add an extension preventing starting a VM while it's being restored Do not allow starting a VM while the restoring management VM has still control over it. Specifically, that restoring VM will not be able to start just restored VM. QubesOS/qubes-issues#5310 --- qubes/ext/backup_restore.py | 39 +++++++++++++++++++++++++++++++++++++ rpm_spec/core-dom0.spec.in | 1 + setup.py | 2 ++ 3 files changed, 42 insertions(+) create mode 100644 qubes/ext/backup_restore.py diff --git a/qubes/ext/backup_restore.py b/qubes/ext/backup_restore.py new file mode 100644 index 00000000..97746efe --- /dev/null +++ b/qubes/ext/backup_restore.py @@ -0,0 +1,39 @@ +# -*- encoding: utf8 -*- +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2019 Marek Marczykowski-Górecki +# +# +# This library 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 library 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 library; if not, see . + +""" +Backup restore related functionality. Specifically: + - prevent starting a domain currently being restored +""" + +import qubes.api +import qubes.ext +import qubes.vm.adminvm + + +class BackupRestoreExtension(qubes.ext.Extension): + # pylint: disable=too-few-public-methods + @qubes.ext.handler('domain-pre-start') + def on_domain_pre_start(self, vm, event, **kwargs): + """Prevent starting a VM during restore""" + # pylint: disable=unused-argument,no-self-use + if 'backup-restore-in-progress' in vm.tags: + raise qubes.exc.QubesVMError( + vm, 'Restore of this domain in progress, cannot start') diff --git a/rpm_spec/core-dom0.spec.in b/rpm_spec/core-dom0.spec.in index 05674aca..ac0fcabd 100644 --- a/rpm_spec/core-dom0.spec.in +++ b/rpm_spec/core-dom0.spec.in @@ -416,6 +416,7 @@ done %{python3_sitelib}/qubes/ext/__pycache__/* %{python3_sitelib}/qubes/ext/__init__.py %{python3_sitelib}/qubes/ext/admin.py +%{python3_sitelib}/qubes/ext/backup_restore.py %{python3_sitelib}/qubes/ext/block.py %{python3_sitelib}/qubes/ext/core_features.py %{python3_sitelib}/qubes/ext/gui.py diff --git a/setup.py b/setup.py index 1a606caf..e6fdd8e1 100644 --- a/setup.py +++ b/setup.py @@ -61,6 +61,8 @@ if __name__ == '__main__': ], 'qubes.ext': [ 'qubes.ext.admin = qubes.ext.admin:AdminExtension', + 'qubes.ext.backup_restore = ' + 'qubes.ext.backup_restore:BackupRestoreExtension', 'qubes.ext.core_features = qubes.ext.core_features:CoreFeatures', 'qubes.ext.gui = qubes.ext.gui:GUI', 'qubes.ext.audio = qubes.ext.audio:AUDIO', From e73320533f940bc85f85eddb41811209d8b0786d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Sun, 15 Sep 2019 03:32:53 +0200 Subject: [PATCH 3/4] Add policy for paranoid mode backup restore Policy allows a VM with 'backup-restore-mgmt' tag to create VMs, and then manage VMs with 'backup-restore-in-progress' tag (which is added by AdminExtension, based on 'tag-created-vm-with' feature). VM with 'backup-restore-mgmt' tag can also call qubes.RestoreById service to a VM with 'backup-restore-storage' tag. This service allows to retrieve backup archive. QubesOS/qubes-issues#5310 --- Makefile | 2 ++ .../85-admin-backup-restore.policy | 26 +++++++++++++++++++ rpm_spec/core-dom0.spec.in | 1 + 3 files changed, 29 insertions(+) create mode 100644 qubes-rpc-policy/85-admin-backup-restore.policy diff --git a/Makefile b/Makefile index 8e4d77af..92a4aac6 100644 --- a/Makefile +++ b/Makefile @@ -174,6 +174,8 @@ endif mkdir -p $(DESTDIR)/usr/libexec/qubes install -m 0644 qubes-rpc-policy/90-default.policy \ $(DESTDIR)/etc/qubes/policy.d/90-default.policy + install -m 0644 qubes-rpc-policy/85-admin-backup-restore.policy \ + $(DESTDIR)/etc/qubes/policy.d/85-admin-backup-restore.policy cp qubes-rpc/qubes.FeaturesRequest $(DESTDIR)/etc/qubes-rpc/ cp qubes-rpc/qubes.GetDate $(DESTDIR)/etc/qubes-rpc/ cp qubes-rpc/qubes.GetRandomizedTime $(DESTDIR)/etc/qubes-rpc/ diff --git a/qubes-rpc-policy/85-admin-backup-restore.policy b/qubes-rpc-policy/85-admin-backup-restore.policy new file mode 100644 index 00000000..3ef1447f --- /dev/null +++ b/qubes-rpc-policy/85-admin-backup-restore.policy @@ -0,0 +1,26 @@ +## File format: +## service-name|* +argument|* source destination action [options] + +## Allow selected DisposableVM perform "paranoid backup restore" +admin.vm.Create.AppVM * @tag:backup-restore-mgmt dom0 allow target=dom0 +admin.vm.Create.StandaloneVM * @tag:backup-restore-mgmt dom0 allow target=dom0 +admin.vm.Create.TemplateVM * @tag:backup-restore-mgmt dom0 allow target=dom0 +admin.vm.List * @tag:backup-restore-mgmt dom0 allow target=dom0 +## Allow checking some basic info about all the VMs, to propose conflicts resolution +admin.vm.List * @tag:backup-restore-mgmt @anyvm allow target=dom0 +admin.vm.property.Get +provides_network @tag:backup-restore-mgmt @anyvm allow target=dom0 +admin.vm.property.Get +template_for_dispvms @tag:backup-restore-mgmt @anyvm allow target=dom0 + +## Allow it to configure just created qubes +admin.vm.feature.Set * @tag:backup-restore-mgmt @tag:backup-restore-in-progress allow target=dom0 +admin.vm.firewall.Set * @tag:backup-restore-mgmt @tag:backup-restore-in-progress allow target=dom0 +admin.vm.property.Set * @tag:backup-restore-mgmt @tag:backup-restore-in-progress allow target=dom0 +admin.vm.tag.Set * @tag:backup-restore-mgmt @tag:backup-restore-in-progress allow target=dom0 +admin.vm.volume.Import * @tag:backup-restore-mgmt @tag:backup-restore-in-progress allow target=dom0 +admin.vm.volume.Info * @tag:backup-restore-mgmt @tag:backup-restore-in-progress allow target=dom0 +admin.vm.volume.List * @tag:backup-restore-mgmt @tag:backup-restore-in-progress allow target=dom0 +admin.vm.volume.Set.revisions_to_keep * @tag:backup-restore-mgmt @tag:backup-restore-in-progress allow target=dom0 + +## And finally, allow it to retrieve the actual backup +qubes.RestoreById * @tag:backup-restore-mgmt @tag:backup-restore-storage allow + diff --git a/rpm_spec/core-dom0.spec.in b/rpm_spec/core-dom0.spec.in index ac0fcabd..4a8dec2f 100644 --- a/rpm_spec/core-dom0.spec.in +++ b/rpm_spec/core-dom0.spec.in @@ -530,6 +530,7 @@ done /etc/xen/scripts/block-snapshot /etc/xen/scripts/block-origin /etc/xen/scripts/vif-route-qubes +%attr(0664,root,qubes) %config(noreplace) /etc/qubes/policy.d/85-admin-backup-restore.policy %attr(0664,root,qubes) %config(noreplace) /etc/qubes/policy.d/90-admin-default.policy %attr(0664,root,qubes) %config(noreplace) /etc/qubes/policy.d/90-default.policy %attr(0664,root,qubes) %config(noreplace) /etc/qubes/policy.d/include/admin-global-ro From 117724a7725796119fc8a7e2acad0707bdf7c50a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Fri, 18 Oct 2019 06:10:30 +0200 Subject: [PATCH 4/4] tests: paranoid backup restore QubesOS/qubes-issues#5310 --- qubes/tests/__init__.py | 1 + qubes/tests/integ/backupdispvm.py | 131 ++++++++++++++++++++++++++++++ rpm_spec/core-dom0.spec.in | 1 + 3 files changed, 133 insertions(+) create mode 100644 qubes/tests/integ/backupdispvm.py diff --git a/qubes/tests/__init__.py b/qubes/tests/__init__.py index 07e78ef1..32af1540 100644 --- a/qubes/tests/__init__.py +++ b/qubes/tests/__init__.py @@ -1451,6 +1451,7 @@ def load_tests(loader, tests, pattern): # pylint: disable=unused-argument 'qubes.tests.integ.salt', 'qubes.tests.integ.backup', 'qubes.tests.integ.backupcompatibility', + 'qubes.tests.integ.backupdispvm', # external modules 'qubes.tests.extra', diff --git a/qubes/tests/integ/backupdispvm.py b/qubes/tests/integ/backupdispvm.py new file mode 100644 index 00000000..e574ef43 --- /dev/null +++ b/qubes/tests/integ/backupdispvm.py @@ -0,0 +1,131 @@ +# +# The Qubes OS Project, https://www.qubes-os.org/ +# +# Copyright (C) 2019 +# Marek Marczykowski-Górecki +# +# This library 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 library 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 library; if not, see . +# + +import hashlib +import logging +import multiprocessing + +import os +import shutil + +import sys + +import asyncio +import tempfile + +import unittest.mock + +import qubes +import qubes.backup +import qubes.storage.lvm +import qubes.tests +import qubes.tests.integ.backup +import qubes.tests.storage_lvm +import qubes.vm +import qubes.vm.appvm +import qubes.vm.templatevm +import qubes.vm.qubesvm + +try: + import qubesadmin.exc + from qubesadmin.backup.dispvm import RestoreInDisposableVM + restore_available = True +except ImportError: + restore_available = False + + +class TC_00_RestoreInDispVM(qubes.tests.integ.backup.BackupTestsMixin): + def setUp(self): + if not restore_available: + self.skipTest('qubesadmin module not installed') + super(TC_00_RestoreInDispVM, self).setUp() + self.mgmt_vm = self.app.add_new_vm( + qubes.vm.appvm.AppVM, + label='red', + name=self.make_vm_name('mgmtvm'), + template=self.template + ) + self.loop.run_until_complete(self.mgmt_vm.create_on_disk()) + self.mgmt_vm.template_for_dispvms = True + self.app.management_dispvm = self.mgmt_vm + self.backupvm = self.app.add_new_vm( + qubes.vm.appvm.AppVM, + label='red', + name=self.make_vm_name('backupvm'), + template=self.template + ) + self.loop.run_until_complete(self.backupvm.create_on_disk()) + + def restore_backup(self, source=None, appvm=None, options=None, + expect_errors=None, manipulate_restore_info=None, + passphrase='qubes'): + args = unittest.mock.Mock(spec=['app', 'appvm', 'backup_location', 'vms']) + args.app = qubesadmin.Qubes() + args.appvm = appvm + args.backup_location = source + # XXX FIXME + args.app.blind_mode = True + args.vms = [] + args.auto_close = True + with tempfile.NamedTemporaryFile() as pass_file: + pass_file.file.write(passphrase.encode()) + pass_file.file.flush() + args.pass_file = pass_file.name + restore_in_dispvm = RestoreInDisposableVM(args.app, args) + try: + backup_log = self.loop.run_until_complete( + self.loop.run_in_executor(None, restore_in_dispvm.run)) + except qubesadmin.exc.BackupRestoreError as e: + self.fail(str(e) + ' backup log: ' + e.backup_log.decode()) + self.app.log.debug(backup_log.decode()) + + def test_000_basic_backup(self): + self.loop.run_until_complete(self.backupvm.start()) + self.loop.run_until_complete(self.backupvm.run_for_stdio( + "mkdir '/var/tmp/backup directory'")) + vms = self.create_backup_vms() + try: + orig_hashes = self.vm_checksum(vms) + vms_info = self.get_vms_info(vms) + self.make_backup(vms, + target='/var/tmp/backup directory', + target_vm=self.backupvm) + self.remove_vms(reversed(vms)) + finally: + del vms + (backup_path, _) = self.loop.run_until_complete( + self.backupvm.run_for_stdio("ls /var/tmp/backup*/qubes-backup*")) + backup_path = backup_path.decode().strip() + self.restore_backup(source=backup_path, + appvm=self.backupvm.name) + self.assertCorrectlyRestored(vms_info, orig_hashes) + + +def create_testcases_for_templates(): + return qubes.tests.create_testcases_for_templates('TC_10_RestoreInDispVM', + TC_00_RestoreInDispVM, qubes.tests.SystemTestCase, + module=sys.modules[__name__]) + +def load_tests(loader, tests, pattern): + tests.addTests(loader.loadTestsFromNames( + create_testcases_for_templates())) + return tests + +qubes.tests.maybe_create_testcases_on_import(create_testcases_for_templates) diff --git a/rpm_spec/core-dom0.spec.in b/rpm_spec/core-dom0.spec.in index 4a8dec2f..b745d9fb 100644 --- a/rpm_spec/core-dom0.spec.in +++ b/rpm_spec/core-dom0.spec.in @@ -479,6 +479,7 @@ done %{python3_sitelib}/qubes/tests/integ/__init__.py %{python3_sitelib}/qubes/tests/integ/backup.py %{python3_sitelib}/qubes/tests/integ/backupcompatibility.py +%{python3_sitelib}/qubes/tests/integ/backupdispvm.py %{python3_sitelib}/qubes/tests/integ/basic.py %{python3_sitelib}/qubes/tests/integ/devices_block.py %{python3_sitelib}/qubes/tests/integ/devices_pci.py