Skip to content

Commit 9ba8b4c

Browse files
committed
Merge remote-tracking branch 'origin/pr/668'
* origin/pr/668: doc: add qubes.vm.remotevm Introduce RelayVM property for RemoteVM Introduce RemoteVM class Pull request description: QubesOS/qubes-issues/issues/9015
2 parents 055554e + 3a62a74 commit 9ba8b4c

File tree

13 files changed

+326
-31
lines changed

13 files changed

+326
-31
lines changed

doc/qubes-vm/remotevm.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
:py:mod:`qubes.vm.remotevm` -- Remote VM
2+
==========================================
3+
4+
.. automodule:: qubes.vm.remotevm
5+
:members:
6+
:show-inheritance:
7+
8+
.. vim: ts=3 sw=3 et

qubes/api/admin.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,11 @@ def _property_reset(self, dest):
370370
async def vm_volume_list(self):
371371
self.enforce(not self.arg)
372372

373-
volume_names = self.fire_event_for_filter(self.dest.volumes.keys())
373+
volume_names = (
374+
self.fire_event_for_filter(self.dest.volumes.keys())
375+
if isinstance(self.dest, qubes.vm.qubesvm.QubesVM)
376+
else []
377+
)
374378
return "".join("{}\n".format(name) for name in volume_names)
375379

376380
@qubes.api.method(
@@ -1262,7 +1266,8 @@ async def _vm_create(
12621266
vm.tags.add("created-by-" + str(self.src))
12631267

12641268
try:
1265-
await vm.create_on_disk(pool=pool, pools=pools)
1269+
if isinstance(vm, qubes.vm.qubesvm.QubesVM):
1270+
await vm.create_on_disk(pool=pool, pools=pools)
12661271
except:
12671272
del self.app.domains[vm]
12681273
raise
@@ -1310,7 +1315,10 @@ async def vm_remove(self):
13101315
if not self.dest.is_halted():
13111316
raise qubes.exc.QubesVMNotHaltedError(self.dest)
13121317

1313-
if self.dest.installed_by_rpm:
1318+
if (
1319+
isinstance(self.dest, qubes.vm.qubesvm.QubesVM)
1320+
and self.dest.installed_by_rpm
1321+
):
13141322
raise qubes.exc.QubesVMInUseError(
13151323
self.dest,
13161324
"VM installed by package manager: " + self.dest.name,

qubes/api/internal.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ class SystemInfoCache:
4949
"property-reset:icon",
5050
"property-set:guivm",
5151
"property-reset:guivm",
52+
"property-set:relayvm",
53+
"property-reset:relayvm",
54+
"property-set:transport_rpc",
55+
"property-reset:transport_rpc",
5256
# technically not changeable, but keep for consistency
5357
"property-set:uuid",
5458
"property-reset:uuid",
@@ -125,6 +129,16 @@ def get_system_info(cls, app):
125129
if getattr(domain, "guivm", None)
126130
else None
127131
),
132+
"relayvm": (
133+
domain.relayvm.name
134+
if getattr(domain, "relayvm", None)
135+
else None
136+
),
137+
"transport_rpc": (
138+
domain.transport_rpc
139+
if getattr(domain, "transport_rpc", None)
140+
else None
141+
),
128142
"power_state": domain.get_power_state(),
129143
"uuid": str(domain.uuid),
130144
}

qubes/app.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -503,15 +503,15 @@ def vms(self):
503503
def add(self, value, _enable_events=True):
504504
"""Add VM to collection
505505
506-
:param qubes.vm.LocalVM value: VM to add
506+
:param qubes.vm.BaseVM value: VM to add
507507
:param _enable_events:
508508
:raises TypeError: when value is of wrong type
509509
:raises ValueError: when there is already VM which has equal ``qid``
510510
"""
511511

512512
# this violates duck typing, but is needed
513513
# for VMProperty to function correctly
514-
if not isinstance(value, qubes.vm.LocalVM):
514+
if not isinstance(value, qubes.vm.BaseVM):
515515
raise TypeError(
516516
"{} holds only LocalVM instances".format(
517517
self.__class__.__name__
@@ -545,7 +545,7 @@ def __getitem__(self, key):
545545
return vm
546546
raise KeyError(key)
547547

548-
if isinstance(key, qubes.vm.LocalVM):
548+
if isinstance(key, qubes.vm.BaseVM):
549549
key = key.uuid
550550

551551
if isinstance(key, uuid.UUID):
@@ -559,10 +559,11 @@ def __getitem__(self, key):
559559

560560
def __delitem__(self, key):
561561
vm = self[key]
562-
if not vm.is_halted():
562+
if isinstance(vm, qubes.vm.qubesvm.QubesVM) and not vm.is_halted():
563563
raise qubes.exc.QubesVMNotHaltedError(vm)
564564
self.app.fire_event("domain-pre-delete", pre_event=True, vm=vm)
565-
vm.libvirt_undefine()
565+
if isinstance(vm, qubes.vm.qubesvm.QubesVM):
566+
vm.libvirt_undefine()
566567
del self._dict[vm.qid]
567568
self.app.fire_event("domain-delete", vm=vm)
568569
if getattr(vm, "dispid", None):
@@ -1654,8 +1655,10 @@ def on_domain_pre_deleted(self, event, vm):
16541655
"see 'journalctl -u qubesd -e' in dom0 for "
16551656
"details".format(vm.name),
16561657
)
1657-
1658-
assignments = vm.get_provided_assignments()
1658+
if isinstance(vm, qubes.vm.qubesvm.QubesVM):
1659+
assignments = vm.get_provided_assignments()
1660+
else:
1661+
assignments = []
16591662
if assignments:
16601663
desc = ", ".join(assignment.port_id for assignment in assignments)
16611664
raise qubes.exc.QubesVMInUseError(

qubes/ext/block.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,11 @@
3232
import qubes.devices
3333
import qubes.exc
3434
import qubes.ext
35+
from qubes.devices import Port
3536
from qubes.ext import utils
3637
from qubes.storage import Storage
3738
from qubes.vm.qubesvm import QubesVM
38-
from qubes.devices import Port
39+
from qubes.vm.remotevm import RemoteVM
3940

4041
name_re = re.compile(r"\A[a-z0-9-]{1,12}\Z")
4142
device_re = re.compile(r"\A[a-z0-9/-]{1,64}\Z")
@@ -347,7 +348,7 @@ def on_qdb_change(self, vm, event, path):
347348
def get_device_attachments(vm_):
348349
result = {}
349350
for vm in vm_.app.domains:
350-
if not vm.is_running():
351+
if not vm.is_running() or isinstance(vm, RemoteVM):
351352
continue
352353

353354
if vm.app.vmm.offline_mode:

qubes/ext/relay.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#
2+
# The Qubes OS Project, https://www.qubes-os.org/
3+
#
4+
# Copyright (C) 2024 Frédéric Pierret <[email protected]>
5+
#
6+
# This library is free software; you can redistribute it and/or
7+
# modify it under the terms of the GNU Lesser General Public
8+
# License as published by the Free Software Foundation; either
9+
# version 2.1 of the License, or (at your option) any later version.
10+
#
11+
# This library is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
# Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public
17+
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
18+
#
19+
20+
import qubes.ext
21+
import qubes.vm.remotevm
22+
23+
24+
class Relay(qubes.ext.Extension):
25+
# pylint: disable=unused-argument
26+
@qubes.ext.handler("domain-init", "domain-load")
27+
def on_domain_init_load(self, vm, event):
28+
if (
29+
getattr(vm, "relayvm", None)
30+
and "relayvm-" + vm.relayvm.name not in vm.tags
31+
):
32+
self.on_property_set(vm, event, name="relayvm", newvalue=vm.relayvm)
33+
34+
@qubes.ext.handler("domain-start")
35+
def on_domain_start(self, vm, event, **kwargs):
36+
if not vm.untrusted_qdb:
37+
return
38+
for domain in vm.app.domains:
39+
if getattr(domain, "relayvm", None) == vm:
40+
vm.untrusted_qdb.write(
41+
f"/remote/{domain.name}", domain.remote_name or domain.name
42+
)
43+
44+
@qubes.ext.handler("property-reset:relayvm", vm=qubes.vm.remotevm.RemoteVM)
45+
def on_property_reset(self, subject, event, name, oldvalue=None):
46+
newvalue = getattr(subject, "relayvm", None)
47+
self.on_property_set(subject, event, name, newvalue, oldvalue)
48+
49+
@qubes.ext.handler("property-set:relayvm", vm=qubes.vm.remotevm.RemoteVM)
50+
def on_property_set(self, subject, event, name, newvalue, oldvalue=None):
51+
# Clean other 'relayvm-XXX' tags.
52+
# qrexec-client-vm can connect to only one domain
53+
tags_list = list(subject.tags)
54+
for tag in tags_list:
55+
if tag.startswith("relayvm-"):
56+
subject.tags.remove(tag)
57+
58+
if newvalue:
59+
relayvm_tag = "relayvm-" + newvalue.name
60+
subject.tags.add(relayvm_tag)
61+
if newvalue.untrusted_qdb:
62+
remote_name = subject.remote_name or subject.name
63+
newvalue.untrusted_qdb.write(
64+
f"/remote/{subject.name}", remote_name
65+
)

qubes/tests/api_internal.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ async def coro_f(*args, **kwargs):
3737

3838

3939
class TC_00_API_Misc(qubes.tests.QubesTestCase):
40+
maxDiff = None
41+
4042
def setUp(self):
4143
super().setUp()
4244
self.app = mock.NonCallableMock()
@@ -195,6 +197,8 @@ def test_010_get_system_info(self):
195197
"icon": "icon-dom0",
196198
"guivm": None,
197199
"power_state": "Running",
200+
"relayvm": None,
201+
"transport_rpc": None,
198202
"uuid": "00000000-0000-0000-0000-000000000000",
199203
},
200204
"vm": {
@@ -205,6 +209,8 @@ def test_010_get_system_info(self):
205209
"icon": "icon-vm",
206210
"guivm": "vm",
207211
"power_state": "Halted",
212+
"relayvm": None,
213+
"transport_rpc": None,
208214
"uuid": str(TEST_UUID),
209215
},
210216
}

qubes/tests/app.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#
2222

2323
import os
24+
import unittest
2425
import unittest.mock as mock
2526

2627
import lxml.etree
@@ -35,6 +36,8 @@
3536
import logging
3637
import time
3738

39+
from qubes.tests.vm.qubesvm import TestQubesDB
40+
3841

3942
class TestApp(qubes.tests.TestEmitter):
4043
pass
@@ -915,6 +918,87 @@ class MyTestHolder(qubes.tests.TestEmitter, qubes.PropertyHolder):
915918
self.assertNotIn("audiovm-sys-audio", appvm.tags)
916919
self.assertNotIn("audiovm-", appvm.tags)
917920

921+
def test_116_remotevm_add_and_remove(self):
922+
remotevm1 = self.app.add_new_vm(
923+
"RemoteVM", name="remote-vm1", label="blue"
924+
)
925+
self.app.add_new_vm("RemoteVM", name="remote-vm2", label="gray")
926+
self.app.add_new_vm(
927+
"AppVM",
928+
name="test-vm",
929+
template=self.template,
930+
label="red",
931+
)
932+
933+
assert remotevm1 in self.app.domains
934+
del self.app.domains["remote-vm1"]
935+
936+
self.assertCountEqual(
937+
{d.name for d in self.app.domains},
938+
{"dom0", "test-template", "test-vm", "remote-vm2"},
939+
)
940+
941+
def test_117_remotevm_status(self):
942+
remotevm1 = self.app.add_new_vm(
943+
"RemoteVM", name="remote-vm1", label="blue"
944+
)
945+
assert [
946+
remotevm1.get_power_state(),
947+
remotevm1.get_cputime(),
948+
remotevm1.get_mem(),
949+
] == ["Running", 0, 0]
950+
951+
@unittest.mock.patch("qubes.vm.qubesvm.QubesVM.untrusted_qdb")
952+
def test_118_remotevm_set_relayvm(self, mock_qubesdb):
953+
class MyTestHolder(qubes.tests.TestEmitter, qubes.PropertyHolder):
954+
relayvm = qubes.property("relayvm")
955+
transport_rpc = qubes.property("transport_rpc")
956+
957+
localrelay = self.app.add_new_vm(
958+
"AppVM",
959+
name="local-relay",
960+
template=self.template,
961+
label="red",
962+
)
963+
# add QDB to localrelay
964+
test_qubesdb = TestQubesDB()
965+
mock_qubesdb.write.side_effect = test_qubesdb.write
966+
mock_qubesdb.rm.side_effect = test_qubesdb.rm
967+
localrelay.untrusted_qdb = test_qubesdb
968+
969+
remotevm = self.app.add_new_vm(
970+
"RemoteVM", name="remote-vm", label="blue"
971+
)
972+
remotevm.remote_name = "myawesomevm"
973+
974+
holder = MyTestHolder(None)
975+
holder.relayvm = "local-relay"
976+
holder.transport_rpc = "qubesair.SSHProxy"
977+
self.assertEqual(holder.relayvm, "local-relay")
978+
self.assertEqual(holder.transport_rpc, "qubesair.SSHProxy")
979+
980+
self.assertEventFired(
981+
holder,
982+
"property-set:relayvm",
983+
kwargs={"name": "relayvm", "newvalue": "local-relay"},
984+
)
985+
986+
self.assertEventFired(
987+
holder,
988+
"property-set:transport_rpc",
989+
kwargs={"name": "transport_rpc", "newvalue": "qubesair.SSHProxy"},
990+
)
991+
992+
# Set RelayVM
993+
remotevm.relayvm = localrelay
994+
self.assertIn("relayvm-local-relay", remotevm.tags)
995+
996+
# Read QDB path
997+
self.assertEqual(
998+
localrelay.untrusted_qdb.read("/remote/remote-vm"),
999+
remotevm.remote_name,
1000+
)
1001+
9181002
def test_200_remove_template(self):
9191003
appvm = self.app.add_new_vm(
9201004
"AppVM", name="test-vm", template=self.template, label="red"

qubes/vm/__init__.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,21 @@ def __init__(self, app, xml, features=None, tags=None, **kwargs):
270270
if hasattr(self, "name"):
271271
self.init_log()
272272

273+
#: operations which shouldn't happen simultaneously with qube startup
274+
# (including another startup of the same qube)
275+
self.startup_lock = asyncio.Lock()
276+
277+
def __str__(self):
278+
return self.name
279+
280+
def __hash__(self):
281+
return self.qid
282+
283+
def __lt__(self, other):
284+
if not isinstance(other, qubes.vm.BaseVM):
285+
return NotImplemented
286+
return self.name < other.name
287+
273288
@qubes.stateless_property
274289
def klass(self):
275290
"""Domain class name"""
@@ -343,6 +358,13 @@ def __repr__(self):
343358
" ".join(proprepr),
344359
)
345360

361+
@qubes.events.handler("domain-init", "domain-load")
362+
def on_domain_init_loaded(self, event):
363+
# pylint: disable=unused-argument
364+
if not hasattr(self, "uuid"):
365+
# pylint: disable=attribute-defined-outside-init
366+
self.uuid = uuid.uuid4()
367+
346368

347369
class LocalVM(BaseVM):
348370
"""Base class for all local VMs
@@ -488,6 +510,8 @@ def get_provided_assignments(
488510
for domain in self.app.domains:
489511
if domain == self:
490512
continue
513+
if getattr(domain, "klass") == "RemoteVM":
514+
continue
491515
for device_collection in domain.devices.values():
492516
for assignment in device_collection.get_assigned_devices(
493517
required_only

0 commit comments

Comments
 (0)