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/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) 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/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 05674aca..b745d9fb 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 @@ -478,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 @@ -529,6 +531,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 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',