Skip to content

Commit 8e4b769

Browse files
committed
Introduce RemoteVM class
1 parent c63c185 commit 8e4b769

8 files changed

Lines changed: 159 additions & 26 deletions

File tree

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/app.py

Lines changed: 11 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,11 @@ 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+
self.log.critical(vm)
1659+
if isinstance(vm, qubes.vm.qubesvm.QubesVM):
1660+
assignments = vm.get_provided_assignments()
1661+
else:
1662+
assignments = []
16591663
if assignments:
16601664
desc = ", ".join(assignment.port_id for assignment in assignments)
16611665
raise qubes.exc.QubesVMInUseError(

qubes/ext/block.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@
3131
import qubes.device_protocol
3232
import qubes.devices
3333
import qubes.ext
34+
from qubes.devices import Port
3435
from qubes.ext import utils
3536
from qubes.storage import Storage
3637
from qubes.vm.qubesvm import QubesVM
37-
from qubes.devices import Port
38+
from qubes.vm.remotevm import RemoteVM
3839

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

352353
if vm.app.vmm.offline_mode:

qubes/tests/app.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,38 @@ class MyTestHolder(qubes.tests.TestEmitter, qubes.PropertyHolder):
915915
self.assertNotIn("audiovm-sys-audio", appvm.tags)
916916
self.assertNotIn("audiovm-", appvm.tags)
917917

918+
def test_116_remotevm_add_and_remove(self):
919+
remotevm1 = self.app.add_new_vm(
920+
"RemoteVM", name="remote-vm1", label="blue"
921+
)
922+
remotevm2 = self.app.add_new_vm(
923+
"RemoteVM", name="remote-vm2", label="gray"
924+
)
925+
qubesvm1 = self.app.add_new_vm(
926+
"AppVM",
927+
name="test-vm",
928+
template=self.template,
929+
label="red",
930+
)
931+
932+
assert remotevm1 in self.app.domains
933+
del self.app.domains["remote-vm1"]
934+
935+
self.assertCountEqual(
936+
{d.name for d in self.app.domains},
937+
{"dom0", "test-template", "test-vm", "remote-vm2"},
938+
)
939+
940+
def test_117_remotevm_status(self):
941+
remotevm1 = self.app.add_new_vm(
942+
"RemoteVM", name="remote-vm1", label="blue"
943+
)
944+
assert [
945+
remotevm1.get_power_state(),
946+
remotevm1.get_cputime(),
947+
remotevm1.get_mem(),
948+
] == ["Running", 0, 0]
949+
918950
def test_200_remove_template(self):
919951
appvm = self.app.add_new_vm(
920952
"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
@@ -269,6 +269,21 @@ def __init__(self, app, xml, features=None, tags=None, **kwargs):
269269
if hasattr(self, "name"):
270270
self.init_log()
271271

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

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

346368
class LocalVM(BaseVM):
347369
"""Base class for all local VMs
@@ -478,6 +500,8 @@ def get_provided_assignments(
478500
for domain in self.app.domains:
479501
if domain == self:
480502
continue
503+
if getattr(domain, "klass") == "RemoteVM":
504+
continue
481505
for device_collection in domain.devices.values():
482506
for assignment in device_collection.get_assigned_devices(
483507
required_only

qubes/vm/qubesvm.py

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,10 +1142,6 @@ def __init__(self, app, xml, volume_config=None, **kwargs):
11421142

11431143
# will be initialized after loading all the properties
11441144

1145-
#: operations which shouldn't happen simultaneously with qube startup
1146-
# (including another startup of the same qube)
1147-
self.startup_lock = asyncio.Lock()
1148-
11491145
# fire hooks
11501146
if xml is None:
11511147
self.events_enabled = True
@@ -1159,15 +1155,11 @@ def close(self):
11591155
self._libvirt_domain = None
11601156
super().close()
11611157

1162-
def __hash__(self):
1163-
return self.qid
1164-
11651158
def __lt__(self, other):
1166-
if not isinstance(other, qubes.vm.LocalVM):
1167-
return NotImplemented
11681159
if isinstance(other, qubes.vm.adminvm.AdminVM):
11691160
return False
1170-
return self.name < other.name
1161+
else:
1162+
return super().__lt__(other)
11711163

11721164
def __xml__(self):
11731165
# pylint: disable=no-member
@@ -1188,10 +1180,7 @@ def __xml__(self):
11881180

11891181
@qubes.events.handler("domain-init", "domain-load")
11901182
def on_domain_init_loaded(self, event):
1191-
# pylint: disable=unused-argument
1192-
if not hasattr(self, "uuid"):
1193-
# pylint: disable=attribute-defined-outside-init
1194-
self.uuid = uuid.uuid4()
1183+
super().on_domain_init_loaded(event)
11951184

11961185
# Initialize VM image storage class;
11971186
# it might be already initialized by a recursive call from a child VM

qubes/vm/remotevm.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# The Qubes OS Project, http://www.qubes-os.org
2+
#
3+
# Copyright (C) 2024 Frédéric Pierret (fepitre) <frederic@invisiblethingslab.com>
4+
#
5+
# This program is free software; you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation; either version 2 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU General Public License along
16+
# with this program. If not, see <https://www.gnu.org/licenses/>.
17+
#
18+
# SPDX-License-Identifier: GPL-3.0-or-later
19+
20+
import asyncio
21+
import grp
22+
import subprocess
23+
import uuid
24+
25+
import qubes
26+
import qubes.exc
27+
import qubes.vm
28+
from qubes.vm import BaseVM
29+
30+
31+
class RemoteVM(BaseVM):
32+
33+
def __init__(self, app, xml, **kwargs):
34+
super().__init__(app, xml, **kwargs)
35+
self.connected_relay_vm = None
36+
if xml is None:
37+
self.events_enabled = True
38+
self.fire_event("domain-init")
39+
40+
def get_mem(self):
41+
return 0
42+
43+
def get_mem_static_max(self):
44+
return 0
45+
46+
def get_cputime(self):
47+
return 0
48+
49+
@staticmethod
50+
def is_running():
51+
# fixme: handle power management option
52+
return True
53+
54+
@staticmethod
55+
def is_halted():
56+
# fixme: handle power management option
57+
return False
58+
59+
@staticmethod
60+
def get_power_state():
61+
# fixme: handle power management option
62+
return "Running"
63+
64+
def start(self, **kwargs):
65+
raise qubes.exc.QubesVMNotHaltedError(self, "Cannot start a RemoteVM.")
66+
67+
def suspend(self):
68+
raise qubes.exc.QubesVMError(self, "Cannot suspend a RemoteVM.")
69+
70+
def shutdown(self):
71+
raise qubes.exc.QubesVMError(self, "Cannot shutdown a RemoteVM.")
72+
73+
def kill(self):
74+
raise qubes.exc.QubesVMError(self, "Cannot kill a RemoteVM.")

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def run(self):
5858
'StandaloneVM = qubes.vm.standalonevm:StandaloneVM',
5959
'AdminVM = qubes.vm.adminvm:AdminVM',
6060
'DispVM = qubes.vm.dispvm:DispVM',
61+
'RemoteVM = qubes.vm.remotevm:RemoteVM',
6162
],
6263
'qubes.ext': [
6364
'qubes.ext.admin = qubes.ext.admin:AdminExtension',

0 commit comments

Comments
 (0)