From 8e4b769865e38b4450eb2d6a4b0913d8c86b04ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Pierret=20=28fepitre=29?= Date: Mon, 25 Nov 2024 19:02:00 +0100 Subject: [PATCH 1/3] Introduce RemoteVM class --- qubes/api/admin.py | 14 +++++++-- qubes/app.py | 18 ++++++----- qubes/ext/block.py | 5 +-- qubes/tests/app.py | 32 +++++++++++++++++++ qubes/vm/__init__.py | 24 ++++++++++++++ qubes/vm/qubesvm.py | 17 ++-------- qubes/vm/remotevm.py | 74 ++++++++++++++++++++++++++++++++++++++++++++ setup.py | 1 + 8 files changed, 159 insertions(+), 26 deletions(-) create mode 100644 qubes/vm/remotevm.py diff --git a/qubes/api/admin.py b/qubes/api/admin.py index 4dcfc776b..a4249bad4 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -370,7 +370,11 @@ def _property_reset(self, dest): async def vm_volume_list(self): self.enforce(not self.arg) - volume_names = self.fire_event_for_filter(self.dest.volumes.keys()) + volume_names = ( + self.fire_event_for_filter(self.dest.volumes.keys()) + if isinstance(self.dest, qubes.vm.qubesvm.QubesVM) + else [] + ) return "".join("{}\n".format(name) for name in volume_names) @qubes.api.method( @@ -1262,7 +1266,8 @@ async def _vm_create( vm.tags.add("created-by-" + str(self.src)) try: - await vm.create_on_disk(pool=pool, pools=pools) + if isinstance(vm, qubes.vm.qubesvm.QubesVM): + await vm.create_on_disk(pool=pool, pools=pools) except: del self.app.domains[vm] raise @@ -1310,7 +1315,10 @@ async def vm_remove(self): if not self.dest.is_halted(): raise qubes.exc.QubesVMNotHaltedError(self.dest) - if self.dest.installed_by_rpm: + if ( + isinstance(self.dest, qubes.vm.qubesvm.QubesVM) + and self.dest.installed_by_rpm + ): raise qubes.exc.QubesVMInUseError( self.dest, "VM installed by package manager: " + self.dest.name, diff --git a/qubes/app.py b/qubes/app.py index 8296dbde2..7ac7e1001 100644 --- a/qubes/app.py +++ b/qubes/app.py @@ -503,7 +503,7 @@ def vms(self): def add(self, value, _enable_events=True): """Add VM to collection - :param qubes.vm.LocalVM value: VM to add + :param qubes.vm.BaseVM value: VM to add :param _enable_events: :raises TypeError: when value is of wrong type :raises ValueError: when there is already VM which has equal ``qid`` @@ -511,7 +511,7 @@ def add(self, value, _enable_events=True): # this violates duck typing, but is needed # for VMProperty to function correctly - if not isinstance(value, qubes.vm.LocalVM): + if not isinstance(value, qubes.vm.BaseVM): raise TypeError( "{} holds only LocalVM instances".format( self.__class__.__name__ @@ -545,7 +545,7 @@ def __getitem__(self, key): return vm raise KeyError(key) - if isinstance(key, qubes.vm.LocalVM): + if isinstance(key, qubes.vm.BaseVM): key = key.uuid if isinstance(key, uuid.UUID): @@ -559,10 +559,11 @@ def __getitem__(self, key): def __delitem__(self, key): vm = self[key] - if not vm.is_halted(): + if isinstance(vm, qubes.vm.qubesvm.QubesVM) and not vm.is_halted(): raise qubes.exc.QubesVMNotHaltedError(vm) self.app.fire_event("domain-pre-delete", pre_event=True, vm=vm) - vm.libvirt_undefine() + if isinstance(vm, qubes.vm.qubesvm.QubesVM): + vm.libvirt_undefine() del self._dict[vm.qid] self.app.fire_event("domain-delete", vm=vm) if getattr(vm, "dispid", None): @@ -1654,8 +1655,11 @@ def on_domain_pre_deleted(self, event, vm): "see 'journalctl -u qubesd -e' in dom0 for " "details".format(vm.name), ) - - assignments = vm.get_provided_assignments() + self.log.critical(vm) + if isinstance(vm, qubes.vm.qubesvm.QubesVM): + assignments = vm.get_provided_assignments() + else: + assignments = [] if assignments: desc = ", ".join(assignment.port_id for assignment in assignments) raise qubes.exc.QubesVMInUseError( diff --git a/qubes/ext/block.py b/qubes/ext/block.py index 244e62371..c704d0246 100644 --- a/qubes/ext/block.py +++ b/qubes/ext/block.py @@ -31,10 +31,11 @@ import qubes.device_protocol import qubes.devices import qubes.ext +from qubes.devices import Port from qubes.ext import utils from qubes.storage import Storage from qubes.vm.qubesvm import QubesVM -from qubes.devices import Port +from qubes.vm.remotevm import RemoteVM name_re = re.compile(r"\A[a-z0-9-]{1,12}\Z") device_re = re.compile(r"\A[a-z0-9/-]{1,64}\Z") @@ -346,7 +347,7 @@ def on_qdb_change(self, vm, event, path): def get_device_attachments(vm_): result = {} for vm in vm_.app.domains: - if not vm.is_running(): + if not vm.is_running() or isinstance(vm, RemoteVM): continue if vm.app.vmm.offline_mode: diff --git a/qubes/tests/app.py b/qubes/tests/app.py index fcd2a8e8a..64e59addf 100644 --- a/qubes/tests/app.py +++ b/qubes/tests/app.py @@ -915,6 +915,38 @@ class MyTestHolder(qubes.tests.TestEmitter, qubes.PropertyHolder): self.assertNotIn("audiovm-sys-audio", appvm.tags) self.assertNotIn("audiovm-", appvm.tags) + def test_116_remotevm_add_and_remove(self): + remotevm1 = self.app.add_new_vm( + "RemoteVM", name="remote-vm1", label="blue" + ) + remotevm2 = self.app.add_new_vm( + "RemoteVM", name="remote-vm2", label="gray" + ) + qubesvm1 = self.app.add_new_vm( + "AppVM", + name="test-vm", + template=self.template, + label="red", + ) + + assert remotevm1 in self.app.domains + del self.app.domains["remote-vm1"] + + self.assertCountEqual( + {d.name for d in self.app.domains}, + {"dom0", "test-template", "test-vm", "remote-vm2"}, + ) + + def test_117_remotevm_status(self): + remotevm1 = self.app.add_new_vm( + "RemoteVM", name="remote-vm1", label="blue" + ) + assert [ + remotevm1.get_power_state(), + remotevm1.get_cputime(), + remotevm1.get_mem(), + ] == ["Running", 0, 0] + def test_200_remove_template(self): appvm = self.app.add_new_vm( "AppVM", name="test-vm", template=self.template, label="red" diff --git a/qubes/vm/__init__.py b/qubes/vm/__init__.py index e5ee67026..513334a7a 100644 --- a/qubes/vm/__init__.py +++ b/qubes/vm/__init__.py @@ -269,6 +269,21 @@ def __init__(self, app, xml, features=None, tags=None, **kwargs): if hasattr(self, "name"): self.init_log() + #: operations which shouldn't happen simultaneously with qube startup + # (including another startup of the same qube) + self.startup_lock = asyncio.Lock() + + def __str__(self): + return self.name + + def __hash__(self): + return self.qid + + def __lt__(self, other): + if not isinstance(other, qubes.vm.BaseVM): + return NotImplemented + return self.name < other.name + @qubes.stateless_property def klass(self): """Domain class name""" @@ -342,6 +357,13 @@ def __repr__(self): " ".join(proprepr), ) + @qubes.events.handler("domain-init", "domain-load") + def on_domain_init_loaded(self, event): + # pylint: disable=unused-argument + if not hasattr(self, "uuid"): + # pylint: disable=attribute-defined-outside-init + self.uuid = uuid.uuid4() + class LocalVM(BaseVM): """Base class for all local VMs @@ -478,6 +500,8 @@ def get_provided_assignments( for domain in self.app.domains: if domain == self: continue + if getattr(domain, "klass") == "RemoteVM": + continue for device_collection in domain.devices.values(): for assignment in device_collection.get_assigned_devices( required_only diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index 3821f9e4c..807d72932 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -1142,10 +1142,6 @@ def __init__(self, app, xml, volume_config=None, **kwargs): # will be initialized after loading all the properties - #: operations which shouldn't happen simultaneously with qube startup - # (including another startup of the same qube) - self.startup_lock = asyncio.Lock() - # fire hooks if xml is None: self.events_enabled = True @@ -1159,15 +1155,11 @@ def close(self): self._libvirt_domain = None super().close() - def __hash__(self): - return self.qid - def __lt__(self, other): - if not isinstance(other, qubes.vm.LocalVM): - return NotImplemented if isinstance(other, qubes.vm.adminvm.AdminVM): return False - return self.name < other.name + else: + return super().__lt__(other) def __xml__(self): # pylint: disable=no-member @@ -1188,10 +1180,7 @@ def __xml__(self): @qubes.events.handler("domain-init", "domain-load") def on_domain_init_loaded(self, event): - # pylint: disable=unused-argument - if not hasattr(self, "uuid"): - # pylint: disable=attribute-defined-outside-init - self.uuid = uuid.uuid4() + super().on_domain_init_loaded(event) # Initialize VM image storage class; # it might be already initialized by a recursive call from a child VM diff --git a/qubes/vm/remotevm.py b/qubes/vm/remotevm.py new file mode 100644 index 000000000..8f9d108f6 --- /dev/null +++ b/qubes/vm/remotevm.py @@ -0,0 +1,74 @@ +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2024 Frédéric Pierret (fepitre) +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program. If not, see . +# +# SPDX-License-Identifier: GPL-3.0-or-later + +import asyncio +import grp +import subprocess +import uuid + +import qubes +import qubes.exc +import qubes.vm +from qubes.vm import BaseVM + + +class RemoteVM(BaseVM): + + def __init__(self, app, xml, **kwargs): + super().__init__(app, xml, **kwargs) + self.connected_relay_vm = None + if xml is None: + self.events_enabled = True + self.fire_event("domain-init") + + def get_mem(self): + return 0 + + def get_mem_static_max(self): + return 0 + + def get_cputime(self): + return 0 + + @staticmethod + def is_running(): + # fixme: handle power management option + return True + + @staticmethod + def is_halted(): + # fixme: handle power management option + return False + + @staticmethod + def get_power_state(): + # fixme: handle power management option + return "Running" + + def start(self, **kwargs): + raise qubes.exc.QubesVMNotHaltedError(self, "Cannot start a RemoteVM.") + + def suspend(self): + raise qubes.exc.QubesVMError(self, "Cannot suspend a RemoteVM.") + + def shutdown(self): + raise qubes.exc.QubesVMError(self, "Cannot shutdown a RemoteVM.") + + def kill(self): + raise qubes.exc.QubesVMError(self, "Cannot kill a RemoteVM.") diff --git a/setup.py b/setup.py index bc2d4f697..19cfcbfc6 100644 --- a/setup.py +++ b/setup.py @@ -58,6 +58,7 @@ def run(self): 'StandaloneVM = qubes.vm.standalonevm:StandaloneVM', 'AdminVM = qubes.vm.adminvm:AdminVM', 'DispVM = qubes.vm.dispvm:DispVM', + 'RemoteVM = qubes.vm.remotevm:RemoteVM', ], 'qubes.ext': [ 'qubes.ext.admin = qubes.ext.admin:AdminExtension', From 70dfcb419e609ee6413090df1cb70ebddbf132cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Pierret=20=28fepitre=29?= Date: Wed, 19 Mar 2025 10:19:58 +0100 Subject: [PATCH 2/3] Introduce RelayVM property for RemoteVM QubesOS/qubes-issues#9015 --- qubes/api/internal.py | 14 ++++++++ qubes/app.py | 1 - qubes/ext/relay.py | 65 +++++++++++++++++++++++++++++++++++++ qubes/tests/api_internal.py | 6 ++++ qubes/tests/app.py | 60 +++++++++++++++++++++++++++++++--- qubes/vm/qubesvm.py | 4 +-- qubes/vm/remotevm.py | 31 ++++++++++++++---- rpm_spec/core-dom0.spec.in | 2 ++ setup.py | 9 ++--- 9 files changed, 173 insertions(+), 19 deletions(-) create mode 100644 qubes/ext/relay.py diff --git a/qubes/api/internal.py b/qubes/api/internal.py index 9d1c686d0..8513b36b6 100644 --- a/qubes/api/internal.py +++ b/qubes/api/internal.py @@ -49,6 +49,10 @@ class SystemInfoCache: "property-reset:icon", "property-set:guivm", "property-reset:guivm", + "property-set:relayvm", + "property-reset:relayvm", + "property-set:transport_rpc", + "property-reset:transport_rpc", # technically not changeable, but keep for consistency "property-set:uuid", "property-reset:uuid", @@ -125,6 +129,16 @@ def get_system_info(cls, app): if getattr(domain, "guivm", None) else None ), + "relayvm": ( + domain.relayvm.name + if getattr(domain, "relayvm", None) + else None + ), + "transport_rpc": ( + domain.transport_rpc + if getattr(domain, "transport_rpc", None) + else None + ), "power_state": domain.get_power_state(), "uuid": str(domain.uuid), } diff --git a/qubes/app.py b/qubes/app.py index 7ac7e1001..a3864189e 100644 --- a/qubes/app.py +++ b/qubes/app.py @@ -1655,7 +1655,6 @@ def on_domain_pre_deleted(self, event, vm): "see 'journalctl -u qubesd -e' in dom0 for " "details".format(vm.name), ) - self.log.critical(vm) if isinstance(vm, qubes.vm.qubesvm.QubesVM): assignments = vm.get_provided_assignments() else: diff --git a/qubes/ext/relay.py b/qubes/ext/relay.py new file mode 100644 index 000000000..d4f5b5833 --- /dev/null +++ b/qubes/ext/relay.py @@ -0,0 +1,65 @@ +# +# The Qubes OS Project, https://www.qubes-os.org/ +# +# Copyright (C) 2024 Frédéric Pierret +# +# 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 qubes.ext +import qubes.vm.remotevm + + +class Relay(qubes.ext.Extension): + # pylint: disable=unused-argument + @qubes.ext.handler("domain-init", "domain-load") + def on_domain_init_load(self, vm, event): + if ( + getattr(vm, "relayvm", None) + and "relayvm-" + vm.relayvm.name not in vm.tags + ): + self.on_property_set(vm, event, name="relayvm", newvalue=vm.relayvm) + + @qubes.ext.handler("domain-start") + def on_domain_start(self, vm, event, **kwargs): + if not vm.untrusted_qdb: + return + for domain in vm.app.domains: + if getattr(domain, "relayvm", None) == vm: + vm.untrusted_qdb.write( + f"/remote/{domain.name}", domain.remote_name or domain.name + ) + + @qubes.ext.handler("property-reset:relayvm", vm=qubes.vm.remotevm.RemoteVM) + def on_property_reset(self, subject, event, name, oldvalue=None): + newvalue = getattr(subject, "relayvm", None) + self.on_property_set(subject, event, name, newvalue, oldvalue) + + @qubes.ext.handler("property-set:relayvm", vm=qubes.vm.remotevm.RemoteVM) + def on_property_set(self, subject, event, name, newvalue, oldvalue=None): + # Clean other 'relayvm-XXX' tags. + # qrexec-client-vm can connect to only one domain + tags_list = list(subject.tags) + for tag in tags_list: + if tag.startswith("relayvm-"): + subject.tags.remove(tag) + + if newvalue: + relayvm_tag = "relayvm-" + newvalue.name + subject.tags.add(relayvm_tag) + if newvalue.untrusted_qdb: + remote_name = subject.remote_name or subject.name + newvalue.untrusted_qdb.write( + f"/remote/{subject.name}", remote_name + ) diff --git a/qubes/tests/api_internal.py b/qubes/tests/api_internal.py index 1c8a20b58..ec1919261 100644 --- a/qubes/tests/api_internal.py +++ b/qubes/tests/api_internal.py @@ -37,6 +37,8 @@ async def coro_f(*args, **kwargs): class TC_00_API_Misc(qubes.tests.QubesTestCase): + maxDiff = None + def setUp(self): super().setUp() self.app = mock.NonCallableMock() @@ -195,6 +197,8 @@ def test_010_get_system_info(self): "icon": "icon-dom0", "guivm": None, "power_state": "Running", + "relayvm": None, + "transport_rpc": None, "uuid": "00000000-0000-0000-0000-000000000000", }, "vm": { @@ -205,6 +209,8 @@ def test_010_get_system_info(self): "icon": "icon-vm", "guivm": "vm", "power_state": "Halted", + "relayvm": None, + "transport_rpc": None, "uuid": str(TEST_UUID), }, } diff --git a/qubes/tests/app.py b/qubes/tests/app.py index 64e59addf..f64306fc6 100644 --- a/qubes/tests/app.py +++ b/qubes/tests/app.py @@ -21,6 +21,7 @@ # import os +import unittest import unittest.mock as mock import lxml.etree @@ -35,6 +36,8 @@ import logging import time +from qubes.tests.vm.qubesvm import TestQubesDB + class TestApp(qubes.tests.TestEmitter): pass @@ -919,10 +922,8 @@ def test_116_remotevm_add_and_remove(self): remotevm1 = self.app.add_new_vm( "RemoteVM", name="remote-vm1", label="blue" ) - remotevm2 = self.app.add_new_vm( - "RemoteVM", name="remote-vm2", label="gray" - ) - qubesvm1 = self.app.add_new_vm( + self.app.add_new_vm("RemoteVM", name="remote-vm2", label="gray") + self.app.add_new_vm( "AppVM", name="test-vm", template=self.template, @@ -947,6 +948,57 @@ def test_117_remotevm_status(self): remotevm1.get_mem(), ] == ["Running", 0, 0] + @unittest.mock.patch("qubes.vm.qubesvm.QubesVM.untrusted_qdb") + def test_118_remotevm_set_relayvm(self, mock_qubesdb): + class MyTestHolder(qubes.tests.TestEmitter, qubes.PropertyHolder): + relayvm = qubes.property("relayvm") + transport_rpc = qubes.property("transport_rpc") + + localrelay = self.app.add_new_vm( + "AppVM", + name="local-relay", + template=self.template, + label="red", + ) + # add QDB to localrelay + test_qubesdb = TestQubesDB() + mock_qubesdb.write.side_effect = test_qubesdb.write + mock_qubesdb.rm.side_effect = test_qubesdb.rm + localrelay.untrusted_qdb = test_qubesdb + + remotevm = self.app.add_new_vm( + "RemoteVM", name="remote-vm", label="blue" + ) + remotevm.remote_name = "myawesomevm" + + holder = MyTestHolder(None) + holder.relayvm = "local-relay" + holder.transport_rpc = "qubesair.SSHProxy" + self.assertEqual(holder.relayvm, "local-relay") + self.assertEqual(holder.transport_rpc, "qubesair.SSHProxy") + + self.assertEventFired( + holder, + "property-set:relayvm", + kwargs={"name": "relayvm", "newvalue": "local-relay"}, + ) + + self.assertEventFired( + holder, + "property-set:transport_rpc", + kwargs={"name": "transport_rpc", "newvalue": "qubesair.SSHProxy"}, + ) + + # Set RelayVM + remotevm.relayvm = localrelay + self.assertIn("relayvm-local-relay", remotevm.tags) + + # Read QDB path + self.assertEqual( + localrelay.untrusted_qdb.read("/remote/remote-vm"), + remotevm.remote_name, + ) + def test_200_remove_template(self): appvm = self.app.add_new_vm( "AppVM", name="test-vm", template=self.template, label="red" diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index 807d72932..8eb69b575 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -30,7 +30,6 @@ import shutil import string import subprocess -import uuid import libvirt # pylint: disable=import-error import lxml.etree @@ -1158,8 +1157,7 @@ def close(self): def __lt__(self, other): if isinstance(other, qubes.vm.adminvm.AdminVM): return False - else: - return super().__lt__(other) + return super().__lt__(other) def __xml__(self): # pylint: disable=no-member diff --git a/qubes/vm/remotevm.py b/qubes/vm/remotevm.py index 8f9d108f6..3dcb851ff 100644 --- a/qubes/vm/remotevm.py +++ b/qubes/vm/remotevm.py @@ -1,6 +1,6 @@ # The Qubes OS Project, http://www.qubes-os.org # -# Copyright (C) 2024 Frédéric Pierret (fepitre) +# Copyright (C) 2024 Frédéric Pierret # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -17,11 +17,6 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -import asyncio -import grp -import subprocess -import uuid - import qubes import qubes.exc import qubes.vm @@ -30,9 +25,31 @@ class RemoteVM(BaseVM): + relayvm = qubes.VMProperty( + "relayvm", + load_stage=4, + allow_none=True, + default=None, + doc="Local qube used as relay.", + ) + + transport_rpc = qubes.property( + "transport_rpc", + type=str, + default=None, + doc="Transport RPC used by the relay.", + ) + + remote_name = qubes.property( + "remote_name", + type=str, + default=None, + doc="Name on the remote host.", + ) + def __init__(self, app, xml, **kwargs): super().__init__(app, xml, **kwargs) - self.connected_relay_vm = None + if xml is None: self.events_enabled = True self.fire_event("domain-init") diff --git a/rpm_spec/core-dom0.spec.in b/rpm_spec/core-dom0.spec.in index 6453f00be..1f464ec63 100644 --- a/rpm_spec/core-dom0.spec.in +++ b/rpm_spec/core-dom0.spec.in @@ -404,6 +404,7 @@ done %{python3_sitelib}/qubes/vm/appvm.py %{python3_sitelib}/qubes/vm/dispvm.py %{python3_sitelib}/qubes/vm/qubesvm.py +%{python3_sitelib}/qubes/vm/remotevm.py %{python3_sitelib}/qubes/vm/standalonevm.py %{python3_sitelib}/qubes/vm/templatevm.py @@ -447,6 +448,7 @@ done %{python3_sitelib}/qubes/ext/gui.py %{python3_sitelib}/qubes/ext/audio.py %{python3_sitelib}/qubes/ext/pci.py +%{python3_sitelib}/qubes/ext/relay.py %{python3_sitelib}/qubes/ext/r3compatibility.py %{python3_sitelib}/qubes/ext/services.py %{python3_sitelib}/qubes/ext/supported_features.py diff --git a/setup.py b/setup.py index 19cfcbfc6..fa2deb9d3 100644 --- a/setup.py +++ b/setup.py @@ -62,19 +62,20 @@ def run(self): ], 'qubes.ext': [ 'qubes.ext.admin = qubes.ext.admin:AdminExtension', + 'qubes.ext.audio = qubes.ext.audio:AUDIO', 'qubes.ext.backup_restore = ' 'qubes.ext.backup_restore:BackupRestoreExtension', + 'qubes.ext.block = qubes.ext.block:BlockDeviceExtension', 'qubes.ext.core_features = qubes.ext.core_features:CoreFeatures', 'qubes.ext.custom_persist = qubes.ext.custom_persist:CustomPersist', 'qubes.ext.gui = qubes.ext.gui:GUI', - 'qubes.ext.audio = qubes.ext.audio:AUDIO', - 'qubes.ext.r3compatibility = qubes.ext.r3compatibility:R3Compatibility', 'qubes.ext.pci = qubes.ext.pci:PCIDeviceExtension', - 'qubes.ext.block = qubes.ext.block:BlockDeviceExtension', + 'qubes.ext.r3compatibility = qubes.ext.r3compatibility:R3Compatibility', + 'qubes.ext.relay = qubes.ext.relay:Relay', 'qubes.ext.services = qubes.ext.services:ServicesExtension', 'qubes.ext.supported_features = qubes.ext.supported_features:SupportedFeaturesExtension', - 'qubes.ext.windows = qubes.ext.windows:WindowsFeatures', 'qubes.ext.vm_config = qubes.ext.vm_config:VMConfig', + 'qubes.ext.windows = qubes.ext.windows:WindowsFeatures', ], 'qubes.devices': [ 'pci = qubes.ext.pci:PCIDevice', From 3a62a74f0e80886abcb7b7ef3af9f957c14314d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Pierret=20=28fepitre=29?= Date: Wed, 16 Apr 2025 09:49:16 +0200 Subject: [PATCH 3/3] doc: add qubes.vm.remotevm --- doc/qubes-vm/remotevm.rst | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 doc/qubes-vm/remotevm.rst diff --git a/doc/qubes-vm/remotevm.rst b/doc/qubes-vm/remotevm.rst new file mode 100644 index 000000000..93dbdcb54 --- /dev/null +++ b/doc/qubes-vm/remotevm.rst @@ -0,0 +1,8 @@ +:py:mod:`qubes.vm.remotevm` -- Remote VM +========================================== + +.. automodule:: qubes.vm.remotevm + :members: + :show-inheritance: + +.. vim: ts=3 sw=3 et